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