1namespace ts.server { 2 interface StackTraceError extends Error { 3 stack?: string; 4 } 5 6 export interface ServerCancellationToken extends HostCancellationToken { 7 setRequest(requestId: number): void; 8 resetRequest(requestId: number): void; 9 } 10 11 export const nullCancellationToken: ServerCancellationToken = { 12 isCancellationRequested: () => false, 13 setRequest: () => void 0, 14 resetRequest: () => void 0 15 }; 16 17 function hrTimeToMilliseconds(time: number[]): number { 18 const seconds = time[0]; 19 const nanoseconds = time[1]; 20 return ((1e9 * seconds) + nanoseconds) / 1000000.0; 21 } 22 23 function isDeclarationFileInJSOnlyNonConfiguredProject(project: Project, file: NormalizedPath) { 24 // Checking for semantic diagnostics is an expensive process. We want to avoid it if we 25 // know for sure it is not needed. 26 // For instance, .d.ts files injected by ATA automatically do not produce any relevant 27 // errors to a JS- only project. 28 // 29 // Note that configured projects can set skipLibCheck (on by default in jsconfig.json) to 30 // disable checking for declaration files. We only need to verify for inferred projects (e.g. 31 // miscellaneous context in VS) and external projects(e.g.VS.csproj project) with only JS 32 // files. 33 // 34 // We still want to check .js files in a JS-only inferred or external project (e.g. if the 35 // file has '// @ts-check'). 36 37 if ((isInferredProject(project) || isExternalProject(project)) && 38 project.isJsOnlyProject()) { 39 const scriptInfo = project.getScriptInfoForNormalizedPath(file); 40 return scriptInfo && !scriptInfo.isJavaScript(); 41 } 42 return false; 43 } 44 45 46 function dtsChangeCanAffectEmit(compilationSettings: CompilerOptions) { 47 return getEmitDeclarations(compilationSettings) || !!compilationSettings.emitDecoratorMetadata; 48 } 49 50 function formatDiag(fileName: NormalizedPath, project: Project, diag: Diagnostic): protocol.Diagnostic { 51 const scriptInfo = project.getScriptInfoForNormalizedPath(fileName)!; // TODO: GH#18217 52 return { 53 start: scriptInfo.positionToLineOffset(diag.start!), 54 end: scriptInfo.positionToLineOffset(diag.start! + diag.length!), // TODO: GH#18217 55 text: flattenDiagnosticMessageText(diag.messageText, "\n"), 56 code: diag.code, 57 category: diagnosticCategoryName(diag), 58 reportsUnnecessary: diag.reportsUnnecessary, 59 reportsDeprecated: diag.reportsDeprecated, 60 source: diag.source, 61 relatedInformation: map(diag.relatedInformation, formatRelatedInformation), 62 }; 63 } 64 65 function formatRelatedInformation(info: DiagnosticRelatedInformation): protocol.DiagnosticRelatedInformation { 66 if (!info.file) { 67 return { 68 message: flattenDiagnosticMessageText(info.messageText, "\n"), 69 category: diagnosticCategoryName(info), 70 code: info.code 71 }; 72 } 73 return { 74 span: { 75 start: convertToLocation(getLineAndCharacterOfPosition(info.file, info.start!)), 76 end: convertToLocation(getLineAndCharacterOfPosition(info.file, info.start! + info.length!)), // TODO: GH#18217 77 file: info.file.fileName 78 }, 79 message: flattenDiagnosticMessageText(info.messageText, "\n"), 80 category: diagnosticCategoryName(info), 81 code: info.code 82 }; 83 } 84 85 function convertToLocation(lineAndCharacter: LineAndCharacter): protocol.Location { 86 return { line: lineAndCharacter.line + 1, offset: lineAndCharacter.character + 1 }; 87 } 88 89 function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName; 90 function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: false): protocol.Diagnostic; 91 function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName { 92 const start = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start!)))!; // TODO: GH#18217 93 const end = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start! + diag.length!)))!; // TODO: GH#18217 94 const text = flattenDiagnosticMessageText(diag.messageText, "\n"); 95 const { code, source } = diag; 96 const category = diagnosticCategoryName(diag); 97 const common = { 98 start, 99 end, 100 text, 101 code, 102 category, 103 reportsUnnecessary: diag.reportsUnnecessary, 104 reportsDeprecated: diag.reportsDeprecated, 105 source, 106 relatedInformation: map(diag.relatedInformation, formatRelatedInformation), 107 }; 108 return includeFileName 109 ? { ...common, fileName: diag.file && diag.file.fileName } 110 : common; 111 } 112 113 export interface PendingErrorCheck { 114 fileName: NormalizedPath; 115 project: Project; 116 } 117 118 function allEditsBeforePos(edits: readonly TextChange[], pos: number): boolean { 119 return edits.every(edit => textSpanEnd(edit.span) < pos); 120 } 121 122 // CommandNames used to be exposed before TS 2.4 as a namespace 123 // In TS 2.4 we switched to an enum, keep this for backward compatibility 124 // The var assignment ensures that even though CommandTypes are a const enum 125 // we want to ensure the value is maintained in the out since the file is 126 // built using --preseveConstEnum. 127 export type CommandNames = protocol.CommandTypes; 128 export const CommandNames = (protocol as any).CommandTypes; 129 130 export function formatMessage<T extends protocol.Message>(msg: T, logger: Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { 131 const verboseLogging = logger.hasLevel(LogLevel.verbose); 132 133 const json = JSON.stringify(msg); 134 if (verboseLogging) { 135 logger.info(`${msg.type}:${indent(json)}`); 136 } 137 138 const len = byteLength(json, "utf8"); 139 return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`; 140 } 141 142 /** 143 * Allows to schedule next step in multistep operation 144 */ 145 interface NextStep { 146 immediate(action: () => void): void; 147 delay(ms: number, action: () => void): void; 148 } 149 150 /** 151 * External capabilities used by multistep operation 152 */ 153 interface MultistepOperationHost { 154 getCurrentRequestId(): number; 155 sendRequestCompletedEvent(requestId: number): void; 156 getServerHost(): ServerHost; 157 isCancellationRequested(): boolean; 158 executeWithRequestId(requestId: number, action: () => void): void; 159 logError(error: Error, message: string): void; 160 } 161 162 /** 163 * Represents operation that can schedule its next step to be executed later. 164 * Scheduling is done via instance of NextStep. If on current step subsequent step was not scheduled - operation is assumed to be completed. 165 */ 166 class MultistepOperation implements NextStep { 167 private requestId: number | undefined; 168 private timerHandle: any; 169 private immediateId: number | undefined; 170 171 constructor(private readonly operationHost: MultistepOperationHost) { } 172 173 public startNew(action: (next: NextStep) => void) { 174 this.complete(); 175 this.requestId = this.operationHost.getCurrentRequestId(); 176 this.executeAction(action); 177 } 178 179 private complete() { 180 if (this.requestId !== undefined) { 181 this.operationHost.sendRequestCompletedEvent(this.requestId); 182 this.requestId = undefined; 183 } 184 this.setTimerHandle(undefined); 185 this.setImmediateId(undefined); 186 } 187 188 public immediate(action: () => void) { 189 const requestId = this.requestId!; 190 Debug.assert(requestId === this.operationHost.getCurrentRequestId(), "immediate: incorrect request id"); 191 this.setImmediateId(this.operationHost.getServerHost().setImmediate(() => { 192 this.immediateId = undefined; 193 this.operationHost.executeWithRequestId(requestId, () => this.executeAction(action)); 194 })); 195 } 196 197 public delay(ms: number, action: () => void) { 198 const requestId = this.requestId!; 199 Debug.assert(requestId === this.operationHost.getCurrentRequestId(), "delay: incorrect request id"); 200 this.setTimerHandle(this.operationHost.getServerHost().setTimeout(() => { 201 this.timerHandle = undefined; 202 this.operationHost.executeWithRequestId(requestId, () => this.executeAction(action)); 203 }, ms)); 204 } 205 206 private executeAction(action: (next: NextStep) => void) { 207 let stop = false; 208 try { 209 if (this.operationHost.isCancellationRequested()) { 210 stop = true; 211 tracing?.instant(tracing.Phase.Session, "stepCanceled", { seq: this.requestId, early: true }); 212 } 213 else { 214 tracing?.push(tracing.Phase.Session, "stepAction", { seq: this.requestId }); 215 action(this); 216 tracing?.pop(); 217 } 218 } 219 catch (e) { 220 // Cancellation or an error may have left incomplete events on the tracing stack. 221 tracing?.popAll(); 222 223 stop = true; 224 // ignore cancellation request 225 if (e instanceof OperationCanceledException) { 226 tracing?.instant(tracing.Phase.Session, "stepCanceled", { seq: this.requestId }); 227 } 228 else { 229 tracing?.instant(tracing.Phase.Session, "stepError", { seq: this.requestId, message: (e as Error).message }); 230 this.operationHost.logError(e, `delayed processing of request ${this.requestId}`); 231 } 232 } 233 if (stop || !this.hasPendingWork()) { 234 this.complete(); 235 } 236 } 237 238 private setTimerHandle(timerHandle: any) { 239 if (this.timerHandle !== undefined) { 240 this.operationHost.getServerHost().clearTimeout(this.timerHandle); 241 } 242 this.timerHandle = timerHandle; 243 } 244 245 private setImmediateId(immediateId: number | undefined) { 246 if (this.immediateId !== undefined) { 247 this.operationHost.getServerHost().clearImmediate(this.immediateId); 248 } 249 this.immediateId = immediateId; 250 } 251 252 private hasPendingWork() { 253 return !!this.timerHandle || !!this.immediateId; 254 } 255 } 256 257 export type Event = <T extends object>(body: T, eventName: string) => void; 258 259 export interface EventSender { 260 event: Event; 261 } 262 263 /** @internal */ 264 export function toEvent(eventName: string, body: object): protocol.Event { 265 return { 266 seq: 0, 267 type: "event", 268 event: eventName, 269 body 270 }; 271 } 272 273 type Projects = readonly Project[] | { 274 readonly projects: readonly Project[]; 275 readonly symLinkedProjects: MultiMap<Path, Project>; 276 }; 277 278 /** 279 * This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. 280 */ 281 function combineProjectOutput<T, U>( 282 defaultValue: T, 283 getValue: (path: Path) => T, 284 projects: Projects, 285 action: (project: Project, value: T) => readonly U[] | U | undefined, 286 ): U[] { 287 const outputs = flatMapToMutable(isArray(projects) ? projects : projects.projects, project => action(project, defaultValue)); 288 if (!isArray(projects) && projects.symLinkedProjects) { 289 projects.symLinkedProjects.forEach((projects, path) => { 290 const value = getValue(path); 291 outputs.push(...flatMap(projects, project => action(project, value))); 292 }); 293 } 294 return deduplicate(outputs, equateValues); 295 } 296 297 interface ProjectNavigateToItems { 298 project: Project; 299 navigateToItems: readonly NavigateToItem[]; 300 } 301 302 function createDocumentSpanSet(): Set<DocumentSpan> { 303 return createSet(({textSpan}) => textSpan.start + 100003 * textSpan.length, documentSpansEqual); 304 } 305 306 function getRenameLocationsWorker( 307 projects: Projects, 308 defaultProject: Project, 309 initialLocation: DocumentPosition, 310 findInStrings: boolean, 311 findInComments: boolean, 312 { providePrefixAndSuffixTextForRename }: UserPreferences 313 ): readonly RenameLocation[] { 314 const perProjectResults = getPerProjectReferences( 315 projects, 316 defaultProject, 317 initialLocation, 318 /*isForRename*/ true, 319 (project, position) => project.getLanguageService().findRenameLocations(position.fileName, position.pos, findInStrings, findInComments, providePrefixAndSuffixTextForRename), 320 (renameLocation, cb) => cb(documentSpanLocation(renameLocation)), 321 ); 322 323 // No filtering or dedup'ing is required if there's exactly one project 324 if (isArray(perProjectResults)) { 325 return perProjectResults; 326 } 327 328 const results: RenameLocation[] = []; 329 const seen = createDocumentSpanSet(); 330 331 perProjectResults.forEach((projectResults, project) => { 332 for (const result of projectResults) { 333 // If there's a mapped location, it'll appear in the results for another project 334 if (!seen.has(result) && !getMappedLocationForProject(documentSpanLocation(result), project)) { 335 results.push(result); 336 seen.add(result); 337 } 338 } 339 }); 340 341 return results; 342 } 343 344 function getDefinitionLocation(defaultProject: Project, initialLocation: DocumentPosition, isForRename: boolean): DocumentPosition | undefined { 345 const infos = defaultProject.getLanguageService().getDefinitionAtPosition(initialLocation.fileName, initialLocation.pos, /*searchOtherFilesOnly*/ false, /*stopAtAlias*/ isForRename); 346 const info = infos && firstOrUndefined(infos); 347 // Note that the value of `isLocal` may depend on whether or not the checker has run on the containing file 348 // (implying that FAR cascading behavior may depend on request order) 349 return info && !info.isLocal ? { fileName: info.fileName, pos: info.textSpan.start } : undefined; 350 } 351 352 function getReferencesWorker( 353 projects: Projects, 354 defaultProject: Project, 355 initialLocation: DocumentPosition, 356 logger: Logger, 357 ): readonly ReferencedSymbol[] { 358 const perProjectResults = getPerProjectReferences( 359 projects, 360 defaultProject, 361 initialLocation, 362 /*isForRename*/ false, 363 (project, position) => { 364 logger.info(`Finding references to ${position.fileName} position ${position.pos} in project ${project.getProjectName()}`); 365 return project.getLanguageService().findReferences(position.fileName, position.pos); 366 }, 367 (referencedSymbol, cb) => { 368 cb(documentSpanLocation(referencedSymbol.definition)); 369 for (const ref of referencedSymbol.references) { 370 cb(documentSpanLocation(ref)); 371 } 372 }, 373 ); 374 375 // No re-mapping or isDefinition updatses are required if there's exactly one project 376 if (isArray(perProjectResults)) { 377 return perProjectResults; 378 } 379 380 // `isDefinition` is only (definitely) correct in `defaultProject` because we might 381 // have started the other project searches from related symbols. Propagate the 382 // correct results to all other projects. 383 384 const defaultProjectResults = perProjectResults.get(defaultProject); 385 if (defaultProjectResults?.[0]?.references[0]?.isDefinition === undefined) { 386 // Clear all isDefinition properties 387 perProjectResults.forEach(projectResults => { 388 for (const referencedSymbol of projectResults) { 389 for (const ref of referencedSymbol.references) { 390 delete ref.isDefinition; 391 } 392 } 393 }); 394 } 395 else { 396 // Correct isDefinition properties from projects other than defaultProject 397 const knownSymbolSpans = createDocumentSpanSet(); 398 for (const referencedSymbol of defaultProjectResults) { 399 for (const ref of referencedSymbol.references) { 400 if (ref.isDefinition) { 401 knownSymbolSpans.add(ref); 402 // One is enough - updateIsDefinitionOfReferencedSymbols will fill out the set based on symbols 403 break; 404 } 405 } 406 } 407 408 const updatedProjects = new Set<Project>(); 409 while (true) { 410 let progress = false; 411 perProjectResults.forEach((referencedSymbols, project) => { 412 if (updatedProjects.has(project)) return; 413 const updated = project.getLanguageService().updateIsDefinitionOfReferencedSymbols(referencedSymbols, knownSymbolSpans); 414 if (updated) { 415 updatedProjects.add(project); 416 progress = true; 417 } 418 }); 419 if (!progress) break; 420 } 421 422 perProjectResults.forEach((referencedSymbols, project) => { 423 if (updatedProjects.has(project)) return; 424 for (const referencedSymbol of referencedSymbols) { 425 for (const ref of referencedSymbol.references) { 426 ref.isDefinition = false; 427 } 428 } 429 }); 430 } 431 432 // We need to de-duplicate and aggregate the results by choosing an authoritative version 433 // of each definition and merging references from all the projects where they appear. 434 435 const results: ReferencedSymbol[] = []; 436 const seenRefs = createDocumentSpanSet(); // It doesn't make sense to have a reference in two definition lists, so we de-dup globally 437 438 // TODO: We might end up with a more logical allocation of refs to defs if we pre-sorted the defs by descending ref-count. 439 // Otherwise, it just ends up attached to the first corresponding def we happen to process. The others may or may not be 440 // dropped later when we check for defs with ref-count 0. 441 perProjectResults.forEach((projectResults, project) => { 442 for (const referencedSymbol of projectResults) { 443 const mappedDefinitionFile = getMappedLocationForProject(documentSpanLocation(referencedSymbol.definition), project); 444 const definition: ReferencedSymbolDefinitionInfo = mappedDefinitionFile === undefined ? 445 referencedSymbol.definition : 446 { 447 ...referencedSymbol.definition, 448 textSpan: createTextSpan(mappedDefinitionFile.pos, referencedSymbol.definition.textSpan.length), // Why would the length be the same in the original? 449 fileName: mappedDefinitionFile.fileName, 450 contextSpan: getMappedContextSpanForProject(referencedSymbol.definition, project) 451 }; 452 453 let symbolToAddTo = find(results, o => documentSpansEqual(o.definition, definition)); 454 if (!symbolToAddTo) { 455 symbolToAddTo = { definition, references: [] }; 456 results.push(symbolToAddTo); 457 } 458 459 for (const ref of referencedSymbol.references) { 460 if (!seenRefs.has(ref) && !getMappedLocationForProject(documentSpanLocation(ref), project)) { 461 seenRefs.add(ref); 462 symbolToAddTo.references.push(ref); 463 } 464 } 465 } 466 }); 467 468 return results.filter(o => o.references.length !== 0); 469 } 470 471 interface ProjectAndLocation { 472 readonly project: Project; 473 readonly location: DocumentPosition; 474 } 475 476 function forEachProjectInProjects(projects: Projects, path: string | undefined, cb: (project: Project, path: string | undefined) => void): void { 477 for (const project of isArray(projects) ? projects : projects.projects) { 478 cb(project, path); 479 } 480 if (!isArray(projects) && projects.symLinkedProjects) { 481 projects.symLinkedProjects.forEach((symlinkedProjects, symlinkedPath) => { 482 for (const project of symlinkedProjects) { 483 cb(project, symlinkedPath); 484 } 485 }); 486 } 487 } 488 489 /** 490 * @param projects Projects initially known to contain {@link initialLocation} 491 * @param defaultProject The default project containing {@link initialLocation} 492 * @param initialLocation Where the search operation was triggered 493 * @param getResultsForPosition This is where you plug in `findReferences`, `renameLocation`, etc 494 * @param forPositionInResult Given an item returned by {@link getResultsForPosition} enumerate the positions referred to by that result 495 * @returns In the common case where there's only one project, returns an array of results from {@link getResultsForPosition}. 496 * If multiple projects were searched - even if they didn't return results - the result will be a map from project to per-project results. 497 */ 498 function getPerProjectReferences<TResult>( 499 projects: Projects, 500 defaultProject: Project, 501 initialLocation: DocumentPosition, 502 isForRename: boolean, 503 getResultsForPosition: (project: Project, location: DocumentPosition) => readonly TResult[] | undefined, 504 forPositionInResult: (result: TResult, cb: (location: DocumentPosition) => void) => void, 505 ): readonly TResult[] | ESMap<Project, readonly TResult[]> { 506 // If `getResultsForPosition` returns results for a project, they go in here 507 const resultsMap = new Map<Project, readonly TResult[]>(); 508 509 const queue = createQueue<ProjectAndLocation>(); 510 511 // In order to get accurate isDefinition values for `defaultProject`, 512 // we need to ensure that it is searched from `initialLocation`. 513 // The easiest way to do this is to search it first. 514 queue.enqueue({ project: defaultProject, location: initialLocation }); 515 516 // This will queue `defaultProject` a second time, but it will be dropped 517 // as a dup when it is dequeued. 518 forEachProjectInProjects(projects, initialLocation.fileName, (project, path) => { 519 const location = { fileName: path!, pos: initialLocation.pos }; 520 queue.enqueue({ project, location }); 521 }); 522 523 const projectService = defaultProject.projectService; 524 const cancellationToken = defaultProject.getCancellationToken(); 525 526 const defaultDefinition = getDefinitionLocation(defaultProject, initialLocation, isForRename); 527 528 // Don't call these unless !!defaultDefinition 529 const getGeneratedDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition!.fileName) ? 530 defaultDefinition : 531 defaultProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(defaultDefinition!)); 532 const getSourceDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition!.fileName) ? 533 defaultDefinition : 534 defaultProject.getLanguageService().getSourceMapper().tryGetSourcePosition(defaultDefinition!)); 535 536 // The keys of resultsMap allow us to check which projects have already been searched, but we also 537 // maintain a set of strings because that's what `loadAncestorProjectTree` wants. 538 const searchedProjectKeys = new Set<string>(); 539 540 onCancellation: 541 while (!queue.isEmpty()) { 542 while (!queue.isEmpty()) { 543 if (cancellationToken.isCancellationRequested()) break onCancellation; 544 545 const { project, location } = queue.dequeue(); 546 547 if (resultsMap.has(project)) continue; 548 if (isLocationProjectReferenceRedirect(project, location)) continue; 549 550 const projectResults = searchPosition(project, location); 551 resultsMap.set(project, projectResults ?? emptyArray); 552 searchedProjectKeys.add(getProjectKey(project)); 553 } 554 555 // At this point, we know about all projects passed in as arguments and any projects in which 556 // `getResultsForPosition` has returned results. We expand that set to include any projects 557 // downstream from any of these and then queue new initial-position searches for any new project 558 // containing `initialLocation`. 559 if (defaultDefinition) { 560 // This seems to mean "load all projects downstream from any member of `seenProjects`". 561 projectService.loadAncestorProjectTree(searchedProjectKeys); 562 projectService.forEachEnabledProject(project => { 563 if (cancellationToken.isCancellationRequested()) return; // There's no mechanism for skipping the remaining projects 564 if (resultsMap.has(project)) return; // Can loop forever without this (enqueue here, dequeue above, repeat) 565 const location = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition); 566 if (location) { 567 queue.enqueue({ project, location }); 568 } 569 }); 570 } 571 } 572 573 // In the common case where there's only one project, return a simpler result to make 574 // it easier for the caller to skip post-processing. 575 if (resultsMap.size === 1) { 576 const it = resultsMap.values().next(); 577 Debug.assert(!it.done); 578 return it.value; 579 } 580 581 return resultsMap; 582 583 function searchPosition(project: Project, location: DocumentPosition): readonly TResult[] | undefined { 584 const projectResults = getResultsForPosition(project, location); 585 if (!projectResults) return undefined; 586 587 for (const result of projectResults) { 588 forPositionInResult(result, position => { 589 // This may trigger a search for a tsconfig, but there are several layers of caching that make it inexpensive 590 const originalLocation = projectService.getOriginalLocationEnsuringConfiguredProject(project, position); 591 if (!originalLocation) return; 592 593 const originalScriptInfo = projectService.getScriptInfo(originalLocation.fileName)!; 594 595 for (const project of originalScriptInfo.containingProjects) { 596 if (!project.isOrphan() && !resultsMap.has(project)) { // Optimization: don't enqueue if will be discarded 597 queue.enqueue({ project, location: originalLocation }); 598 } 599 } 600 601 const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalScriptInfo); 602 if (symlinkedProjectsMap) { 603 symlinkedProjectsMap.forEach((symlinkedProjects, symlinkedPath) => { 604 for (const symlinkedProject of symlinkedProjects) { 605 if (!symlinkedProject.isOrphan() && !resultsMap.has(symlinkedProject)) { // Optimization: don't enqueue if will be discarded 606 queue.enqueue({ project: symlinkedProject, location: { fileName: symlinkedPath as string, pos: originalLocation.pos } }); 607 } 608 } 609 }); 610 } 611 }); 612 } 613 614 return projectResults; 615 } 616 } 617 618 function mapDefinitionInProject( 619 definition: DocumentPosition, 620 project: Project, 621 getGeneratedDefinition: () => DocumentPosition | undefined, 622 getSourceDefinition: () => DocumentPosition | undefined 623 ): DocumentPosition | undefined { 624 // If the definition is actually from the project, definition is correct as is 625 if (project.containsFile(toNormalizedPath(definition.fileName)) && 626 !isLocationProjectReferenceRedirect(project, definition)) { 627 return definition; 628 } 629 const generatedDefinition = getGeneratedDefinition(); 630 if (generatedDefinition && project.containsFile(toNormalizedPath(generatedDefinition.fileName))) return generatedDefinition; 631 const sourceDefinition = getSourceDefinition(); 632 return sourceDefinition && project.containsFile(toNormalizedPath(sourceDefinition.fileName)) ? sourceDefinition : undefined; 633 } 634 635 function isLocationProjectReferenceRedirect(project: Project, location: DocumentPosition | undefined) { 636 if (!location) return false; 637 const program = project.getLanguageService().getProgram(); 638 if (!program) return false; 639 const sourceFile = program.getSourceFile(location.fileName); 640 641 // It is possible that location is attached to project but 642 // the program actually includes its redirect instead. 643 // This happens when rootFile in project is one of the file from referenced project 644 // Thus root is attached but program doesnt have the actual .ts file but .d.ts 645 // If this is not the file we were actually looking, return rest of the toDo 646 return !!sourceFile && 647 sourceFile.resolvedPath !== sourceFile.path && 648 sourceFile.resolvedPath !== project.toPath(location.fileName); 649 } 650 651 function getProjectKey(project: Project) { 652 return isConfiguredProject(project) ? project.canonicalConfigFilePath : project.getProjectName(); 653 } 654 655 function documentSpanLocation({ fileName, textSpan }: DocumentSpan): DocumentPosition { 656 return { fileName, pos: textSpan.start }; 657 } 658 659 function getMappedLocationForProject(location: DocumentPosition, project: Project): DocumentPosition | undefined { 660 return getMappedLocation(location, project.getSourceMapper(), p => project.projectService.fileExists(p as NormalizedPath)); 661 } 662 663 function getMappedDocumentSpanForProject(documentSpan: DocumentSpan, project: Project): DocumentSpan | undefined { 664 return getMappedDocumentSpan(documentSpan, project.getSourceMapper(), p => project.projectService.fileExists(p as NormalizedPath)); 665 } 666 667 function getMappedContextSpanForProject(documentSpan: DocumentSpan, project: Project): TextSpan | undefined { 668 return getMappedContextSpan(documentSpan, project.getSourceMapper(), p => project.projectService.fileExists(p as NormalizedPath)); 669 } 670 671 const invalidPartialSemanticModeCommands: readonly CommandNames[] = [ 672 CommandNames.OpenExternalProject, 673 CommandNames.OpenExternalProjects, 674 CommandNames.CloseExternalProject, 675 CommandNames.SynchronizeProjectList, 676 CommandNames.EmitOutput, 677 CommandNames.CompileOnSaveAffectedFileList, 678 CommandNames.CompileOnSaveEmitFile, 679 CommandNames.CompilerOptionsDiagnosticsFull, 680 CommandNames.EncodedSemanticClassificationsFull, 681 CommandNames.SemanticDiagnosticsSync, 682 CommandNames.SuggestionDiagnosticsSync, 683 CommandNames.GeterrForProject, 684 CommandNames.Reload, 685 CommandNames.ReloadProjects, 686 CommandNames.GetCodeFixes, 687 CommandNames.GetCodeFixesFull, 688 CommandNames.GetCombinedCodeFix, 689 CommandNames.GetCombinedCodeFixFull, 690 CommandNames.ApplyCodeActionCommand, 691 CommandNames.GetSupportedCodeFixes, 692 CommandNames.GetApplicableRefactors, 693 CommandNames.GetEditsForRefactor, 694 CommandNames.GetEditsForRefactorFull, 695 CommandNames.OrganizeImports, 696 CommandNames.OrganizeImportsFull, 697 CommandNames.GetEditsForFileRename, 698 CommandNames.GetEditsForFileRenameFull, 699 CommandNames.PrepareCallHierarchy, 700 CommandNames.ProvideCallHierarchyIncomingCalls, 701 CommandNames.ProvideCallHierarchyOutgoingCalls, 702 ]; 703 704 const invalidSyntacticModeCommands: readonly CommandNames[] = [ 705 ...invalidPartialSemanticModeCommands, 706 CommandNames.Definition, 707 CommandNames.DefinitionFull, 708 CommandNames.DefinitionAndBoundSpan, 709 CommandNames.DefinitionAndBoundSpanFull, 710 CommandNames.TypeDefinition, 711 CommandNames.Implementation, 712 CommandNames.ImplementationFull, 713 CommandNames.References, 714 CommandNames.ReferencesFull, 715 CommandNames.Rename, 716 CommandNames.RenameLocationsFull, 717 CommandNames.RenameInfoFull, 718 CommandNames.Quickinfo, 719 CommandNames.QuickinfoFull, 720 CommandNames.CompletionInfo, 721 CommandNames.Completions, 722 CommandNames.CompletionsFull, 723 CommandNames.CompletionDetails, 724 CommandNames.CompletionDetailsFull, 725 CommandNames.SignatureHelp, 726 CommandNames.SignatureHelpFull, 727 CommandNames.Navto, 728 CommandNames.NavtoFull, 729 CommandNames.Occurrences, 730 CommandNames.DocumentHighlights, 731 CommandNames.DocumentHighlightsFull, 732 ]; 733 734 export interface SessionOptions { 735 host: ServerHost; 736 cancellationToken: ServerCancellationToken; 737 useSingleInferredProject: boolean; 738 useInferredProjectPerProjectRoot: boolean; 739 typingsInstaller: ITypingsInstaller; 740 byteLength: (buf: string, encoding?: string) => number; 741 hrtime: (start?: number[]) => number[]; 742 logger: Logger; 743 /** 744 * If falsy, all events are suppressed. 745 */ 746 canUseEvents: boolean; 747 eventHandler?: ProjectServiceEventHandler; 748 /** Has no effect if eventHandler is also specified. */ 749 suppressDiagnosticEvents?: boolean; 750 /** @deprecated use serverMode instead */ 751 syntaxOnly?: boolean; 752 serverMode?: LanguageServiceMode; 753 throttleWaitMilliseconds?: number; 754 noGetErrOnBackgroundUpdate?: boolean; 755 756 globalPlugins?: readonly string[]; 757 pluginProbeLocations?: readonly string[]; 758 allowLocalPluginLoads?: boolean; 759 typesMapLocation?: string; 760 } 761 762 export class Session<TMessage = string> implements EventSender { 763 private readonly gcTimer: GcTimer; 764 protected projectService: ProjectService; 765 private changeSeq = 0; 766 767 private performanceData: protocol.PerformanceData | undefined; 768 769 private currentRequestId!: number; 770 private errorCheck: MultistepOperation; 771 772 protected host: ServerHost; 773 private readonly cancellationToken: ServerCancellationToken; 774 protected readonly typingsInstaller: ITypingsInstaller; 775 protected byteLength: (buf: string, encoding?: string) => number; 776 private hrtime: (start?: number[]) => number[]; 777 protected logger: Logger; 778 779 protected canUseEvents: boolean; 780 private suppressDiagnosticEvents?: boolean; 781 private eventHandler: ProjectServiceEventHandler | undefined; 782 private readonly noGetErrOnBackgroundUpdate?: boolean; 783 784 constructor(opts: SessionOptions) { 785 this.host = opts.host; 786 this.cancellationToken = opts.cancellationToken; 787 this.typingsInstaller = opts.typingsInstaller; 788 this.byteLength = opts.byteLength; 789 this.hrtime = opts.hrtime; 790 this.logger = opts.logger; 791 this.canUseEvents = opts.canUseEvents; 792 this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents; 793 this.noGetErrOnBackgroundUpdate = opts.noGetErrOnBackgroundUpdate; 794 795 const { throttleWaitMilliseconds } = opts; 796 797 this.eventHandler = this.canUseEvents 798 ? opts.eventHandler || (event => this.defaultEventHandler(event)) 799 : undefined; 800 const multistepOperationHost: MultistepOperationHost = { 801 executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action), 802 getCurrentRequestId: () => this.currentRequestId, 803 getServerHost: () => this.host, 804 logError: (err, cmd) => this.logError(err, cmd), 805 sendRequestCompletedEvent: requestId => this.sendRequestCompletedEvent(requestId), 806 isCancellationRequested: () => this.cancellationToken.isCancellationRequested() 807 }; 808 this.errorCheck = new MultistepOperation(multistepOperationHost); 809 const settings: ProjectServiceOptions = { 810 host: this.host, 811 logger: this.logger, 812 cancellationToken: this.cancellationToken, 813 useSingleInferredProject: opts.useSingleInferredProject, 814 useInferredProjectPerProjectRoot: opts.useInferredProjectPerProjectRoot, 815 typingsInstaller: this.typingsInstaller, 816 throttleWaitMilliseconds, 817 eventHandler: this.eventHandler, 818 suppressDiagnosticEvents: this.suppressDiagnosticEvents, 819 globalPlugins: opts.globalPlugins, 820 pluginProbeLocations: opts.pluginProbeLocations, 821 allowLocalPluginLoads: opts.allowLocalPluginLoads, 822 typesMapLocation: opts.typesMapLocation, 823 syntaxOnly: opts.syntaxOnly, 824 serverMode: opts.serverMode, 825 session: this 826 }; 827 this.projectService = new ProjectService(settings); 828 this.projectService.setPerformanceEventHandler(this.performanceEventHandler.bind(this)); 829 this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); 830 831 // Make sure to setup handlers to throw error for not allowed commands on syntax server 832 switch (this.projectService.serverMode) { 833 case LanguageServiceMode.Semantic: 834 break; 835 case LanguageServiceMode.PartialSemantic: 836 invalidPartialSemanticModeCommands.forEach(commandName => 837 this.handlers.set(commandName, request => { 838 throw new Error(`Request: ${request.command} not allowed in LanguageServiceMode.PartialSemantic`); 839 }) 840 ); 841 break; 842 case LanguageServiceMode.Syntactic: 843 invalidSyntacticModeCommands.forEach(commandName => 844 this.handlers.set(commandName, request => { 845 throw new Error(`Request: ${request.command} not allowed in LanguageServiceMode.Syntactic`); 846 }) 847 ); 848 break; 849 default: 850 Debug.assertNever(this.projectService.serverMode); 851 } 852 } 853 854 private sendRequestCompletedEvent(requestId: number): void { 855 this.event<protocol.RequestCompletedEventBody>({ request_seq: requestId }, "requestCompleted"); 856 } 857 858 private addPerformanceData(key: keyof protocol.PerformanceData, value: number) { 859 if (!this.performanceData) { 860 this.performanceData = {}; 861 } 862 this.performanceData[key] = (this.performanceData[key] ?? 0) + value; 863 } 864 865 private performanceEventHandler(event: PerformanceEvent) { 866 switch (event.kind) { 867 case "UpdateGraph": 868 this.addPerformanceData("updateGraphDurationMs", event.durationMs); 869 break; 870 case "CreatePackageJsonAutoImportProvider": 871 this.addPerformanceData("createAutoImportProviderProgramDurationMs", event.durationMs); 872 break; 873 } 874 } 875 876 private defaultEventHandler(event: ProjectServiceEvent) { 877 switch (event.eventName) { 878 case ProjectsUpdatedInBackgroundEvent: 879 const { openFiles } = event.data; 880 this.projectsUpdatedInBackgroundEvent(openFiles); 881 break; 882 case ProjectLoadingStartEvent: 883 const { project, reason } = event.data; 884 this.event<protocol.ProjectLoadingStartEventBody>( 885 { projectName: project.getProjectName(), reason }, 886 ProjectLoadingStartEvent); 887 break; 888 case ProjectLoadingFinishEvent: 889 const { project: finishProject } = event.data; 890 this.event<protocol.ProjectLoadingFinishEventBody>({ projectName: finishProject.getProjectName() }, ProjectLoadingFinishEvent); 891 break; 892 case LargeFileReferencedEvent: 893 const { file, fileSize, maxFileSize } = event.data; 894 this.event<protocol.LargeFileReferencedEventBody>({ file, fileSize, maxFileSize }, LargeFileReferencedEvent); 895 break; 896 case ConfigFileDiagEvent: 897 const { triggerFile, configFileName: configFile, diagnostics } = event.data; 898 const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true)); 899 this.event<protocol.ConfigFileDiagnosticEventBody>({ 900 triggerFile, 901 configFile, 902 diagnostics: bakedDiags 903 }, ConfigFileDiagEvent); 904 break; 905 case ProjectLanguageServiceStateEvent: { 906 const eventName: protocol.ProjectLanguageServiceStateEventName = ProjectLanguageServiceStateEvent; 907 this.event<protocol.ProjectLanguageServiceStateEventBody>({ 908 projectName: event.data.project.getProjectName(), 909 languageServiceEnabled: event.data.languageServiceEnabled 910 }, eventName); 911 break; 912 } 913 case ProjectInfoTelemetryEvent: { 914 const eventName: protocol.TelemetryEventName = "telemetry"; 915 this.event<protocol.TelemetryEventBody>({ 916 telemetryEventName: event.eventName, 917 payload: event.data, 918 }, eventName); 919 break; 920 } 921 } 922 } 923 924 private projectsUpdatedInBackgroundEvent(openFiles: string[]): void { 925 this.projectService.logger.info(`got projects updated in background, updating diagnostics for ${openFiles}`); 926 if (openFiles.length) { 927 if (!this.suppressDiagnosticEvents && !this.noGetErrOnBackgroundUpdate) { 928 // For now only queue error checking for open files. We can change this to include non open files as well 929 this.errorCheck.startNew(next => this.updateErrorCheck(next, openFiles, 100, /*requireOpen*/ true)); 930 } 931 932 // Send project changed event 933 this.event<protocol.ProjectsUpdatedInBackgroundEventBody>({ 934 openFiles 935 }, ProjectsUpdatedInBackgroundEvent); 936 } 937 } 938 939 public logError(err: Error, cmd: string): void { 940 this.logErrorWorker(err, cmd); 941 } 942 943 private logErrorWorker(err: Error & PossibleProgramFileInfo, cmd: string, fileRequest?: protocol.FileRequestArgs): void { 944 let msg = "Exception on executing command " + cmd; 945 if (err.message) { 946 msg += ":\n" + indent(err.message); 947 if ((err as StackTraceError).stack) { 948 msg += "\n" + indent((err as StackTraceError).stack!); 949 } 950 } 951 952 if (this.logger.hasLevel(LogLevel.verbose)) { 953 if (fileRequest) { 954 try { 955 const { file, project } = this.getFileAndProject(fileRequest); 956 const scriptInfo = project.getScriptInfoForNormalizedPath(file); 957 if (scriptInfo) { 958 const text = getSnapshotText(scriptInfo.getSnapshot()); 959 msg += `\n\nFile text of ${fileRequest.file}:${indent(text)}\n`; 960 } 961 } 962 catch { } // eslint-disable-line no-empty 963 } 964 965 966 if (err.ProgramFiles) { 967 msg += `\n\nProgram files: ${JSON.stringify(err.ProgramFiles)}\n`; 968 msg += `\n\nProjects::\n`; 969 let counter = 0; 970 const addProjectInfo = (project: Project) => { 971 msg += `\nProject '${project.projectName}' (${ProjectKind[project.projectKind]}) ${counter}\n`; 972 msg += project.filesToString(/*writeProjectFileNames*/ true); 973 msg += "\n-----------------------------------------------\n"; 974 counter++; 975 }; 976 this.projectService.externalProjects.forEach(addProjectInfo); 977 this.projectService.configuredProjects.forEach(addProjectInfo); 978 this.projectService.inferredProjects.forEach(addProjectInfo); 979 } 980 } 981 982 this.logger.msg(msg, Msg.Err); 983 } 984 985 public send(msg: protocol.Message) { 986 if (msg.type === "event" && !this.canUseEvents) { 987 if (this.logger.hasLevel(LogLevel.verbose)) { 988 this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); 989 } 990 return; 991 } 992 this.writeMessage(msg); 993 } 994 995 protected writeMessage(msg: protocol.Message) { 996 const msgText = formatMessage(msg, this.logger, this.byteLength, this.host.newLine); 997 perfLogger.logEvent(`Response message size: ${msgText.length}`); 998 this.host.write(msgText); 999 } 1000 1001 public event<T extends object>(body: T, eventName: string): void { 1002 this.send(toEvent(eventName, body)); 1003 } 1004 1005 // For backwards-compatibility only. 1006 /** @deprecated */ 1007 public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { 1008 this.doOutput(info, cmdName, reqSeq!, /*success*/ !errorMsg, errorMsg); // TODO: GH#18217 1009 } 1010 1011 private doOutput(info: {} | undefined, cmdName: string, reqSeq: number, success: boolean, message?: string): void { 1012 const res: protocol.Response = { 1013 seq: 0, 1014 type: "response", 1015 command: cmdName, 1016 request_seq: reqSeq, 1017 success, 1018 performanceData: this.performanceData 1019 }; 1020 1021 if (success) { 1022 let metadata: unknown; 1023 if (isArray(info)) { 1024 res.body = info; 1025 metadata = (info as WithMetadata<readonly any[]>).metadata; 1026 delete (info as WithMetadata<readonly any[]>).metadata; 1027 } 1028 else if (typeof info === "object") { 1029 if ((info as WithMetadata<{}>).metadata) { 1030 const { metadata: infoMetadata, ...body } = (info as WithMetadata<{}>); 1031 res.body = body; 1032 metadata = infoMetadata; 1033 } 1034 else { 1035 res.body = info; 1036 } 1037 } 1038 else { 1039 res.body = info; 1040 } 1041 if (metadata) res.metadata = metadata; 1042 } 1043 else { 1044 Debug.assert(info === undefined); 1045 } 1046 if (message) { 1047 res.message = message; 1048 } 1049 this.send(res); 1050 } 1051 1052 private semanticCheck(file: NormalizedPath, project: Project) { 1053 tracing?.push(tracing.Phase.Session, "semanticCheck", { file, configFilePath: (project as ConfiguredProject).canonicalConfigFilePath }); // undefined is fine if the cast fails 1054 const diags = isDeclarationFileInJSOnlyNonConfiguredProject(project, file) 1055 ? emptyArray 1056 : project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file); 1057 this.sendDiagnosticsEvent(file, project, diags, "semanticDiag"); 1058 tracing?.pop(); 1059 } 1060 1061 private syntacticCheck(file: NormalizedPath, project: Project) { 1062 tracing?.push(tracing.Phase.Session, "syntacticCheck", { file, configFilePath: (project as ConfiguredProject).canonicalConfigFilePath }); // undefined is fine if the cast fails 1063 this.sendDiagnosticsEvent(file, project, project.getLanguageService().getSyntacticDiagnostics(file), "syntaxDiag"); 1064 tracing?.pop(); 1065 } 1066 1067 private suggestionCheck(file: NormalizedPath, project: Project) { 1068 tracing?.push(tracing.Phase.Session, "suggestionCheck", { file, configFilePath: (project as ConfiguredProject).canonicalConfigFilePath }); // undefined is fine if the cast fails 1069 this.sendDiagnosticsEvent(file, project, project.getLanguageService().getSuggestionDiagnostics(file), "suggestionDiag"); 1070 tracing?.pop(); 1071 } 1072 1073 private sendDiagnosticsEvent(file: NormalizedPath, project: Project, diagnostics: readonly Diagnostic[], kind: protocol.DiagnosticEventKind): void { 1074 try { 1075 this.event<protocol.DiagnosticEventBody>({ file, diagnostics: diagnostics.map(diag => formatDiag(file, project, diag)) }, kind); 1076 } 1077 catch (err) { 1078 this.logError(err, kind); 1079 } 1080 } 1081 1082 /** It is the caller's responsibility to verify that `!this.suppressDiagnosticEvents`. */ 1083 private updateErrorCheck(next: NextStep, checkList: readonly string[] | readonly PendingErrorCheck[], ms: number, requireOpen = true) { 1084 Debug.assert(!this.suppressDiagnosticEvents); // Caller's responsibility 1085 1086 const seq = this.changeSeq; 1087 const followMs = Math.min(ms, 200); 1088 1089 let index = 0; 1090 const goNext = () => { 1091 index++; 1092 if (checkList.length > index) { 1093 next.delay(followMs, checkOne); 1094 } 1095 }; 1096 const checkOne = () => { 1097 if (this.changeSeq !== seq) { 1098 return; 1099 } 1100 1101 let item: string | PendingErrorCheck | undefined = checkList[index]; 1102 if (isString(item)) { 1103 // Find out project for the file name 1104 item = this.toPendingErrorCheck(item); 1105 if (!item) { 1106 // Ignore file if there is no project for the file 1107 goNext(); 1108 return; 1109 } 1110 } 1111 1112 const { fileName, project } = item; 1113 1114 // Ensure the project is up to date before checking if this file is present in the project. 1115 updateProjectIfDirty(project); 1116 if (!project.containsFile(fileName, requireOpen)) { 1117 return; 1118 } 1119 1120 this.syntacticCheck(fileName, project); 1121 if (this.changeSeq !== seq) { 1122 return; 1123 } 1124 1125 // Don't provide semantic diagnostics unless we're in full semantic mode. 1126 if (project.projectService.serverMode !== LanguageServiceMode.Semantic) { 1127 goNext(); 1128 return; 1129 } 1130 next.immediate(() => { 1131 this.semanticCheck(fileName, project); 1132 if (this.changeSeq !== seq) { 1133 return; 1134 } 1135 1136 if (this.getPreferences(fileName).disableSuggestions) { 1137 goNext(); 1138 return; 1139 } 1140 next.immediate(() => { 1141 this.suggestionCheck(fileName, project); 1142 goNext(); 1143 }); 1144 }); 1145 }; 1146 1147 if (checkList.length > index && this.changeSeq === seq) { 1148 next.delay(ms, checkOne); 1149 } 1150 } 1151 1152 private cleanProjects(caption: string, projects: Project[]) { 1153 if (!projects) { 1154 return; 1155 } 1156 this.logger.info(`cleaning ${caption}`); 1157 for (const p of projects) { 1158 p.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache(); 1159 } 1160 } 1161 1162 private cleanup() { 1163 this.cleanProjects("inferred projects", this.projectService.inferredProjects); 1164 this.cleanProjects("configured projects", arrayFrom(this.projectService.configuredProjects.values())); 1165 this.cleanProjects("external projects", this.projectService.externalProjects); 1166 if (this.host.gc) { 1167 this.logger.info(`host.gc()`); 1168 this.host.gc(); 1169 } 1170 } 1171 1172 private getEncodedSyntacticClassifications(args: protocol.EncodedSyntacticClassificationsRequestArgs) { 1173 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1174 return languageService.getEncodedSyntacticClassifications(file, args); 1175 } 1176 1177 private getEncodedSemanticClassifications(args: protocol.EncodedSemanticClassificationsRequestArgs) { 1178 const { file, project } = this.getFileAndProject(args); 1179 const format = args.format === "2020" ? SemanticClassificationFormat.TwentyTwenty : SemanticClassificationFormat.Original; 1180 return project.getLanguageService().getEncodedSemanticClassifications(file, args, format); 1181 } 1182 1183 private getProject(projectFileName: string | undefined): Project | undefined { 1184 return projectFileName === undefined ? undefined : this.projectService.findProject(projectFileName); 1185 } 1186 1187 private getConfigFileAndProject(args: protocol.FileRequestArgs): { configFile: NormalizedPath | undefined, project: Project | undefined } { 1188 const project = this.getProject(args.projectFileName); 1189 const file = toNormalizedPath(args.file); 1190 1191 return { 1192 configFile: project && project.hasConfigFile(file) ? file : undefined, 1193 project 1194 }; 1195 } 1196 1197 private getConfigFileDiagnostics(configFile: NormalizedPath, project: Project, includeLinePosition: boolean) { 1198 const projectErrors = project.getAllProjectErrors(); 1199 const optionsErrors = project.getLanguageService().getCompilerOptionsDiagnostics(); 1200 const diagnosticsForConfigFile = filter( 1201 concatenate(projectErrors, optionsErrors), 1202 diagnostic => !!diagnostic.file && diagnostic.file.fileName === configFile 1203 ); 1204 return includeLinePosition ? 1205 this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnosticsForConfigFile) : 1206 map( 1207 diagnosticsForConfigFile, 1208 diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ false) 1209 ); 1210 } 1211 1212 private convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics: readonly Diagnostic[]): protocol.DiagnosticWithLinePosition[] { 1213 return diagnostics.map<protocol.DiagnosticWithLinePosition>(d => ({ 1214 message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), 1215 start: d.start!, // TODO: GH#18217 1216 length: d.length!, // TODO: GH#18217 1217 category: diagnosticCategoryName(d), 1218 code: d.code, 1219 source: d.source, 1220 startLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start!)))!, // TODO: GH#18217 1221 endLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start! + d.length!)))!, // TODO: GH#18217 1222 reportsUnnecessary: d.reportsUnnecessary, 1223 reportsDeprecated: d.reportsDeprecated, 1224 relatedInformation: map(d.relatedInformation, formatRelatedInformation) 1225 })); 1226 } 1227 1228 private getCompilerOptionsDiagnostics(args: protocol.CompilerOptionsDiagnosticsRequestArgs) { 1229 const project = this.getProject(args.projectFileName)!; 1230 // Get diagnostics that dont have associated file with them 1231 // The diagnostics which have file would be in config file and 1232 // would be reported as part of configFileDiagnostics 1233 return this.convertToDiagnosticsWithLinePosition( 1234 filter( 1235 project.getLanguageService().getCompilerOptionsDiagnostics(), 1236 diagnostic => !diagnostic.file 1237 ), 1238 /*scriptInfo*/ undefined 1239 ); 1240 } 1241 1242 private convertToDiagnosticsWithLinePosition(diagnostics: readonly Diagnostic[], scriptInfo: ScriptInfo | undefined): protocol.DiagnosticWithLinePosition[] { 1243 return diagnostics.map(d => ({ 1244 message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), 1245 start: d.start, 1246 length: d.length, 1247 category: diagnosticCategoryName(d), 1248 code: d.code, 1249 source: d.source, 1250 startLocation: scriptInfo && scriptInfo.positionToLineOffset(d.start!), // TODO: GH#18217 1251 endLocation: scriptInfo && scriptInfo.positionToLineOffset(d.start! + d.length!), 1252 reportsUnnecessary: d.reportsUnnecessary, 1253 reportsDeprecated: d.reportsDeprecated, 1254 relatedInformation: map(d.relatedInformation, formatRelatedInformation), 1255 }) as protocol.DiagnosticWithLinePosition); 1256 } 1257 1258 private getDiagnosticsWorker( 1259 args: protocol.FileRequestArgs, isSemantic: boolean, selector: (project: Project, file: string) => readonly Diagnostic[], includeLinePosition: boolean 1260 ): readonly protocol.DiagnosticWithLinePosition[] | readonly protocol.Diagnostic[] { 1261 const { project, file } = this.getFileAndProject(args); 1262 if (isSemantic && isDeclarationFileInJSOnlyNonConfiguredProject(project, file)) { 1263 return emptyArray; 1264 } 1265 const scriptInfo = project.getScriptInfoForNormalizedPath(file); 1266 const diagnostics = selector(project, file); 1267 return includeLinePosition 1268 ? this.convertToDiagnosticsWithLinePosition(diagnostics, scriptInfo) 1269 : diagnostics.map(d => formatDiag(file, project, d)); 1270 } 1271 1272 private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): readonly protocol.FileSpanWithContext[] | readonly DefinitionInfo[] { 1273 const { file, project } = this.getFileAndProject(args); 1274 const position = this.getPositionInFile(args, file); 1275 const definitions = this.mapDefinitionInfoLocations(project.getLanguageService().getDefinitionAtPosition(file, position) || emptyArray, project); 1276 return simplifiedResult ? this.mapDefinitionInfo(definitions, project) : definitions.map(Session.mapToOriginalLocation); 1277 } 1278 1279 private mapDefinitionInfoLocations(definitions: readonly DefinitionInfo[], project: Project): readonly DefinitionInfo[] { 1280 return definitions.map((info): DefinitionInfo => { 1281 const newDocumentSpan = getMappedDocumentSpanForProject(info, project); 1282 return !newDocumentSpan ? info : { 1283 ...newDocumentSpan, 1284 containerKind: info.containerKind, 1285 containerName: info.containerName, 1286 kind: info.kind, 1287 name: info.name, 1288 failedAliasResolution: info.failedAliasResolution, 1289 ...info.unverified && { unverified: info.unverified }, 1290 }; 1291 }); 1292 } 1293 1294 private getDefinitionAndBoundSpan(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.DefinitionInfoAndBoundSpan | DefinitionInfoAndBoundSpan { 1295 const { file, project } = this.getFileAndProject(args); 1296 const position = this.getPositionInFile(args, file); 1297 const scriptInfo = Debug.checkDefined(project.getScriptInfo(file)); 1298 1299 const unmappedDefinitionAndBoundSpan = project.getLanguageService().getDefinitionAndBoundSpan(file, position); 1300 1301 if (!unmappedDefinitionAndBoundSpan || !unmappedDefinitionAndBoundSpan.definitions) { 1302 return { 1303 definitions: emptyArray, 1304 textSpan: undefined! // TODO: GH#18217 1305 }; 1306 } 1307 1308 const definitions = this.mapDefinitionInfoLocations(unmappedDefinitionAndBoundSpan.definitions, project); 1309 const { textSpan } = unmappedDefinitionAndBoundSpan; 1310 1311 if (simplifiedResult) { 1312 return { 1313 definitions: this.mapDefinitionInfo(definitions, project), 1314 textSpan: toProtocolTextSpan(textSpan, scriptInfo) 1315 }; 1316 } 1317 1318 return { 1319 definitions: definitions.map(Session.mapToOriginalLocation), 1320 textSpan, 1321 }; 1322 } 1323 1324 private findSourceDefinition(args: protocol.FileLocationRequestArgs): readonly protocol.DefinitionInfo[] { 1325 const { file, project } = this.getFileAndProject(args); 1326 const position = this.getPositionInFile(args, file); 1327 const unmappedDefinitions = project.getLanguageService().getDefinitionAtPosition(file, position); 1328 let definitions: readonly DefinitionInfo[] = this.mapDefinitionInfoLocations(unmappedDefinitions || emptyArray, project).slice(); 1329 const needsJsResolution = this.projectService.serverMode === LanguageServiceMode.Semantic && ( 1330 !some(definitions, d => toNormalizedPath(d.fileName) !== file && !d.isAmbient) || 1331 some(definitions, d => !!d.failedAliasResolution)); 1332 1333 if (needsJsResolution) { 1334 const definitionSet = createSet<DefinitionInfo>(d => d.textSpan.start, documentSpansEqual); 1335 definitions?.forEach(d => definitionSet.add(d)); 1336 const noDtsProject = project.getNoDtsResolutionProject([file]); 1337 const ls = noDtsProject.getLanguageService(); 1338 const jsDefinitions = ls.getDefinitionAtPosition(file, position, /*searchOtherFilesOnly*/ true, /*stopAtAlias*/ false) 1339 ?.filter(d => toNormalizedPath(d.fileName) !== file); 1340 if (some(jsDefinitions)) { 1341 for (const jsDefinition of jsDefinitions) { 1342 if (jsDefinition.unverified) { 1343 const refined = tryRefineDefinition(jsDefinition, project.getLanguageService().getProgram()!, ls.getProgram()!); 1344 if (some(refined)) { 1345 for (const def of refined) { 1346 definitionSet.add(def); 1347 } 1348 continue; 1349 } 1350 } 1351 definitionSet.add(jsDefinition); 1352 } 1353 } 1354 else { 1355 const ambientCandidates = definitions.filter(d => toNormalizedPath(d.fileName) !== file && d.isAmbient); 1356 for (const candidate of some(ambientCandidates) ? ambientCandidates : getAmbientCandidatesByClimbingAccessChain()) { 1357 const fileNameToSearch = findImplementationFileFromDtsFileName(candidate.fileName, file, noDtsProject); 1358 if (!fileNameToSearch || !ensureRoot(noDtsProject, fileNameToSearch)) { 1359 continue; 1360 } 1361 const noDtsProgram = ls.getProgram()!; 1362 const fileToSearch = Debug.checkDefined(noDtsProgram.getSourceFile(fileNameToSearch)); 1363 for (const match of searchForDeclaration(candidate.name, fileToSearch, noDtsProgram)) { 1364 definitionSet.add(match); 1365 } 1366 } 1367 } 1368 definitions = arrayFrom(definitionSet.values()); 1369 } 1370 1371 definitions = definitions.filter(d => !d.isAmbient && !d.failedAliasResolution); 1372 return this.mapDefinitionInfo(definitions, project); 1373 1374 function findImplementationFileFromDtsFileName(fileName: string, resolveFromFile: string, auxiliaryProject: Project) { 1375 const nodeModulesPathParts = getNodeModulePathParts(fileName, isOhpm(project.getCompilationSettings().packageManagerType) ? ohModulesPathPart : nodeModulesPathPart); 1376 if (nodeModulesPathParts && fileName.lastIndexOf(nodeModulesPathPart) === nodeModulesPathParts.topLevelNodeModulesIndex) { 1377 // Second check ensures the fileName only contains one `/node_modules/`. If there's more than one I give up. 1378 const packageDirectory = fileName.substring(0, nodeModulesPathParts.packageRootIndex); 1379 const packageJsonCache = project.getModuleResolutionCache()?.getPackageJsonInfoCache(); 1380 const compilerOptions = project.getCompilationSettings(); 1381 const packageJson = getPackageScopeForPath(getNormalizedAbsolutePath(packageDirectory + "/package.json", project.getCurrentDirectory()), getTemporaryModuleResolutionState(packageJsonCache, project, compilerOptions)); 1382 if (!packageJson) return undefined; 1383 // Use fake options instead of actual compiler options to avoid following export map if the project uses node16 or nodenext - 1384 // Mapping from an export map entry across packages is out of scope for now. Returned entrypoints will only be what can be 1385 // resolved from the package root under --moduleResolution node 1386 const entrypoints = getEntrypointsFromPackageJsonInfo( 1387 packageJson, 1388 { moduleResolution: ModuleResolutionKind.NodeJs }, 1389 project, 1390 project.getModuleResolutionCache()); 1391 // This substring is correct only because we checked for a single `/node_modules/` at the top. 1392 const packageNamePathPart = fileName.substring( 1393 nodeModulesPathParts.topLevelPackageNameIndex + 1, 1394 nodeModulesPathParts.packageRootIndex); 1395 const packageName = getPackageNameFromTypesPackageName(unmangleScopedPackageName(packageNamePathPart)); 1396 const path = project.toPath(fileName); 1397 if (entrypoints && some(entrypoints, e => project.toPath(e) === path)) { 1398 // This file was the main entrypoint of a package. Try to resolve that same package name with 1399 // the auxiliary project that only resolves to implementation files. 1400 const [implementationResolution] = auxiliaryProject.resolveModuleNames([packageName], resolveFromFile); 1401 return implementationResolution?.resolvedFileName; 1402 } 1403 else { 1404 // It wasn't the main entrypoint but we are in node_modules. Try a subpath into the package. 1405 const pathToFileInPackage = fileName.substring(nodeModulesPathParts.packageRootIndex + 1); 1406 const specifier = `${packageName}/${removeFileExtension(pathToFileInPackage)}`; 1407 const [implementationResolution] = auxiliaryProject.resolveModuleNames([specifier], resolveFromFile); 1408 return implementationResolution?.resolvedFileName; 1409 } 1410 } 1411 // We're not in node_modules, and we only get to this function if non-dts module resolution failed. 1412 // I'm not sure what else I can do here that isn't already covered by that module resolution. 1413 return undefined; 1414 } 1415 1416 // In 'foo.bar./**/baz', if we got not results on 'baz', see if we can get an ambient definition 1417 // for 'bar' or 'foo' (in that order) so we can search for declarations of 'baz' later. 1418 function getAmbientCandidatesByClimbingAccessChain(): readonly { name: string, fileName: string }[] { 1419 const ls = project.getLanguageService(); 1420 const program = ls.getProgram()!; 1421 const initialNode = getTouchingPropertyName(program.getSourceFile(file)!, position); 1422 if ((isStringLiteralLike(initialNode) || isIdentifier(initialNode)) && isAccessExpression(initialNode.parent)) { 1423 return forEachNameInAccessChainWalkingLeft(initialNode, nameInChain => { 1424 if (nameInChain === initialNode) return undefined; 1425 const candidates = ls.getDefinitionAtPosition(file, nameInChain.getStart(), /*searchOtherFilesOnly*/ true, /*stopAtAlias*/ false) 1426 ?.filter(d => toNormalizedPath(d.fileName) !== file && d.isAmbient) 1427 .map(d => ({ 1428 fileName: d.fileName, 1429 name: getTextOfIdentifierOrLiteral(initialNode) 1430 })); 1431 if (some(candidates)) { 1432 return candidates; 1433 } 1434 }) || emptyArray; 1435 } 1436 return emptyArray; 1437 } 1438 1439 function tryRefineDefinition(definition: DefinitionInfo, program: Program, noDtsProgram: Program) { 1440 const fileToSearch = noDtsProgram.getSourceFile(definition.fileName); 1441 if (!fileToSearch) { 1442 return undefined; 1443 } 1444 const initialNode = getTouchingPropertyName(program.getSourceFile(file)!, position); 1445 const symbol = program.getTypeChecker().getSymbolAtLocation(initialNode); 1446 const importSpecifier = symbol && getDeclarationOfKind<ImportSpecifier>(symbol, SyntaxKind.ImportSpecifier); 1447 if (!importSpecifier) return undefined; 1448 1449 const nameToSearch = importSpecifier.propertyName?.text || importSpecifier.name.text; 1450 return searchForDeclaration(nameToSearch, fileToSearch, noDtsProgram); 1451 } 1452 1453 function searchForDeclaration(declarationName: string, fileToSearch: SourceFile, noDtsProgram: Program) { 1454 const matches = FindAllReferences.Core.getTopMostDeclarationNamesInFile(declarationName, fileToSearch); 1455 return mapDefined(matches, match => { 1456 const symbol = noDtsProgram.getTypeChecker().getSymbolAtLocation(match); 1457 const decl = getDeclarationFromName(match); 1458 if (symbol && decl) { 1459 // I think the last argument to this is supposed to be the start node, but it doesn't seem important. 1460 // Callers internal to GoToDefinition already get confused about this. 1461 return GoToDefinition.createDefinitionInfo(decl, noDtsProgram.getTypeChecker(), symbol, decl, /*unverified*/ true); 1462 } 1463 }); 1464 } 1465 1466 function ensureRoot(project: Project, fileName: string) { 1467 const info = project.getScriptInfo(fileName); 1468 if (!info) return false; 1469 if (!project.containsScriptInfo(info)) { 1470 project.addRoot(info); 1471 project.updateGraph(); 1472 } 1473 return true; 1474 } 1475 } 1476 1477 private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput { 1478 const { file, project } = this.getFileAndProject(args); 1479 if (!project.shouldEmitFile(project.getScriptInfo(file))) { 1480 return { emitSkipped: true, outputFiles: [], diagnostics: [] }; 1481 } 1482 const result = project.getLanguageService().getEmitOutput(file); 1483 return args.richResponse ? 1484 { 1485 ...result, 1486 diagnostics: args.includeLinePosition ? 1487 this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(result.diagnostics) : 1488 result.diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true)) 1489 } : 1490 result; 1491 } 1492 1493 private mapJSDocTagInfo(tags: JSDocTagInfo[] | undefined, project: Project, richResponse: boolean): protocol.JSDocTagInfo[] { 1494 return tags ? tags.map(tag => ({ 1495 ...tag, 1496 text: richResponse && Array.isArray(tag.text) ? this.mapDisplayParts(tag.text, project): getTextOfJSDocTagInfo(tag) 1497 })) : []; 1498 } 1499 1500 private mapDisplayParts(parts: SymbolDisplayPart[] | undefined, project: Project): protocol.SymbolDisplayPart[] { 1501 if (!parts) { 1502 return []; 1503 } 1504 return parts.map(part => part.kind !== "linkName" ? part : { 1505 ...part, 1506 target: this.toFileSpan((part as JSDocLinkDisplayPart).target.fileName, (part as JSDocLinkDisplayPart).target.textSpan, project), 1507 }); 1508 } 1509 1510 private mapSignatureHelpItems(items: SignatureHelpItem[], project: Project, richResponse: boolean): protocol.SignatureHelpItem[] { 1511 return items.map(item => ({ 1512 ...item, 1513 documentation: this.mapDisplayParts(item.documentation, project), 1514 parameters: item.parameters.map(p => ({ ...p, documentation: this.mapDisplayParts(p.documentation, project) })), 1515 tags: this.mapJSDocTagInfo(item.tags, project, richResponse), 1516 })); 1517 } 1518 1519 private mapDefinitionInfo(definitions: readonly DefinitionInfo[], project: Project): readonly protocol.DefinitionInfo[] { 1520 return definitions.map(def => ({ ...this.toFileSpanWithContext(def.fileName, def.textSpan, def.contextSpan, project), ...def.unverified && { unverified: def.unverified } })); 1521 } 1522 1523 /* 1524 * When we map a .d.ts location to .ts, Visual Studio gets confused because there's no associated Roslyn Document in 1525 * the same project which corresponds to the file. VS Code has no problem with this, and luckily we have two protocols. 1526 * This retains the existing behavior for the "simplified" (VS Code) protocol but stores the .d.ts location in a 1527 * set of additional fields, and does the reverse for VS (store the .d.ts location where 1528 * it used to be and stores the .ts location in the additional fields). 1529 */ 1530 private static mapToOriginalLocation<T extends DocumentSpan>(def: T): T { 1531 if (def.originalFileName) { 1532 Debug.assert(def.originalTextSpan !== undefined, "originalTextSpan should be present if originalFileName is"); 1533 return { 1534 ...def as any, 1535 fileName: def.originalFileName, 1536 textSpan: def.originalTextSpan, 1537 targetFileName: def.fileName, 1538 targetTextSpan: def.textSpan, 1539 contextSpan: def.originalContextSpan, 1540 targetContextSpan: def.contextSpan 1541 }; 1542 } 1543 return def; 1544 } 1545 1546 private toFileSpan(fileName: string, textSpan: TextSpan, project: Project): protocol.FileSpan { 1547 const ls = project.getLanguageService(); 1548 const start = ls.toLineColumnOffset!(fileName, textSpan.start); // TODO: GH#18217 1549 const end = ls.toLineColumnOffset!(fileName, textSpanEnd(textSpan)); 1550 1551 return { 1552 file: fileName, 1553 start: { line: start.line + 1, offset: start.character + 1 }, 1554 end: { line: end.line + 1, offset: end.character + 1 } 1555 }; 1556 } 1557 1558 private toFileSpanWithContext(fileName: string, textSpan: TextSpan, contextSpan: TextSpan | undefined, project: Project): protocol.FileSpanWithContext { 1559 const fileSpan = this.toFileSpan(fileName, textSpan, project); 1560 const context = contextSpan && this.toFileSpan(fileName, contextSpan, project); 1561 return context ? 1562 { ...fileSpan, contextStart: context.start, contextEnd: context.end } : 1563 fileSpan; 1564 } 1565 1566 private getTypeDefinition(args: protocol.FileLocationRequestArgs): readonly protocol.FileSpanWithContext[] { 1567 const { file, project } = this.getFileAndProject(args); 1568 const position = this.getPositionInFile(args, file); 1569 1570 const definitions = this.mapDefinitionInfoLocations(project.getLanguageService().getTypeDefinitionAtPosition(file, position) || emptyArray, project); 1571 return this.mapDefinitionInfo(definitions, project); 1572 } 1573 1574 private mapImplementationLocations(implementations: readonly ImplementationLocation[], project: Project): readonly ImplementationLocation[] { 1575 return implementations.map((info): ImplementationLocation => { 1576 const newDocumentSpan = getMappedDocumentSpanForProject(info, project); 1577 return !newDocumentSpan ? info : { 1578 ...newDocumentSpan, 1579 kind: info.kind, 1580 displayParts: info.displayParts, 1581 }; 1582 }); 1583 } 1584 1585 private getImplementation(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): readonly protocol.FileSpanWithContext[] | readonly ImplementationLocation[] { 1586 const { file, project } = this.getFileAndProject(args); 1587 const position = this.getPositionInFile(args, file); 1588 const implementations = this.mapImplementationLocations(project.getLanguageService().getImplementationAtPosition(file, position) || emptyArray, project); 1589 return simplifiedResult ? 1590 implementations.map(({ fileName, textSpan, contextSpan }) => this.toFileSpanWithContext(fileName, textSpan, contextSpan, project)) : 1591 implementations.map(Session.mapToOriginalLocation); 1592 } 1593 1594 private getOccurrences(args: protocol.FileLocationRequestArgs): readonly protocol.OccurrencesResponseItem[] { 1595 const { file, project } = this.getFileAndProject(args); 1596 const position = this.getPositionInFile(args, file); 1597 const occurrences = project.getLanguageService().getOccurrencesAtPosition(file, position); 1598 return occurrences ? 1599 occurrences.map<protocol.OccurrencesResponseItem>(occurrence => { 1600 const { fileName, isWriteAccess, textSpan, isInString, contextSpan } = occurrence; 1601 const scriptInfo = project.getScriptInfo(fileName)!; 1602 return { 1603 ...toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo), 1604 file: fileName, 1605 isWriteAccess, 1606 ...(isInString ? { isInString } : undefined) 1607 }; 1608 }) : 1609 emptyArray; 1610 } 1611 1612 private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs) { 1613 const { configFile } = this.getConfigFileAndProject(args); 1614 if (configFile) { 1615 // all the config file errors are reported as part of semantic check so nothing to report here 1616 return emptyArray; 1617 } 1618 1619 return this.getDiagnosticsWorker(args, /*isSemantic*/ false, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), !!args.includeLinePosition); 1620 } 1621 1622 private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs) { 1623 const { configFile, project } = this.getConfigFileAndProject(args); 1624 if (configFile) { 1625 return this.getConfigFileDiagnostics(configFile, project!, !!args.includeLinePosition); // TODO: GH#18217 1626 } 1627 return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file), !!args.includeLinePosition); 1628 } 1629 1630 private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs) { 1631 const { configFile } = this.getConfigFileAndProject(args); 1632 if (configFile) { 1633 // Currently there are no info diagnostics for config files. 1634 return emptyArray; 1635 } 1636 // isSemantic because we don't want to info diagnostics in declaration files for JS-only users 1637 return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSuggestionDiagnostics(file), !!args.includeLinePosition); 1638 } 1639 1640 private getJsxClosingTag(args: protocol.JsxClosingTagRequestArgs): TextInsertion | undefined { 1641 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1642 const position = this.getPositionInFile(args, file); 1643 const tag = languageService.getJsxClosingTagAtPosition(file, position); 1644 return tag === undefined ? undefined : { newText: tag.newText, caretOffset: 0 }; 1645 } 1646 1647 private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): readonly protocol.DocumentHighlightsItem[] | readonly DocumentHighlights[] { 1648 const { file, project } = this.getFileAndProject(args); 1649 const position = this.getPositionInFile(args, file); 1650 const documentHighlights = project.getLanguageService().getDocumentHighlights(file, position, args.filesToSearch); 1651 1652 if (!documentHighlights) return emptyArray; 1653 if (!simplifiedResult) return documentHighlights; 1654 1655 return documentHighlights.map<protocol.DocumentHighlightsItem>(({ fileName, highlightSpans }) => { 1656 const scriptInfo = project.getScriptInfo(fileName)!; 1657 return { 1658 file: fileName, 1659 highlightSpans: highlightSpans.map(({ textSpan, kind, contextSpan }) => ({ 1660 ...toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo), 1661 kind 1662 })) 1663 }; 1664 }); 1665 } 1666 1667 private provideInlayHints(args: protocol.InlayHintsRequestArgs): readonly protocol.InlayHintItem[] { 1668 const { file, project } = this.getFileAndProject(args); 1669 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 1670 const hints = project.getLanguageService().provideInlayHints(file, args, this.getPreferences(file)); 1671 1672 return hints.map(hint => ({ 1673 ...hint, 1674 position: scriptInfo.positionToLineOffset(hint.position), 1675 })); 1676 } 1677 1678 private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void { 1679 this.projectService.setCompilerOptionsForInferredProjects(args.options, args.projectRootPath); 1680 } 1681 1682 private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo { 1683 return this.getProjectInfoWorker(args.file, args.projectFileName, args.needFileNameList, /*excludeConfigFiles*/ false); 1684 } 1685 1686 private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string | undefined, needFileNameList: boolean, excludeConfigFiles: boolean) { 1687 const { project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName); 1688 updateProjectIfDirty(project); 1689 const projectInfo = { 1690 configFileName: project.getProjectName(), 1691 languageServiceDisabled: !project.languageServiceEnabled, 1692 fileNames: needFileNameList ? project.getFileNames(/*excludeFilesFromExternalLibraries*/ false, excludeConfigFiles) : undefined 1693 }; 1694 return projectInfo; 1695 } 1696 1697 private getRenameInfo(args: protocol.FileLocationRequestArgs): RenameInfo { 1698 const { file, project } = this.getFileAndProject(args); 1699 const position = this.getPositionInFile(args, file); 1700 const preferences = this.getPreferences(file); 1701 return project.getLanguageService().getRenameInfo(file, position, preferences); 1702 } 1703 1704 private getProjects(args: protocol.FileRequestArgs, getScriptInfoEnsuringProjectsUptoDate?: boolean, ignoreNoProjectError?: boolean): Projects { 1705 let projects: readonly Project[] | undefined; 1706 let symLinkedProjects: MultiMap<Path, Project> | undefined; 1707 if (args.projectFileName) { 1708 const project = this.getProject(args.projectFileName); 1709 if (project) { 1710 projects = [project]; 1711 } 1712 } 1713 else { 1714 const scriptInfo = getScriptInfoEnsuringProjectsUptoDate ? 1715 this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file) : 1716 this.projectService.getScriptInfo(args.file); 1717 if (!scriptInfo) { 1718 if (ignoreNoProjectError) return emptyArray; 1719 this.projectService.logErrorForScriptInfoNotFound(args.file); 1720 return Errors.ThrowNoProject(); 1721 } 1722 else if (!getScriptInfoEnsuringProjectsUptoDate) { 1723 // Ensure there are containing projects are present 1724 this.projectService.ensureDefaultProjectForFile(scriptInfo); 1725 } 1726 projects = scriptInfo.containingProjects; 1727 symLinkedProjects = this.projectService.getSymlinkedProjects(scriptInfo); 1728 } 1729 // filter handles case when 'projects' is undefined 1730 projects = filter(projects, p => p.languageServiceEnabled && !p.isOrphan()); 1731 if (!ignoreNoProjectError && (!projects || !projects.length) && !symLinkedProjects) { 1732 this.projectService.logErrorForScriptInfoNotFound(args.file ?? args.projectFileName); 1733 return Errors.ThrowNoProject(); 1734 } 1735 return symLinkedProjects ? { projects: projects!, symLinkedProjects } : projects!; // TODO: GH#18217 1736 } 1737 1738 private getDefaultProject(args: protocol.FileRequestArgs) { 1739 if (args.projectFileName) { 1740 const project = this.getProject(args.projectFileName); 1741 if (project) { 1742 return project; 1743 } 1744 if (!args.file) { 1745 return Errors.ThrowNoProject(); 1746 } 1747 } 1748 const info = this.projectService.getScriptInfo(args.file)!; 1749 return info.getDefaultProject(); 1750 } 1751 1752 private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | readonly RenameLocation[] { 1753 const file = toNormalizedPath(args.file); 1754 const position = this.getPositionInFile(args, file); 1755 const projects = this.getProjects(args); 1756 const defaultProject = this.getDefaultProject(args); 1757 const preferences = this.getPreferences(file); 1758 const renameInfo: protocol.RenameInfo = this.mapRenameInfo( 1759 defaultProject.getLanguageService().getRenameInfo(file, position, preferences), Debug.checkDefined(this.projectService.getScriptInfo(file))); 1760 1761 if (!renameInfo.canRename) return simplifiedResult ? { info: renameInfo, locs: [] } : []; 1762 1763 const locations = getRenameLocationsWorker( 1764 projects, 1765 defaultProject, 1766 { fileName: args.file, pos: position }, 1767 !!args.findInStrings, 1768 !!args.findInComments, 1769 preferences, 1770 ); 1771 if (!simplifiedResult) return locations; 1772 return { info: renameInfo, locs: this.toSpanGroups(locations) }; 1773 } 1774 1775 private mapRenameInfo(info: RenameInfo, scriptInfo: ScriptInfo): protocol.RenameInfo { 1776 if (info.canRename) { 1777 const { canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan } = info; 1778 return identity<protocol.RenameInfoSuccess>( 1779 { canRename, fileToRename, displayName, fullDisplayName, kind, kindModifiers, triggerSpan: toProtocolTextSpan(triggerSpan, scriptInfo) }); 1780 } 1781 else { 1782 return info; 1783 } 1784 } 1785 1786 private toSpanGroups(locations: readonly RenameLocation[]): readonly protocol.SpanGroup[] { 1787 const map = new Map<string, protocol.SpanGroup>(); 1788 for (const { fileName, textSpan, contextSpan, originalContextSpan: _2, originalTextSpan: _, originalFileName: _1, ...prefixSuffixText } of locations) { 1789 let group = map.get(fileName); 1790 if (!group) map.set(fileName, group = { file: fileName, locs: [] }); 1791 const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(fileName)); 1792 group.locs.push({ ...toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo), ...prefixSuffixText }); 1793 } 1794 return arrayFrom(map.values()); 1795 } 1796 1797 private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | readonly ReferencedSymbol[] { 1798 const file = toNormalizedPath(args.file); 1799 const projects = this.getProjects(args); 1800 const position = this.getPositionInFile(args, file); 1801 const references = getReferencesWorker( 1802 projects, 1803 this.getDefaultProject(args), 1804 { fileName: args.file, pos: position }, 1805 this.logger, 1806 ); 1807 1808 if (!simplifiedResult) return references; 1809 1810 const preferences = this.getPreferences(file); 1811 const defaultProject = this.getDefaultProject(args); 1812 const scriptInfo = defaultProject.getScriptInfoForNormalizedPath(file)!; 1813 const nameInfo = defaultProject.getLanguageService().getQuickInfoAtPosition(file, position); 1814 const symbolDisplayString = nameInfo ? displayPartsToString(nameInfo.displayParts) : ""; 1815 const nameSpan = nameInfo && nameInfo.textSpan; 1816 const symbolStartOffset = nameSpan ? scriptInfo.positionToLineOffset(nameSpan.start).offset : 0; 1817 const symbolName = nameSpan ? scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan)) : ""; 1818 const refs: readonly protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol => { 1819 return referencedSymbol.references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry, preferences)); 1820 }); 1821 return { refs, symbolName, symbolStartOffset, symbolDisplayString }; 1822 } 1823 1824 private getFileReferences(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.FileReferencesResponseBody | readonly ReferenceEntry[] { 1825 const projects = this.getProjects(args); 1826 const fileName = args.file; 1827 const preferences = this.getPreferences(toNormalizedPath(fileName)); 1828 1829 const references: ReferenceEntry[] = []; 1830 const seen = createDocumentSpanSet(); 1831 1832 forEachProjectInProjects(projects, /*path*/ undefined, project => { 1833 if (project.getCancellationToken().isCancellationRequested()) return; 1834 1835 const projectOutputs = project.getLanguageService().getFileReferences(fileName); 1836 if (projectOutputs) { 1837 for (const referenceEntry of projectOutputs) { 1838 if (!seen.has(referenceEntry)) { 1839 references.push(referenceEntry); 1840 seen.add(referenceEntry); 1841 } 1842 } 1843 } 1844 }); 1845 1846 if (!simplifiedResult) return references; 1847 const refs = references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry, preferences)); 1848 return { 1849 refs, 1850 symbolName: `"${args.file}"` 1851 }; 1852 } 1853 1854 /** 1855 * @param fileName is the name of the file to be opened 1856 * @param fileContent is a version of the file content that is known to be more up to date than the one on disk 1857 */ 1858 private openClientFile(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: NormalizedPath) { 1859 this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath); 1860 } 1861 1862 private getPosition(args: protocol.Location & { position?: number }, scriptInfo: ScriptInfo): number { 1863 return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); 1864 } 1865 1866 private getPositionInFile(args: protocol.Location & { position?: number }, file: NormalizedPath): number { 1867 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 1868 return this.getPosition(args, scriptInfo); 1869 } 1870 1871 private getFileAndProject(args: protocol.FileRequestArgs): FileAndProject { 1872 return this.getFileAndProjectWorker(args.file, args.projectFileName); 1873 } 1874 1875 private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) { 1876 const { file, project } = this.getFileAndProject(args); 1877 return { 1878 file, 1879 languageService: project.getLanguageService(/*ensureSynchronized*/ false) 1880 }; 1881 } 1882 1883 private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string | undefined): { file: NormalizedPath, project: Project } { 1884 const file = toNormalizedPath(uncheckedFileName); 1885 const project = this.getProject(projectFileName) || this.projectService.ensureDefaultProjectForFile(file); 1886 return { file, project }; 1887 } 1888 1889 private getOutliningSpans(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.OutliningSpan[] | OutliningSpan[] { 1890 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1891 const spans = languageService.getOutliningSpans(file); 1892 if (simplifiedResult) { 1893 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 1894 return spans.map(s => ({ 1895 textSpan: toProtocolTextSpan(s.textSpan, scriptInfo), 1896 hintSpan: toProtocolTextSpan(s.hintSpan, scriptInfo), 1897 bannerText: s.bannerText, 1898 autoCollapse: s.autoCollapse, 1899 kind: s.kind 1900 })); 1901 } 1902 else { 1903 return spans; 1904 } 1905 } 1906 1907 private getTodoComments(args: protocol.TodoCommentRequestArgs) { 1908 const { file, project } = this.getFileAndProject(args); 1909 return project.getLanguageService().getTodoComments(file, args.descriptors); 1910 } 1911 1912 private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { 1913 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1914 const position = this.getPositionInFile(args, file); 1915 return languageService.getDocCommentTemplateAtPosition(file, position, this.getPreferences(file)); 1916 } 1917 1918 private getSpanOfEnclosingComment(args: protocol.SpanOfEnclosingCommentRequestArgs) { 1919 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1920 const onlyMultiLine = args.onlyMultiLine; 1921 const position = this.getPositionInFile(args, file); 1922 return languageService.getSpanOfEnclosingComment(file, position, onlyMultiLine); 1923 } 1924 1925 private getIndentation(args: protocol.IndentationRequestArgs) { 1926 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1927 const position = this.getPositionInFile(args, file); 1928 const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); 1929 const indentation = languageService.getIndentationAtPosition(file, position, options); 1930 return { position, indentation }; 1931 } 1932 1933 private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { 1934 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1935 const position = this.getPositionInFile(args, file); 1936 return languageService.getBreakpointStatementAtPosition(file, position); 1937 } 1938 1939 private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { 1940 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1941 const position = this.getPositionInFile(args, file); 1942 return languageService.getNameOrDottedNameSpan(file, position, position); 1943 } 1944 1945 private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { 1946 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1947 const position = this.getPositionInFile(args, file); 1948 return languageService.isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0)); 1949 } 1950 1951 private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo | undefined { 1952 const { file, project } = this.getFileAndProject(args); 1953 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 1954 const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo)); 1955 if (!quickInfo) { 1956 return undefined; 1957 } 1958 1959 const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc; 1960 if (simplifiedResult) { 1961 const displayString = displayPartsToString(quickInfo.displayParts); 1962 return { 1963 kind: quickInfo.kind, 1964 kindModifiers: quickInfo.kindModifiers, 1965 start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start), 1966 end: scriptInfo.positionToLineOffset(textSpanEnd(quickInfo.textSpan)), 1967 displayString, 1968 documentation: useDisplayParts ? this.mapDisplayParts(quickInfo.documentation, project) : displayPartsToString(quickInfo.documentation), 1969 tags: this.mapJSDocTagInfo(quickInfo.tags, project, useDisplayParts), 1970 }; 1971 } 1972 else { 1973 return useDisplayParts ? quickInfo : { 1974 ...quickInfo, 1975 tags: this.mapJSDocTagInfo(quickInfo.tags, project, /*useDisplayParts*/ false) as JSDocTagInfo[] 1976 }; 1977 } 1978 } 1979 1980 private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] | undefined { 1981 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1982 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 1983 1984 const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); 1985 const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); 1986 1987 // TODO: avoid duplicate code (with formatonkey) 1988 const edits = languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.getFormatOptions(file)); 1989 if (!edits) { 1990 return undefined; 1991 } 1992 1993 return edits.map(edit => this.convertTextChangeToCodeEdit(edit, scriptInfo)); 1994 } 1995 1996 private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { 1997 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 1998 const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); 1999 return languageService.getFormattingEditsForRange(file, args.position!, args.endPosition!, options); // TODO: GH#18217 2000 } 2001 2002 private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { 2003 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2004 const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); 2005 return languageService.getFormattingEditsForDocument(file, options); 2006 } 2007 2008 private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { 2009 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2010 const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); 2011 return languageService.getFormattingEditsAfterKeystroke(file, args.position!, args.key, options); // TODO: GH#18217 2012 } 2013 2014 private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] | undefined { 2015 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2016 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2017 const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); 2018 const formatOptions = this.getFormatOptions(file); 2019 const edits = languageService.getFormattingEditsAfterKeystroke(file, position, args.key, 2020 formatOptions); 2021 // Check whether we should auto-indent. This will be when 2022 // the position is on a line containing only whitespace. 2023 // This should leave the edits returned from 2024 // getFormattingEditsAfterKeystroke either empty or pertaining 2025 // only to the previous line. If all this is true, then 2026 // add edits necessary to properly indent the current line. 2027 if ((args.key === "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { 2028 const { lineText, absolutePosition } = scriptInfo.getAbsolutePositionAndLineText(args.line); 2029 if (lineText && lineText.search("\\S") < 0) { 2030 const preferredIndent = languageService.getIndentationAtPosition(file, position, formatOptions); 2031 let hasIndent = 0; 2032 let i: number, len: number; 2033 for (i = 0, len = lineText.length; i < len; i++) { 2034 if (lineText.charAt(i) === " ") { 2035 hasIndent++; 2036 } 2037 else if (lineText.charAt(i) === "\t") { 2038 hasIndent += formatOptions.tabSize!; // TODO: GH#18217 2039 } 2040 else { 2041 break; 2042 } 2043 } 2044 // i points to the first non whitespace character 2045 if (preferredIndent !== hasIndent) { 2046 const firstNoWhiteSpacePosition = absolutePosition + i; 2047 edits.push({ 2048 span: createTextSpanFromBounds(absolutePosition, firstNoWhiteSpacePosition), 2049 newText: formatting.getIndentationString(preferredIndent, formatOptions) 2050 }); 2051 } 2052 } 2053 } 2054 2055 if (!edits) { 2056 return undefined; 2057 } 2058 2059 return edits.map((edit) => { 2060 return { 2061 start: scriptInfo.positionToLineOffset(edit.span.start), 2062 end: scriptInfo.positionToLineOffset(textSpanEnd(edit.span)), 2063 newText: edit.newText ? edit.newText : "" 2064 }; 2065 }); 2066 } 2067 2068 private getCompletions(args: protocol.CompletionsRequestArgs, kind: protocol.CommandTypes.CompletionInfo | protocol.CommandTypes.Completions | protocol.CommandTypes.CompletionsFull): WithMetadata<readonly protocol.CompletionEntry[]> | protocol.CompletionInfo | CompletionInfo | undefined { 2069 const { file, project } = this.getFileAndProject(args); 2070 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2071 const position = this.getPosition(args, scriptInfo); 2072 2073 const completions = project.getLanguageService().getCompletionsAtPosition( 2074 file, 2075 position, 2076 { 2077 ...convertUserPreferences(this.getPreferences(file)), 2078 triggerCharacter: args.triggerCharacter, 2079 triggerKind: args.triggerKind as CompletionTriggerKind | undefined, 2080 includeExternalModuleExports: args.includeExternalModuleExports, 2081 includeInsertTextCompletions: args.includeInsertTextCompletions, 2082 }, 2083 project.projectService.getFormatCodeOptions(file), 2084 ); 2085 if (completions === undefined) return undefined; 2086 2087 if (kind === protocol.CommandTypes.CompletionsFull) return completions; 2088 2089 const prefix = args.prefix || ""; 2090 const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => { 2091 if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) { 2092 const { 2093 name, 2094 kind, 2095 kindModifiers, 2096 sortText, 2097 insertText, 2098 replacementSpan, 2099 hasAction, 2100 source, 2101 sourceDisplay, 2102 labelDetails, 2103 isSnippet, 2104 isRecommended, 2105 isPackageJsonImport, 2106 isImportStatementCompletion, 2107 jsDoc, 2108 displayParts, 2109 data } = entry; 2110 const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined; 2111 // Use `hasAction || undefined` to avoid serializing `false`. 2112 return { 2113 name, 2114 kind, 2115 kindModifiers, 2116 sortText, 2117 insertText, 2118 replacementSpan: convertedSpan, 2119 isSnippet, 2120 hasAction: hasAction || undefined, 2121 source, 2122 sourceDisplay, 2123 labelDetails, 2124 isRecommended, 2125 isPackageJsonImport, 2126 isImportStatementCompletion, 2127 jsDoc, 2128 displayParts, 2129 data 2130 }; 2131 } 2132 }); 2133 2134 if (kind === protocol.CommandTypes.Completions) { 2135 if (completions.metadata) (entries as WithMetadata<readonly protocol.CompletionEntry[]>).metadata = completions.metadata; 2136 return entries; 2137 } 2138 2139 const res: protocol.CompletionInfo = { 2140 ...completions, 2141 optionalReplacementSpan: completions.optionalReplacementSpan && toProtocolTextSpan(completions.optionalReplacementSpan, scriptInfo), 2142 entries, 2143 }; 2144 return res; 2145 } 2146 2147 private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs, fullResult: boolean): readonly protocol.CompletionEntryDetails[] | readonly CompletionEntryDetails[] { 2148 const { file, project } = this.getFileAndProject(args); 2149 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2150 const position = this.getPosition(args, scriptInfo); 2151 const formattingOptions = project.projectService.getFormatCodeOptions(file); 2152 const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc; 2153 2154 const result = mapDefined(args.entryNames, entryName => { 2155 const { name, source, data } = typeof entryName === "string" ? { name: entryName, source: undefined, data: undefined } : entryName; 2156 return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file), data ? cast(data, isCompletionEntryData) : undefined); 2157 }); 2158 return fullResult 2159 ? (useDisplayParts ? result : result.map(details => ({ ...details, tags: this.mapJSDocTagInfo(details.tags, project, /*richResponse*/ false) as JSDocTagInfo[] }))) 2160 : result.map(details => ({ 2161 ...details, 2162 codeActions: map(details.codeActions, action => this.mapCodeAction(action)), 2163 documentation: this.mapDisplayParts(details.documentation, project), 2164 tags: this.mapJSDocTagInfo(details.tags, project, useDisplayParts), 2165 })); 2166 } 2167 2168 private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): readonly protocol.CompileOnSaveAffectedFileListSingleProject[] { 2169 const projects = this.getProjects(args, /*getScriptInfoEnsuringProjectsUptoDate*/ true, /*ignoreNoProjectError*/ true); 2170 const info = this.projectService.getScriptInfo(args.file); 2171 if (!info) { 2172 return emptyArray; 2173 } 2174 2175 return combineProjectOutput( 2176 info, 2177 path => this.projectService.getScriptInfoForPath(path)!, 2178 projects, 2179 (project, info) => { 2180 if (!project.compileOnSaveEnabled || !project.languageServiceEnabled || project.isOrphan()) { 2181 return undefined; 2182 } 2183 2184 const compilationSettings = project.getCompilationSettings(); 2185 2186 if (!!compilationSettings.noEmit || isDeclarationFileName(info.fileName) && !dtsChangeCanAffectEmit(compilationSettings)) { 2187 // avoid triggering emit when a change is made in a .d.ts when declaration emit and decorator metadata emit are disabled 2188 return undefined; 2189 } 2190 2191 return { 2192 projectFileName: project.getProjectName(), 2193 fileNames: project.getCompileOnSaveAffectedFileList(info), 2194 projectUsesOutFile: !!outFile(compilationSettings) 2195 }; 2196 } 2197 ); 2198 } 2199 2200 private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs): boolean | protocol.EmitResult | EmitResult { 2201 const { file, project } = this.getFileAndProject(args); 2202 if (!project) { 2203 Errors.ThrowNoProject(); 2204 } 2205 if (!project.languageServiceEnabled) { 2206 return args.richResponse ? { emitSkipped: true, diagnostics: [] } : false; 2207 } 2208 const scriptInfo = project.getScriptInfo(file)!; 2209 const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); 2210 return args.richResponse ? 2211 { 2212 emitSkipped, 2213 diagnostics: args.includeLinePosition ? 2214 this.convertToDiagnosticsWithLinePositionFromDiagnosticFile(diagnostics) : 2215 diagnostics.map(d => formatDiagnosticToProtocol(d, /*includeFileName*/ true)) 2216 } : 2217 !emitSkipped; 2218 } 2219 2220 private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems | undefined { 2221 const { file, project } = this.getFileAndProject(args); 2222 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2223 const position = this.getPosition(args, scriptInfo); 2224 const helpItems = project.getLanguageService().getSignatureHelpItems(file, position, args); 2225 const useDisplayParts = !!this.getPreferences(file).displayPartsForJSDoc; 2226 if (helpItems && simplifiedResult) { 2227 const span = helpItems.applicableSpan; 2228 return { 2229 ...helpItems, 2230 applicableSpan: { 2231 start: scriptInfo.positionToLineOffset(span.start), 2232 end: scriptInfo.positionToLineOffset(span.start + span.length) 2233 }, 2234 items: this.mapSignatureHelpItems(helpItems.items, project, useDisplayParts), 2235 }; 2236 } 2237 else if (useDisplayParts || !helpItems) { 2238 return helpItems; 2239 } 2240 else { 2241 return { 2242 ...helpItems, 2243 items: helpItems.items.map(item => ({ ...item, tags: this.mapJSDocTagInfo(item.tags, project, /*richResponse*/ false) as JSDocTagInfo[] })) 2244 }; 2245 } 2246 } 2247 2248 private toPendingErrorCheck(uncheckedFileName: string): PendingErrorCheck | undefined { 2249 const fileName = toNormalizedPath(uncheckedFileName); 2250 const project = this.projectService.tryGetDefaultProjectForFile(fileName); 2251 return project && { fileName, project }; 2252 } 2253 2254 private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void { 2255 if (this.suppressDiagnosticEvents) { 2256 return; 2257 } 2258 2259 if (fileNames.length > 0) { 2260 this.updateErrorCheck(next, fileNames, delay); 2261 } 2262 } 2263 2264 private change(args: protocol.ChangeRequestArgs) { 2265 const scriptInfo = this.projectService.getScriptInfo(args.file)!; 2266 Debug.assert(!!scriptInfo); 2267 const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); 2268 const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); 2269 if (start >= 0) { 2270 this.changeSeq++; 2271 this.projectService.applyChangesToFile(scriptInfo, singleIterator({ 2272 span: { start, length: end - start }, 2273 newText: args.insertString! // TODO: GH#18217 2274 })); 2275 } 2276 } 2277 2278 private reload(args: protocol.ReloadRequestArgs, reqSeq: number) { 2279 const file = toNormalizedPath(args.file); 2280 const tempFileName = args.tmpfile === undefined ? undefined : toNormalizedPath(args.tmpfile); 2281 const info = this.projectService.getScriptInfoForNormalizedPath(file); 2282 if (info) { 2283 this.changeSeq++; 2284 // make sure no changes happen before this one is finished 2285 if (info.reloadFromFile(tempFileName)) { 2286 this.doOutput(/*info*/ undefined, CommandNames.Reload, reqSeq, /*success*/ true); 2287 } 2288 } 2289 } 2290 2291 private saveToTmp(fileName: string, tempFileName: string) { 2292 const scriptInfo = this.projectService.getScriptInfo(fileName); 2293 if (scriptInfo) { 2294 scriptInfo.saveTo(tempFileName); 2295 } 2296 } 2297 2298 private closeClientFile(fileName: string) { 2299 if (!fileName) { 2300 return; 2301 } 2302 const file = normalizePath(fileName); 2303 this.projectService.closeClientFile(file); 2304 } 2305 2306 private mapLocationNavigationBarItems(items: NavigationBarItem[], scriptInfo: ScriptInfo): protocol.NavigationBarItem[] { 2307 return map(items, item => ({ 2308 text: item.text, 2309 kind: item.kind, 2310 kindModifiers: item.kindModifiers, 2311 spans: item.spans.map(span => toProtocolTextSpan(span, scriptInfo)), 2312 childItems: this.mapLocationNavigationBarItems(item.childItems, scriptInfo), 2313 indent: item.indent 2314 })); 2315 } 2316 2317 private getNavigationBarItems(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] | undefined { 2318 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2319 const items = languageService.getNavigationBarItems(file); 2320 return !items 2321 ? undefined 2322 : simplifiedResult 2323 ? this.mapLocationNavigationBarItems(items, this.projectService.getScriptInfoForNormalizedPath(file)!) 2324 : items; 2325 } 2326 2327 private toLocationNavigationTree(tree: NavigationTree, scriptInfo: ScriptInfo): protocol.NavigationTree { 2328 return { 2329 text: tree.text, 2330 kind: tree.kind, 2331 kindModifiers: tree.kindModifiers, 2332 spans: tree.spans.map(span => toProtocolTextSpan(span, scriptInfo)), 2333 nameSpan: tree.nameSpan && toProtocolTextSpan(tree.nameSpan, scriptInfo), 2334 childItems: map(tree.childItems, item => this.toLocationNavigationTree(item, scriptInfo)) 2335 }; 2336 } 2337 2338 private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree | undefined { 2339 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2340 const tree = languageService.getNavigationTree(file); 2341 return !tree 2342 ? undefined 2343 : simplifiedResult 2344 ? this.toLocationNavigationTree(tree, this.projectService.getScriptInfoForNormalizedPath(file)!) 2345 : tree; 2346 } 2347 2348 private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): readonly protocol.NavtoItem[] | readonly NavigateToItem[] { 2349 const full = this.getFullNavigateToItems(args); 2350 return !simplifiedResult ? 2351 flatMap(full, ({ navigateToItems }) => navigateToItems) : 2352 flatMap( 2353 full, 2354 ({ project, navigateToItems }) => navigateToItems.map(navItem => { 2355 const scriptInfo = project.getScriptInfo(navItem.fileName)!; 2356 const bakedItem: protocol.NavtoItem = { 2357 name: navItem.name, 2358 kind: navItem.kind, 2359 kindModifiers: navItem.kindModifiers, 2360 isCaseSensitive: navItem.isCaseSensitive, 2361 matchKind: navItem.matchKind, 2362 file: navItem.fileName, 2363 start: scriptInfo.positionToLineOffset(navItem.textSpan.start), 2364 end: scriptInfo.positionToLineOffset(textSpanEnd(navItem.textSpan)) 2365 }; 2366 if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { 2367 bakedItem.kindModifiers = navItem.kindModifiers; 2368 } 2369 if (navItem.containerName && (navItem.containerName.length > 0)) { 2370 bakedItem.containerName = navItem.containerName; 2371 } 2372 if (navItem.containerKind && (navItem.containerKind.length > 0)) { 2373 bakedItem.containerKind = navItem.containerKind; 2374 } 2375 return bakedItem; 2376 }) 2377 ); 2378 } 2379 2380 private getFullNavigateToItems(args: protocol.NavtoRequestArgs): ProjectNavigateToItems[] { 2381 const { currentFileOnly, searchValue, maxResultCount, projectFileName } = args; 2382 2383 if (currentFileOnly) { 2384 Debug.assertIsDefined(args.file); 2385 const { file, project } = this.getFileAndProject(args as protocol.FileRequestArgs); 2386 return [{ project, navigateToItems: project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file) }]; 2387 } 2388 2389 const outputs: ProjectNavigateToItems[] = []; 2390 2391 // This is effectively a hashset with `name` as the custom hash and `navigateToItemIsEqualTo` as the custom equals. 2392 // `name` is a very cheap hash function, but we could incorporate other properties to reduce collisions. 2393 const seenItems = new Map<string, NavigateToItem[]>(); // name to items with that name 2394 2395 if (!args.file && !projectFileName) { 2396 // VS Code's `Go to symbol in workspaces` sends request like this by default. 2397 // There's a setting to have it send a file name (reverting to older behavior). 2398 2399 // TODO (https://github.com/microsoft/TypeScript/issues/47839) 2400 // This appears to have been intended to search all projects but, in practice, it seems to only search 2401 // those that are downstream from already-loaded projects. 2402 // Filtering by !isSourceOfProjectReferenceRedirect is new, but seems appropriate and consistent with 2403 // the case below. 2404 this.projectService.loadAncestorProjectTree(); 2405 this.projectService.forEachEnabledProject(project => addItemsForProject(project)); 2406 } 2407 else { 2408 // VS's `Go to symbol` sends requests with just a project and doesn't want cascading since it will 2409 // send a separate request for each project of interest 2410 2411 // TODO (https://github.com/microsoft/TypeScript/issues/47839) 2412 // This doesn't really make sense unless it's a single project matching `projectFileName` 2413 const projects = this.getProjects(args as protocol.FileRequestArgs); 2414 forEachProjectInProjects(projects, /*path*/ undefined, project => addItemsForProject(project)); 2415 } 2416 2417 return outputs; 2418 2419 // Mutates `outputs` 2420 function addItemsForProject(project: Project) { 2421 const projectItems = project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*filename*/ undefined, /*excludeDts*/ project.isNonTsProject()); 2422 const unseenItems = filter(projectItems, item => tryAddSeenItem(item) && !getMappedLocationForProject(documentSpanLocation(item), project)); 2423 if (unseenItems.length) { 2424 outputs.push({ project, navigateToItems: unseenItems }); 2425 } 2426 } 2427 2428 // Returns true if the item had not been seen before 2429 // Mutates `seenItems` 2430 function tryAddSeenItem(item: NavigateToItem) { 2431 const name = item.name; 2432 if (!seenItems.has(name)) { 2433 seenItems.set(name, [item]); 2434 return true; 2435 } 2436 2437 const seen = seenItems.get(name)!; 2438 for (const seenItem of seen) { 2439 if (navigateToItemIsEqualTo(seenItem, item)) { 2440 return false; 2441 } 2442 } 2443 2444 seen.push(item); 2445 return true; 2446 } 2447 2448 function navigateToItemIsEqualTo(a: NavigateToItem, b: NavigateToItem): boolean { 2449 if (a === b) { 2450 return true; 2451 } 2452 if (!a || !b) { 2453 return false; 2454 } 2455 return a.containerKind === b.containerKind && 2456 a.containerName === b.containerName && 2457 a.fileName === b.fileName && 2458 a.isCaseSensitive === b.isCaseSensitive && 2459 a.kind === b.kind && 2460 a.kindModifiers === b.kindModifiers && 2461 a.matchKind === b.matchKind && 2462 a.name === b.name && 2463 a.textSpan.start === b.textSpan.start && 2464 a.textSpan.length === b.textSpan.length; 2465 } 2466 } 2467 2468 private getSupportedCodeFixes(): string[] { 2469 return getSupportedCodeFixes(); 2470 } 2471 2472 private isLocation(locationOrSpan: protocol.FileLocationOrRangeRequestArgs): locationOrSpan is protocol.FileLocationRequestArgs { 2473 return (locationOrSpan as protocol.FileLocationRequestArgs).line !== undefined; 2474 } 2475 2476 private extractPositionOrRange(args: protocol.FileLocationOrRangeRequestArgs, scriptInfo: ScriptInfo): number | TextRange { 2477 let position: number | undefined; 2478 let textRange: TextRange | undefined; 2479 if (this.isLocation(args)) { 2480 position = getPosition(args); 2481 } 2482 else { 2483 textRange = this.getRange(args, scriptInfo); 2484 } 2485 return Debug.checkDefined(position === undefined ? textRange : position); 2486 2487 function getPosition(loc: protocol.FileLocationRequestArgs) { 2488 return loc.position !== undefined ? loc.position : scriptInfo.lineOffsetToPosition(loc.line, loc.offset); 2489 } 2490 } 2491 2492 private getRange(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo): TextRange { 2493 const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); 2494 2495 return { pos: startPosition, end: endPosition }; 2496 } 2497 2498 private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] { 2499 const { file, project } = this.getFileAndProject(args); 2500 const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; 2501 return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind); 2502 } 2503 2504 private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { 2505 const { file, project } = this.getFileAndProject(args); 2506 const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; 2507 const result = project.getLanguageService().getEditsForRefactor( 2508 file, 2509 this.getFormatOptions(file), 2510 this.extractPositionOrRange(args, scriptInfo), 2511 args.refactor, 2512 args.action, 2513 this.getPreferences(file), 2514 ); 2515 2516 if (result === undefined) { 2517 return { 2518 edits: [] 2519 }; 2520 } 2521 2522 if (simplifiedResult) { 2523 const { renameFilename, renameLocation, edits } = result; 2524 let mappedRenameLocation: protocol.Location | undefined; 2525 if (renameFilename !== undefined && renameLocation !== undefined) { 2526 const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!; 2527 mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits); 2528 } 2529 return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) }; 2530 } 2531 else { 2532 return result; 2533 } 2534 } 2535 2536 private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { 2537 Debug.assert(args.scope.type === "file"); 2538 const { file, project } = this.getFileAndProject(args.scope.args); 2539 const changes = project.getLanguageService().organizeImports( 2540 { 2541 fileName: file, 2542 mode: args.mode as OrganizeImportsMode | undefined ?? (args.skipDestructiveCodeActions ? OrganizeImportsMode.SortAndCombine : undefined), 2543 type: "file", 2544 }, 2545 this.getFormatOptions(file), 2546 this.getPreferences(file) 2547 ); 2548 if (simplifiedResult) { 2549 return this.mapTextChangesToCodeEdits(changes); 2550 } 2551 else { 2552 return changes; 2553 } 2554 } 2555 2556 private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { 2557 const oldPath = toNormalizedPath(args.oldFilePath); 2558 const newPath = toNormalizedPath(args.newFilePath); 2559 const formatOptions = this.getHostFormatOptions(); 2560 const preferences = this.getHostPreferences(); 2561 2562 2563 const seenFiles = new Set<string>(); 2564 const textChanges: FileTextChanges[] = []; 2565 // TODO (https://github.com/microsoft/TypeScript/issues/47839) 2566 // This appears to have been intended to search all projects but, in practice, it seems to only search 2567 // those that are downstream from already-loaded projects. 2568 this.projectService.loadAncestorProjectTree(); 2569 this.projectService.forEachEnabledProject(project => { 2570 const projectTextChanges = project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences); 2571 const projectFiles: string[] = []; 2572 for (const textChange of projectTextChanges) { 2573 if (!seenFiles.has(textChange.fileName)) { 2574 textChanges.push(textChange); 2575 projectFiles.push(textChange.fileName); 2576 } 2577 } 2578 for (const file of projectFiles) { 2579 seenFiles.add(file); 2580 } 2581 }); 2582 2583 return simplifiedResult ? textChanges.map(c => this.mapTextChangeToCodeEdit(c)) : textChanges; 2584 } 2585 2586 private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): readonly protocol.CodeFixAction[] | readonly CodeFixAction[] | undefined { 2587 const { file, project } = this.getFileAndProject(args); 2588 2589 const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; 2590 const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); 2591 2592 let codeActions: readonly CodeFixAction[]; 2593 try { 2594 codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file)); 2595 } 2596 catch(e) { 2597 const ls = project.getLanguageService(); 2598 const existingDiagCodes = [ 2599 ...ls.getSyntacticDiagnostics(file), 2600 ...ls.getSemanticDiagnostics(file), 2601 ...ls.getSuggestionDiagnostics(file) 2602 ].map(d => 2603 decodedTextSpanIntersectsWith(startPosition, endPosition - startPosition, d.start!, d.length!) 2604 && d.code); 2605 const badCode = args.errorCodes.find(c => !existingDiagCodes.includes(c)); 2606 if (badCode !== undefined) { 2607 e.message = `BADCLIENT: Bad error code, ${badCode} not found in range ${startPosition}..${endPosition} (found: ${existingDiagCodes.join(", ")}); could have caused this error:\n${e.message}`; 2608 } 2609 throw e; 2610 } 2611 return simplifiedResult ? codeActions.map(codeAction => this.mapCodeFixAction(codeAction)) : codeActions; 2612 } 2613 2614 private getCombinedCodeFix({ scope, fixId }: protocol.GetCombinedCodeFixRequestArgs, simplifiedResult: boolean): protocol.CombinedCodeActions | CombinedCodeActions { 2615 Debug.assert(scope.type === "file"); 2616 const { file, project } = this.getFileAndProject(scope.args); 2617 const res = project.getLanguageService().getCombinedCodeFix({ type: "file", fileName: file }, fixId, this.getFormatOptions(file), this.getPreferences(file)); 2618 if (simplifiedResult) { 2619 return { changes: this.mapTextChangesToCodeEdits(res.changes), commands: res.commands }; 2620 } 2621 else { 2622 return res; 2623 } 2624 } 2625 2626 private applyCodeActionCommand(args: protocol.ApplyCodeActionCommandRequestArgs): {} { 2627 const commands = args.command as CodeActionCommand | CodeActionCommand[]; // They should be sending back the command we sent them. 2628 for (const command of toArray(commands)) { 2629 const { file, project } = this.getFileAndProject(command); 2630 project.getLanguageService().applyCodeActionCommand(command, this.getFormatOptions(file)).then( 2631 _result => { /* TODO: GH#20447 report success message? */ }, 2632 _error => { /* TODO: GH#20447 report errors */ }); 2633 } 2634 return {}; 2635 } 2636 2637 private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { 2638 let startPosition: number | undefined, endPosition: number | undefined; 2639 if (args.startPosition !== undefined) { 2640 startPosition = args.startPosition; 2641 } 2642 else { 2643 startPosition = scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); 2644 // save the result so we don't always recompute 2645 args.startPosition = startPosition; 2646 } 2647 2648 if (args.endPosition !== undefined) { 2649 endPosition = args.endPosition; 2650 } 2651 else { 2652 endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); 2653 args.endPosition = endPosition; 2654 } 2655 2656 return { startPosition, endPosition }; 2657 } 2658 2659 private mapCodeAction({ description, changes, commands }: CodeAction): protocol.CodeAction { 2660 return { description, changes: this.mapTextChangesToCodeEdits(changes), commands }; 2661 } 2662 2663 private mapCodeFixAction({ fixName, description, changes, commands, fixId, fixAllDescription }: CodeFixAction): protocol.CodeFixAction { 2664 return { fixName, description, changes: this.mapTextChangesToCodeEdits(changes), commands, fixId, fixAllDescription }; 2665 } 2666 2667 private mapTextChangesToCodeEdits(textChanges: readonly FileTextChanges[]): protocol.FileCodeEdits[] { 2668 return textChanges.map(change => this.mapTextChangeToCodeEdit(change)); 2669 } 2670 2671 private mapTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits { 2672 const scriptInfo = this.projectService.getScriptInfoOrConfig(textChanges.fileName); 2673 if (!!textChanges.isNewFile === !!scriptInfo) { 2674 if (!scriptInfo) { // and !isNewFile 2675 this.projectService.logErrorForScriptInfoNotFound(textChanges.fileName); 2676 } 2677 Debug.fail("Expected isNewFile for (only) new files. " + JSON.stringify({ isNewFile: !!textChanges.isNewFile, hasScriptInfo: !!scriptInfo })); 2678 } 2679 return scriptInfo 2680 ? { fileName: textChanges.fileName, textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEdit(textChange, scriptInfo)) } 2681 : convertNewFileTextChangeToCodeEdit(textChanges); 2682 } 2683 2684 private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { 2685 return { 2686 start: scriptInfo.positionToLineOffset(change.span.start), 2687 end: scriptInfo.positionToLineOffset(change.span.start + change.span.length), 2688 newText: change.newText ? change.newText : "" 2689 }; 2690 } 2691 2692 private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] | undefined { 2693 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2694 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2695 const position = this.getPosition(args, scriptInfo); 2696 2697 const spans = languageService.getBraceMatchingAtPosition(file, position); 2698 return !spans 2699 ? undefined 2700 : simplifiedResult 2701 ? spans.map(span => toProtocolTextSpan(span, scriptInfo)) 2702 : spans; 2703 } 2704 2705 private getDiagnosticsForProject(next: NextStep, delay: number, fileName: string): void { 2706 if (this.suppressDiagnosticEvents) { 2707 return; 2708 } 2709 2710 const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true, /*excludeConfigFiles*/ true); 2711 if (languageServiceDisabled) { 2712 return; 2713 } 2714 2715 // No need to analyze lib.d.ts 2716 const fileNamesInProject = fileNames!.filter(value => !stringContains(value, "lib.d.ts")); // TODO: GH#18217 2717 if (fileNamesInProject.length === 0) { 2718 return; 2719 } 2720 2721 // Sort the file name list to make the recently touched files come first 2722 const highPriorityFiles: NormalizedPath[] = []; 2723 const mediumPriorityFiles: NormalizedPath[] = []; 2724 const lowPriorityFiles: NormalizedPath[] = []; 2725 const veryLowPriorityFiles: NormalizedPath[] = []; 2726 const normalizedFileName = toNormalizedPath(fileName); 2727 const project = this.projectService.ensureDefaultProjectForFile(normalizedFileName); 2728 for (const fileNameInProject of fileNamesInProject) { 2729 if (this.getCanonicalFileName(fileNameInProject) === this.getCanonicalFileName(fileName)) { 2730 highPriorityFiles.push(fileNameInProject); 2731 } 2732 else { 2733 const info = this.projectService.getScriptInfo(fileNameInProject)!; // TODO: GH#18217 2734 if (!info.isScriptOpen()) { 2735 if (isDeclarationFileName(fileNameInProject)) { 2736 veryLowPriorityFiles.push(fileNameInProject); 2737 } 2738 else { 2739 lowPriorityFiles.push(fileNameInProject); 2740 } 2741 } 2742 else { 2743 mediumPriorityFiles.push(fileNameInProject); 2744 } 2745 } 2746 } 2747 2748 const sortedFiles = [...highPriorityFiles, ...mediumPriorityFiles, ...lowPriorityFiles, ...veryLowPriorityFiles]; 2749 const checkList = sortedFiles.map(fileName => ({ fileName, project })); 2750 // Project level error analysis runs on background files too, therefore 2751 // doesn't require the file to be opened 2752 this.updateErrorCheck(next, checkList, delay, /*requireOpen*/ false); 2753 } 2754 2755 private configurePlugin(args: protocol.ConfigurePluginRequestArguments) { 2756 this.projectService.configurePlugin(args); 2757 } 2758 2759 private getSmartSelectionRange(args: protocol.SelectionRangeRequestArgs, simplifiedResult: boolean) { 2760 const { locations } = args; 2761 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2762 const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(file)); 2763 2764 return map(locations, location => { 2765 const pos = this.getPosition(location, scriptInfo); 2766 const selectionRange = languageService.getSmartSelectionRange(file, pos); 2767 return simplifiedResult ? this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; 2768 }); 2769 } 2770 2771 private toggleLineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { 2772 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2773 const scriptInfo = this.projectService.getScriptInfo(file)!; 2774 const textRange = this.getRange(args, scriptInfo); 2775 2776 const textChanges = languageService.toggleLineComment(file, textRange); 2777 2778 if (simplifiedResult) { 2779 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2780 2781 return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); 2782 } 2783 2784 return textChanges; 2785 } 2786 2787 private toggleMultilineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { 2788 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2789 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2790 const textRange = this.getRange(args, scriptInfo); 2791 2792 const textChanges = languageService.toggleMultilineComment(file, textRange); 2793 2794 if (simplifiedResult) { 2795 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2796 2797 return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); 2798 } 2799 2800 return textChanges; 2801 } 2802 2803 private commentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { 2804 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2805 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2806 const textRange = this.getRange(args, scriptInfo); 2807 2808 const textChanges = languageService.commentSelection(file, textRange); 2809 2810 if (simplifiedResult) { 2811 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2812 2813 return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); 2814 } 2815 2816 return textChanges; 2817 } 2818 2819 private uncommentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { 2820 const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); 2821 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2822 const textRange = this.getRange(args, scriptInfo); 2823 2824 const textChanges = languageService.uncommentSelection(file, textRange); 2825 2826 if (simplifiedResult) { 2827 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; 2828 2829 return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); 2830 } 2831 2832 return textChanges; 2833 } 2834 2835 private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { 2836 const result: protocol.SelectionRange = { 2837 textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo), 2838 }; 2839 if (selectionRange.parent) { 2840 result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo); 2841 } 2842 return result; 2843 } 2844 2845 private getScriptInfoFromProjectService(file: string) { 2846 const normalizedFile = toNormalizedPath(file); 2847 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(normalizedFile); 2848 if (!scriptInfo) { 2849 this.projectService.logErrorForScriptInfoNotFound(normalizedFile); 2850 return Errors.ThrowNoProject(); 2851 } 2852 return scriptInfo; 2853 } 2854 2855 private toProtocolCallHierarchyItem(item: CallHierarchyItem): protocol.CallHierarchyItem { 2856 const scriptInfo = this.getScriptInfoFromProjectService(item.file); 2857 return { 2858 name: item.name, 2859 kind: item.kind, 2860 kindModifiers: item.kindModifiers, 2861 file: item.file, 2862 containerName: item.containerName, 2863 span: toProtocolTextSpan(item.span, scriptInfo), 2864 selectionSpan: toProtocolTextSpan(item.selectionSpan, scriptInfo) 2865 }; 2866 } 2867 2868 private toProtocolCallHierarchyIncomingCall(incomingCall: CallHierarchyIncomingCall): protocol.CallHierarchyIncomingCall { 2869 const scriptInfo = this.getScriptInfoFromProjectService(incomingCall.from.file); 2870 return { 2871 from: this.toProtocolCallHierarchyItem(incomingCall.from), 2872 fromSpans: incomingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo)) 2873 }; 2874 } 2875 2876 private toProtocolCallHierarchyOutgoingCall(outgoingCall: CallHierarchyOutgoingCall, scriptInfo: ScriptInfo): protocol.CallHierarchyOutgoingCall { 2877 return { 2878 to: this.toProtocolCallHierarchyItem(outgoingCall.to), 2879 fromSpans: outgoingCall.fromSpans.map(fromSpan => toProtocolTextSpan(fromSpan, scriptInfo)) 2880 }; 2881 } 2882 2883 private prepareCallHierarchy(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyItem | protocol.CallHierarchyItem[] | undefined { 2884 const { file, project } = this.getFileAndProject(args); 2885 const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); 2886 if (scriptInfo) { 2887 const position = this.getPosition(args, scriptInfo); 2888 const result = project.getLanguageService().prepareCallHierarchy(file, position); 2889 return result && mapOneOrMany(result, item => this.toProtocolCallHierarchyItem(item)); 2890 } 2891 return undefined; 2892 } 2893 2894 private provideCallHierarchyIncomingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyIncomingCall[] { 2895 const { file, project } = this.getFileAndProject(args); 2896 const scriptInfo = this.getScriptInfoFromProjectService(file); 2897 const incomingCalls = project.getLanguageService().provideCallHierarchyIncomingCalls(file, this.getPosition(args, scriptInfo)); 2898 return incomingCalls.map(call => this.toProtocolCallHierarchyIncomingCall(call)); 2899 } 2900 2901 private provideCallHierarchyOutgoingCalls(args: protocol.FileLocationRequestArgs): protocol.CallHierarchyOutgoingCall[] { 2902 const { file, project } = this.getFileAndProject(args); 2903 const scriptInfo = this.getScriptInfoFromProjectService(file); 2904 const outgoingCalls = project.getLanguageService().provideCallHierarchyOutgoingCalls(file, this.getPosition(args, scriptInfo)); 2905 return outgoingCalls.map(call => this.toProtocolCallHierarchyOutgoingCall(call, scriptInfo)); 2906 } 2907 2908 getCanonicalFileName(fileName: string) { 2909 const name = this.host.useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName); 2910 return normalizePath(name); 2911 } 2912 2913 exit() { /*overridden*/ } 2914 2915 private notRequired(): HandlerResponse { 2916 return { responseRequired: false }; 2917 } 2918 2919 private requiredResponse(response: {} | undefined): HandlerResponse { 2920 return { response, responseRequired: true }; 2921 } 2922 2923 private handlers = new Map(getEntries<(request: protocol.Request) => HandlerResponse>({ 2924 [CommandNames.Status]: () => { 2925 const response: protocol.StatusResponseBody = { version: ts.version }; // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier 2926 return this.requiredResponse(response); 2927 }, 2928 [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { 2929 this.projectService.openExternalProject(request.arguments); 2930 // TODO: GH#20447 report errors 2931 return this.requiredResponse(/*response*/ true); 2932 }, 2933 [CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => { 2934 this.projectService.openExternalProjects(request.arguments.projects); 2935 // TODO: GH#20447 report errors 2936 return this.requiredResponse(/*response*/ true); 2937 }, 2938 [CommandNames.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => { 2939 this.projectService.closeExternalProject(request.arguments.projectFileName); 2940 // TODO: GH#20447 report errors 2941 return this.requiredResponse(/*response*/ true); 2942 }, 2943 [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { 2944 const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects, request.arguments.includeProjectReferenceRedirectInfo); 2945 if (!result.some(p => p.projectErrors && p.projectErrors.length !== 0)) { 2946 return this.requiredResponse(result); 2947 } 2948 const converted = map(result, p => { 2949 if (!p.projectErrors || p.projectErrors.length === 0) { 2950 return p; 2951 } 2952 return { 2953 info: p.info, 2954 changes: p.changes, 2955 files: p.files, 2956 projectErrors: this.convertToDiagnosticsWithLinePosition(p.projectErrors, /*scriptInfo*/ undefined) 2957 }; 2958 }); 2959 return this.requiredResponse(converted); 2960 }, 2961 [CommandNames.UpdateOpen]: (request: protocol.UpdateOpenRequest) => { 2962 this.changeSeq++; 2963 this.projectService.applyChangesInOpenFiles( 2964 request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), file => ({ 2965 fileName: file.file, 2966 content: file.fileContent, 2967 scriptKind: file.scriptKindName, 2968 projectRootPath: file.projectRootPath 2969 })), 2970 request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({ 2971 fileName: file.fileName, 2972 changes: mapDefinedIterator(arrayReverseIterator(file.textChanges), change => { 2973 const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(file.fileName)); 2974 const start = scriptInfo.lineOffsetToPosition(change.start.line, change.start.offset); 2975 const end = scriptInfo.lineOffsetToPosition(change.end.line, change.end.offset); 2976 return start >= 0 ? { span: { start, length: end - start }, newText: change.newText } : undefined; 2977 }) 2978 })), 2979 request.arguments.closedFiles 2980 ); 2981 return this.requiredResponse(/*response*/ true); 2982 }, 2983 [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { 2984 this.changeSeq++; 2985 this.projectService.applyChangesInOpenFiles( 2986 request.arguments.openFiles && arrayIterator(request.arguments.openFiles), 2987 request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({ 2988 fileName: file.fileName, 2989 // apply changes in reverse order 2990 changes: arrayReverseIterator(file.changes) 2991 })), 2992 request.arguments.closedFiles 2993 ); 2994 // TODO: report errors 2995 return this.requiredResponse(/*response*/ true); 2996 }, 2997 [CommandNames.Exit]: () => { 2998 this.exit(); 2999 return this.notRequired(); 3000 }, 3001 [CommandNames.Definition]: (request: protocol.DefinitionRequest) => { 3002 return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ true)); 3003 }, 3004 [CommandNames.DefinitionFull]: (request: protocol.DefinitionRequest) => { 3005 return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ false)); 3006 }, 3007 [CommandNames.DefinitionAndBoundSpan]: (request: protocol.DefinitionAndBoundSpanRequest) => { 3008 return this.requiredResponse(this.getDefinitionAndBoundSpan(request.arguments, /*simplifiedResult*/ true)); 3009 }, 3010 [CommandNames.DefinitionAndBoundSpanFull]: (request: protocol.DefinitionAndBoundSpanRequest) => { 3011 return this.requiredResponse(this.getDefinitionAndBoundSpan(request.arguments, /*simplifiedResult*/ false)); 3012 }, 3013 [CommandNames.FindSourceDefinition]: (request: protocol.FindSourceDefinitionRequest) => { 3014 return this.requiredResponse(this.findSourceDefinition(request.arguments)); 3015 }, 3016 [CommandNames.EmitOutput]: (request: protocol.EmitOutputRequest) => { 3017 return this.requiredResponse(this.getEmitOutput(request.arguments)); 3018 }, 3019 [CommandNames.TypeDefinition]: (request: protocol.FileLocationRequest) => { 3020 return this.requiredResponse(this.getTypeDefinition(request.arguments)); 3021 }, 3022 [CommandNames.Implementation]: (request: protocol.Request) => { 3023 return this.requiredResponse(this.getImplementation(request.arguments, /*simplifiedResult*/ true)); 3024 }, 3025 [CommandNames.ImplementationFull]: (request: protocol.Request) => { 3026 return this.requiredResponse(this.getImplementation(request.arguments, /*simplifiedResult*/ false)); 3027 }, 3028 [CommandNames.References]: (request: protocol.FileLocationRequest) => { 3029 return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ true)); 3030 }, 3031 [CommandNames.ReferencesFull]: (request: protocol.FileLocationRequest) => { 3032 return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ false)); 3033 }, 3034 [CommandNames.Rename]: (request: protocol.RenameRequest) => { 3035 return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ true)); 3036 }, 3037 [CommandNames.RenameLocationsFull]: (request: protocol.RenameFullRequest) => { 3038 return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ false)); 3039 }, 3040 [CommandNames.RenameInfoFull]: (request: protocol.FileLocationRequest) => { 3041 return this.requiredResponse(this.getRenameInfo(request.arguments)); 3042 }, 3043 [CommandNames.Open]: (request: protocol.OpenRequest) => { 3044 this.openClientFile( 3045 toNormalizedPath(request.arguments.file), 3046 request.arguments.fileContent, 3047 convertScriptKindName(request.arguments.scriptKindName!), // TODO: GH#18217 3048 request.arguments.projectRootPath ? toNormalizedPath(request.arguments.projectRootPath) : undefined); 3049 return this.notRequired(); 3050 }, 3051 [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { 3052 return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplifiedResult*/ true)); 3053 }, 3054 [CommandNames.QuickinfoFull]: (request: protocol.QuickInfoRequest) => { 3055 return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplifiedResult*/ false)); 3056 }, 3057 [CommandNames.GetOutliningSpans]: (request: protocol.FileRequest) => { 3058 return this.requiredResponse(this.getOutliningSpans(request.arguments, /*simplifiedResult*/ true)); 3059 }, 3060 [CommandNames.GetOutliningSpansFull]: (request: protocol.FileRequest) => { 3061 return this.requiredResponse(this.getOutliningSpans(request.arguments, /*simplifiedResult*/ false)); 3062 }, 3063 [CommandNames.TodoComments]: (request: protocol.TodoCommentRequest) => { 3064 return this.requiredResponse(this.getTodoComments(request.arguments)); 3065 }, 3066 [CommandNames.Indentation]: (request: protocol.IndentationRequest) => { 3067 return this.requiredResponse(this.getIndentation(request.arguments)); 3068 }, 3069 [CommandNames.NameOrDottedNameSpan]: (request: protocol.FileLocationRequest) => { 3070 return this.requiredResponse(this.getNameOrDottedNameSpan(request.arguments)); 3071 }, 3072 [CommandNames.BreakpointStatement]: (request: protocol.FileLocationRequest) => { 3073 return this.requiredResponse(this.getBreakpointStatement(request.arguments)); 3074 }, 3075 [CommandNames.BraceCompletion]: (request: protocol.BraceCompletionRequest) => { 3076 return this.requiredResponse(this.isValidBraceCompletion(request.arguments)); 3077 }, 3078 [CommandNames.DocCommentTemplate]: (request: protocol.DocCommentTemplateRequest) => { 3079 return this.requiredResponse(this.getDocCommentTemplate(request.arguments)); 3080 }, 3081 [CommandNames.GetSpanOfEnclosingComment]: (request: protocol.SpanOfEnclosingCommentRequest) => { 3082 return this.requiredResponse(this.getSpanOfEnclosingComment(request.arguments)); 3083 }, 3084 [CommandNames.FileReferences]: (request: protocol.FileReferencesRequest) => { 3085 return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ true)); 3086 }, 3087 [CommandNames.FileReferencesFull]: (request: protocol.FileReferencesRequest) => { 3088 return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ false)); 3089 }, 3090 [CommandNames.Format]: (request: protocol.FormatRequest) => { 3091 return this.requiredResponse(this.getFormattingEditsForRange(request.arguments)); 3092 }, 3093 [CommandNames.Formatonkey]: (request: protocol.FormatOnKeyRequest) => { 3094 return this.requiredResponse(this.getFormattingEditsAfterKeystroke(request.arguments)); 3095 }, 3096 [CommandNames.FormatFull]: (request: protocol.FormatRequest) => { 3097 return this.requiredResponse(this.getFormattingEditsForDocumentFull(request.arguments)); 3098 }, 3099 [CommandNames.FormatonkeyFull]: (request: protocol.FormatOnKeyRequest) => { 3100 return this.requiredResponse(this.getFormattingEditsAfterKeystrokeFull(request.arguments)); 3101 }, 3102 [CommandNames.FormatRangeFull]: (request: protocol.FormatRequest) => { 3103 return this.requiredResponse(this.getFormattingEditsForRangeFull(request.arguments)); 3104 }, 3105 [CommandNames.CompletionInfo]: (request: protocol.CompletionsRequest) => { 3106 return this.requiredResponse(this.getCompletions(request.arguments, CommandNames.CompletionInfo)); 3107 }, 3108 [CommandNames.Completions]: (request: protocol.CompletionsRequest) => { 3109 return this.requiredResponse(this.getCompletions(request.arguments, CommandNames.Completions)); 3110 }, 3111 [CommandNames.CompletionsFull]: (request: protocol.CompletionsRequest) => { 3112 return this.requiredResponse(this.getCompletions(request.arguments, CommandNames.CompletionsFull)); 3113 }, 3114 [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { 3115 return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*fullResult*/ false)); 3116 }, 3117 [CommandNames.CompletionDetailsFull]: (request: protocol.CompletionDetailsRequest) => { 3118 return this.requiredResponse(this.getCompletionEntryDetails(request.arguments, /*fullResult*/ true)); 3119 }, 3120 [CommandNames.CompileOnSaveAffectedFileList]: (request: protocol.CompileOnSaveAffectedFileListRequest) => { 3121 return this.requiredResponse(this.getCompileOnSaveAffectedFileList(request.arguments)); 3122 }, 3123 [CommandNames.CompileOnSaveEmitFile]: (request: protocol.CompileOnSaveEmitFileRequest) => { 3124 return this.requiredResponse(this.emitFile(request.arguments)); 3125 }, 3126 [CommandNames.SignatureHelp]: (request: protocol.SignatureHelpRequest) => { 3127 return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ true)); 3128 }, 3129 [CommandNames.SignatureHelpFull]: (request: protocol.SignatureHelpRequest) => { 3130 return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ false)); 3131 }, 3132 [CommandNames.CompilerOptionsDiagnosticsFull]: (request: protocol.CompilerOptionsDiagnosticsRequest) => { 3133 return this.requiredResponse(this.getCompilerOptionsDiagnostics(request.arguments)); 3134 }, 3135 [CommandNames.EncodedSyntacticClassificationsFull]: (request: protocol.EncodedSyntacticClassificationsRequest) => { 3136 return this.requiredResponse(this.getEncodedSyntacticClassifications(request.arguments)); 3137 }, 3138 [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.EncodedSemanticClassificationsRequest) => { 3139 return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments)); 3140 }, 3141 [CommandNames.Cleanup]: () => { 3142 this.cleanup(); 3143 return this.requiredResponse(/*response*/ true); 3144 }, 3145 [CommandNames.SemanticDiagnosticsSync]: (request: protocol.SemanticDiagnosticsSyncRequest) => { 3146 return this.requiredResponse(this.getSemanticDiagnosticsSync(request.arguments)); 3147 }, 3148 [CommandNames.SyntacticDiagnosticsSync]: (request: protocol.SyntacticDiagnosticsSyncRequest) => { 3149 return this.requiredResponse(this.getSyntacticDiagnosticsSync(request.arguments)); 3150 }, 3151 [CommandNames.SuggestionDiagnosticsSync]: (request: protocol.SuggestionDiagnosticsSyncRequest) => { 3152 return this.requiredResponse(this.getSuggestionDiagnosticsSync(request.arguments)); 3153 }, 3154 [CommandNames.Geterr]: (request: protocol.GeterrRequest) => { 3155 this.errorCheck.startNew(next => this.getDiagnostics(next, request.arguments.delay, request.arguments.files)); 3156 return this.notRequired(); 3157 }, 3158 [CommandNames.GeterrForProject]: (request: protocol.GeterrForProjectRequest) => { 3159 this.errorCheck.startNew(next => this.getDiagnosticsForProject(next, request.arguments.delay, request.arguments.file)); 3160 return this.notRequired(); 3161 }, 3162 [CommandNames.Change]: (request: protocol.ChangeRequest) => { 3163 this.change(request.arguments); 3164 return this.notRequired(); 3165 }, 3166 [CommandNames.Configure]: (request: protocol.ConfigureRequest) => { 3167 this.projectService.setHostConfiguration(request.arguments); 3168 this.doOutput(/*info*/ undefined, CommandNames.Configure, request.seq, /*success*/ true); 3169 return this.notRequired(); 3170 }, 3171 [CommandNames.Reload]: (request: protocol.ReloadRequest) => { 3172 this.reload(request.arguments, request.seq); 3173 return this.requiredResponse({ reloadFinished: true }); 3174 }, 3175 [CommandNames.Saveto]: (request: protocol.Request) => { 3176 const savetoArgs = request.arguments as protocol.SavetoRequestArgs; 3177 this.saveToTmp(savetoArgs.file, savetoArgs.tmpfile); 3178 return this.notRequired(); 3179 }, 3180 [CommandNames.Close]: (request: protocol.Request) => { 3181 const closeArgs = request.arguments as protocol.FileRequestArgs; 3182 this.closeClientFile(closeArgs.file); 3183 return this.notRequired(); 3184 }, 3185 [CommandNames.Navto]: (request: protocol.NavtoRequest) => { 3186 return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ true)); 3187 }, 3188 [CommandNames.NavtoFull]: (request: protocol.NavtoRequest) => { 3189 return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ false)); 3190 }, 3191 [CommandNames.Brace]: (request: protocol.FileLocationRequest) => { 3192 return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ true)); 3193 }, 3194 [CommandNames.BraceFull]: (request: protocol.FileLocationRequest) => { 3195 return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ false)); 3196 }, 3197 [CommandNames.NavBar]: (request: protocol.FileRequest) => { 3198 return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ true)); 3199 }, 3200 [CommandNames.NavBarFull]: (request: protocol.FileRequest) => { 3201 return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ false)); 3202 }, 3203 [CommandNames.NavTree]: (request: protocol.FileRequest) => { 3204 return this.requiredResponse(this.getNavigationTree(request.arguments, /*simplifiedResult*/ true)); 3205 }, 3206 [CommandNames.NavTreeFull]: (request: protocol.FileRequest) => { 3207 return this.requiredResponse(this.getNavigationTree(request.arguments, /*simplifiedResult*/ false)); 3208 }, 3209 [CommandNames.Occurrences]: (request: protocol.FileLocationRequest) => { 3210 return this.requiredResponse(this.getOccurrences(request.arguments)); 3211 }, 3212 [CommandNames.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { 3213 return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); 3214 }, 3215 [CommandNames.DocumentHighlightsFull]: (request: protocol.DocumentHighlightsRequest) => { 3216 return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ false)); 3217 }, 3218 [CommandNames.CompilerOptionsForInferredProjects]: (request: protocol.SetCompilerOptionsForInferredProjectsRequest) => { 3219 this.setCompilerOptionsForInferredProjects(request.arguments); 3220 return this.requiredResponse(/*response*/ true); 3221 }, 3222 [CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => { 3223 return this.requiredResponse(this.getProjectInfo(request.arguments)); 3224 }, 3225 [CommandNames.ReloadProjects]: () => { 3226 this.projectService.reloadProjects(); 3227 return this.notRequired(); 3228 }, 3229 [CommandNames.JsxClosingTag]: (request: protocol.JsxClosingTagRequest) => { 3230 return this.requiredResponse(this.getJsxClosingTag(request.arguments)); 3231 }, 3232 [CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => { 3233 return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true)); 3234 }, 3235 [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { 3236 return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); 3237 }, 3238 [CommandNames.GetCombinedCodeFix]: (request: protocol.GetCombinedCodeFixRequest) => { 3239 return this.requiredResponse(this.getCombinedCodeFix(request.arguments, /*simplifiedResult*/ true)); 3240 }, 3241 [CommandNames.GetCombinedCodeFixFull]: (request: protocol.GetCombinedCodeFixRequest) => { 3242 return this.requiredResponse(this.getCombinedCodeFix(request.arguments, /*simplifiedResult*/ false)); 3243 }, 3244 [CommandNames.ApplyCodeActionCommand]: (request: protocol.ApplyCodeActionCommandRequest) => { 3245 return this.requiredResponse(this.applyCodeActionCommand(request.arguments)); 3246 }, 3247 [CommandNames.GetSupportedCodeFixes]: () => { 3248 return this.requiredResponse(this.getSupportedCodeFixes()); 3249 }, 3250 [CommandNames.GetApplicableRefactors]: (request: protocol.GetApplicableRefactorsRequest) => { 3251 return this.requiredResponse(this.getApplicableRefactors(request.arguments)); 3252 }, 3253 [CommandNames.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => { 3254 return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true)); 3255 }, 3256 [CommandNames.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => { 3257 return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false)); 3258 }, 3259 [CommandNames.OrganizeImports]: (request: protocol.OrganizeImportsRequest) => { 3260 return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ true)); 3261 }, 3262 [CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => { 3263 return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false)); 3264 }, 3265 [CommandNames.GetEditsForFileRename]: (request: protocol.GetEditsForFileRenameRequest) => { 3266 return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ true)); 3267 }, 3268 [CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => { 3269 return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false)); 3270 }, 3271 [CommandNames.ConfigurePlugin]: (request: protocol.ConfigurePluginRequest) => { 3272 this.configurePlugin(request.arguments); 3273 this.doOutput(/*info*/ undefined, CommandNames.ConfigurePlugin, request.seq, /*success*/ true); 3274 return this.notRequired(); 3275 }, 3276 [CommandNames.SelectionRange]: (request: protocol.SelectionRangeRequest) => { 3277 return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ true)); 3278 }, 3279 [CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => { 3280 return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false)); 3281 }, 3282 [CommandNames.PrepareCallHierarchy]: (request: protocol.PrepareCallHierarchyRequest) => { 3283 return this.requiredResponse(this.prepareCallHierarchy(request.arguments)); 3284 }, 3285 [CommandNames.ProvideCallHierarchyIncomingCalls]: (request: protocol.ProvideCallHierarchyIncomingCallsRequest) => { 3286 return this.requiredResponse(this.provideCallHierarchyIncomingCalls(request.arguments)); 3287 }, 3288 [CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => { 3289 return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments)); 3290 }, 3291 [CommandNames.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => { 3292 return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ true)); 3293 }, 3294 [CommandNames.ToggleLineCommentFull]: (request: protocol.ToggleLineCommentRequest) => { 3295 return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ false)); 3296 }, 3297 [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { 3298 return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ true)); 3299 }, 3300 [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { 3301 return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ false)); 3302 }, 3303 [CommandNames.CommentSelection]: (request: protocol.CommentSelectionRequest) => { 3304 return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ true)); 3305 }, 3306 [CommandNames.CommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { 3307 return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ false)); 3308 }, 3309 [CommandNames.UncommentSelection]: (request: protocol.UncommentSelectionRequest) => { 3310 return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ true)); 3311 }, 3312 [CommandNames.UncommentSelectionFull]: (request: protocol.UncommentSelectionRequest) => { 3313 return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ false)); 3314 }, 3315 [CommandNames.ProvideInlayHints]: (request: protocol.InlayHintsRequest) => { 3316 return this.requiredResponse(this.provideInlayHints(request.arguments)); 3317 } 3318 })); 3319 3320 public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { 3321 if (this.handlers.has(command)) { 3322 throw new Error(`Protocol handler already exists for command "${command}"`); 3323 } 3324 this.handlers.set(command, handler); 3325 } 3326 3327 private setCurrentRequest(requestId: number): void { 3328 Debug.assert(this.currentRequestId === undefined); 3329 this.currentRequestId = requestId; 3330 this.cancellationToken.setRequest(requestId); 3331 } 3332 3333 private resetCurrentRequest(requestId: number): void { 3334 Debug.assert(this.currentRequestId === requestId); 3335 this.currentRequestId = undefined!; // TODO: GH#18217 3336 this.cancellationToken.resetRequest(requestId); 3337 } 3338 3339 public executeWithRequestId<T>(requestId: number, f: () => T) { 3340 try { 3341 this.setCurrentRequest(requestId); 3342 return f(); 3343 } 3344 finally { 3345 this.resetCurrentRequest(requestId); 3346 } 3347 } 3348 3349 public executeCommand(request: protocol.Request): HandlerResponse { 3350 const handler = this.handlers.get(request.command); 3351 if (handler) { 3352 const response = this.executeWithRequestId(request.seq, () => handler(request)); 3353 this.projectService.enableRequestedPlugins(); 3354 return response; 3355 } 3356 else { 3357 this.logger.msg(`Unrecognized JSON command:${stringifyIndented(request)}`, Msg.Err); 3358 this.doOutput(/*info*/ undefined, CommandNames.Unknown, request.seq, /*success*/ false, `Unrecognized JSON command: ${request.command}`); 3359 return { responseRequired: false }; 3360 } 3361 } 3362 3363 public onMessage(message: TMessage) { 3364 this.gcTimer.scheduleCollect(); 3365 3366 this.performanceData = undefined; 3367 3368 let start: number[] | undefined; 3369 if (this.logger.hasLevel(LogLevel.requestTime)) { 3370 start = this.hrtime(); 3371 if (this.logger.hasLevel(LogLevel.verbose)) { 3372 this.logger.info(`request:${indent(this.toStringMessage(message))}`); 3373 } 3374 } 3375 3376 let request: protocol.Request | undefined; 3377 let relevantFile: protocol.FileRequestArgs | undefined; 3378 try { 3379 request = this.parseMessage(message); 3380 relevantFile = request.arguments && (request as protocol.FileRequest).arguments.file ? (request as protocol.FileRequest).arguments : undefined; 3381 3382 tracing?.instant(tracing.Phase.Session, "request", { seq: request.seq, command: request.command }); 3383 perfLogger.logStartCommand("" + request.command, this.toStringMessage(message).substring(0, 100)); 3384 3385 tracing?.push(tracing.Phase.Session, "executeCommand", { seq: request.seq, command: request.command }, /*separateBeginAndEnd*/ true); 3386 const { response, responseRequired } = this.executeCommand(request); 3387 tracing?.pop(); 3388 3389 if (this.logger.hasLevel(LogLevel.requestTime)) { 3390 const elapsedTime = hrTimeToMilliseconds(this.hrtime(start)).toFixed(4); 3391 if (responseRequired) { 3392 this.logger.perftrc(`${request.seq}::${request.command}: elapsed time (in milliseconds) ${elapsedTime}`); 3393 } 3394 else { 3395 this.logger.perftrc(`${request.seq}::${request.command}: async elapsed time (in milliseconds) ${elapsedTime}`); 3396 } 3397 } 3398 3399 // Note: Log before writing the response, else the editor can complete its activity before the server does 3400 perfLogger.logStopCommand("" + request.command, "Success"); 3401 tracing?.instant(tracing.Phase.Session, "response", { seq: request.seq, command: request.command, success: !!response }); 3402 if (response) { 3403 this.doOutput(response, request.command, request.seq, /*success*/ true); 3404 } 3405 else if (responseRequired) { 3406 this.doOutput(/*info*/ undefined, request.command, request.seq, /*success*/ false, "No content available."); 3407 } 3408 } 3409 catch (err) { 3410 // Cancellation or an error may have left incomplete events on the tracing stack. 3411 tracing?.popAll(); 3412 3413 if (err instanceof OperationCanceledException) { 3414 // Handle cancellation exceptions 3415 perfLogger.logStopCommand("" + (request && request.command), "Canceled: " + err); 3416 tracing?.instant(tracing.Phase.Session, "commandCanceled", { seq: request?.seq, command: request?.command }); 3417 this.doOutput({ canceled: true }, request!.command, request!.seq, /*success*/ true); 3418 return; 3419 } 3420 3421 this.logErrorWorker(err, this.toStringMessage(message), relevantFile); 3422 perfLogger.logStopCommand("" + (request && request.command), "Error: " + err); 3423 tracing?.instant(tracing.Phase.Session, "commandError", { seq: request?.seq, command: request?.command, message: (err as Error).message }); 3424 3425 this.doOutput( 3426 /*info*/ undefined, 3427 request ? request.command : CommandNames.Unknown, 3428 request ? request.seq : 0, 3429 /*success*/ false, 3430 "Error processing request. " + (err as StackTraceError).message + "\n" + (err as StackTraceError).stack); 3431 } 3432 } 3433 3434 protected parseMessage(message: TMessage): protocol.Request { 3435 return JSON.parse(message as any as string) as protocol.Request; 3436 } 3437 3438 protected toStringMessage(message: TMessage): string { 3439 return message as any as string; 3440 } 3441 3442 private getFormatOptions(file: NormalizedPath): FormatCodeSettings { 3443 return this.projectService.getFormatCodeOptions(file); 3444 } 3445 3446 private getPreferences(file: NormalizedPath): protocol.UserPreferences { 3447 return this.projectService.getPreferences(file); 3448 } 3449 3450 private getHostFormatOptions(): FormatCodeSettings { 3451 return this.projectService.getHostFormatCodeOptions(); 3452 } 3453 3454 private getHostPreferences(): protocol.UserPreferences { 3455 return this.projectService.getHostPreferences(); 3456 } 3457 } 3458 3459 interface FileAndProject { 3460 readonly file: NormalizedPath; 3461 readonly project: Project; 3462 } 3463 3464 function toProtocolTextSpan(textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.TextSpan { 3465 return { 3466 start: scriptInfo.positionToLineOffset(textSpan.start), 3467 end: scriptInfo.positionToLineOffset(textSpanEnd(textSpan)) 3468 }; 3469 } 3470 3471 function toProtocolTextSpanWithContext(span: TextSpan, contextSpan: TextSpan | undefined, scriptInfo: ScriptInfo): protocol.TextSpanWithContext { 3472 const textSpan = toProtocolTextSpan(span, scriptInfo); 3473 const contextTextSpan = contextSpan && toProtocolTextSpan(contextSpan, scriptInfo); 3474 return contextTextSpan ? 3475 { ...textSpan, contextStart: contextTextSpan.start, contextEnd: contextTextSpan.end } : 3476 textSpan; 3477 } 3478 3479 function convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfoOrConfig): protocol.CodeEdit { 3480 return { start: positionToLineOffset(scriptInfo, change.span.start), end: positionToLineOffset(scriptInfo, textSpanEnd(change.span)), newText: change.newText }; 3481 } 3482 3483 function positionToLineOffset(info: ScriptInfoOrConfig, position: number): protocol.Location { 3484 return isConfigFile(info) ? locationFromLineAndCharacter(info.getLineAndCharacterOfPosition(position)) : info.positionToLineOffset(position); 3485 } 3486 3487 function locationFromLineAndCharacter(lc: LineAndCharacter): protocol.Location { 3488 return { line: lc.line + 1, offset: lc.character + 1 }; 3489 } 3490 3491 function convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits { 3492 Debug.assert(textChanges.textChanges.length === 1); 3493 const change = first(textChanges.textChanges); 3494 Debug.assert(change.span.start === 0 && change.span.length === 0); 3495 return { fileName: textChanges.fileName, textChanges: [{ start: { line: 0, offset: 0 }, end: { line: 0, offset: 0 }, newText: change.newText }] }; 3496 } 3497 3498 export interface HandlerResponse { 3499 response?: {}; 3500 responseRequired?: boolean; 3501 } 3502 3503 /* @internal */ // Exported only for tests 3504 export function getLocationInNewDocument(oldText: string, renameFilename: string, renameLocation: number, edits: readonly FileTextChanges[]): protocol.Location { 3505 const newText = applyEdits(oldText, renameFilename, edits); 3506 const { line, character } = computeLineAndCharacterOfPosition(computeLineStarts(newText), renameLocation); 3507 return { line: line + 1, offset: character + 1 }; 3508 } 3509 3510 function applyEdits(text: string, textFilename: string, edits: readonly FileTextChanges[]): string { 3511 for (const { fileName, textChanges } of edits) { 3512 if (fileName !== textFilename) { 3513 continue; 3514 } 3515 3516 for (let i = textChanges.length - 1; i >= 0; i--) { 3517 const { newText, span: { start, length } } = textChanges[i]; 3518 text = text.slice(0, start) + newText + text.slice(start + length); 3519 } 3520 } 3521 3522 return text; 3523 } 3524 3525 function referenceEntryToReferencesResponseItem(projectService: ProjectService, { fileName, textSpan, contextSpan, isWriteAccess, isDefinition }: ReferencedSymbolEntry, { disableLineTextInReferences }: protocol.UserPreferences): protocol.ReferencesResponseItem { 3526 const scriptInfo = Debug.checkDefined(projectService.getScriptInfo(fileName)); 3527 const span = toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo); 3528 const lineText = disableLineTextInReferences ? undefined : getLineText(scriptInfo, span); 3529 return { 3530 file: fileName, 3531 ...span, 3532 lineText, 3533 isWriteAccess, 3534 isDefinition 3535 }; 3536 } 3537 3538 function getLineText(scriptInfo: ScriptInfo, span: protocol.TextSpanWithContext) { 3539 const lineSpan = scriptInfo.lineToTextSpan(span.start.line - 1); 3540 return scriptInfo.getSnapshot().getText(lineSpan.start, textSpanEnd(lineSpan)).replace(/\r|\n/g, ""); 3541 } 3542 3543 function isCompletionEntryData(data: any): data is CompletionEntryData { 3544 return data === undefined || data && typeof data === "object" 3545 && typeof data.exportName === "string" 3546 && (data.fileName === undefined || typeof data.fileName === "string") 3547 && (data.ambientModuleName === undefined || typeof data.ambientModuleName === "string" 3548 && (data.isPackageJsonImport === undefined || typeof data.isPackageJsonImport === "boolean")); 3549 } 3550} 3551