1namespace ts { 2 export interface DocumentHighlights { 3 fileName: string; 4 highlightSpans: HighlightSpan[]; 5 } 6 7 /* @internal */ 8 export namespace DocumentHighlights { 9 export function getDocumentHighlights(program: Program, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { 10 const node = getTouchingPropertyName(sourceFile, position); 11 12 if (node.parent && (isJsxOpeningElement(node.parent) && node.parent.tagName === node || isJsxClosingElement(node.parent))) { 13 // For a JSX element, just highlight the matching tag, not all references. 14 const { openingElement, closingElement } = node.parent.parent; 15 const highlightSpans = [openingElement, closingElement].map(({ tagName }) => getHighlightSpanForNode(tagName, sourceFile)); 16 return [{ fileName: sourceFile.fileName, highlightSpans }]; 17 } 18 19 return getSemanticDocumentHighlights(position, node, program, cancellationToken, sourceFilesToSearch) || getSyntacticDocumentHighlights(node, sourceFile); 20 } 21 22 function getHighlightSpanForNode(node: Node, sourceFile: SourceFile): HighlightSpan { 23 return { 24 fileName: sourceFile.fileName, 25 textSpan: createTextSpanFromNode(node, sourceFile), 26 kind: HighlightSpanKind.none 27 }; 28 } 29 30 function getSemanticDocumentHighlights(position: number, node: Node, program: Program, cancellationToken: CancellationToken, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { 31 const sourceFilesSet = new Set(sourceFilesToSearch.map(f => f.fileName)); 32 const referenceEntries = FindAllReferences.getReferenceEntriesForNode(position, node, program, sourceFilesToSearch, cancellationToken, /*options*/ undefined, sourceFilesSet); 33 if (!referenceEntries) return undefined; 34 const map = arrayToMultiMap(referenceEntries.map(FindAllReferences.toHighlightSpan), e => e.fileName, e => e.span); 35 const getCanonicalFileName = createGetCanonicalFileName(program.useCaseSensitiveFileNames()); 36 return mapDefined(arrayFrom(map.entries()), ([fileName, highlightSpans]) => { 37 if (!sourceFilesSet.has(fileName)) { 38 if (!program.redirectTargetsMap.has(toPath(fileName, program.getCurrentDirectory(), getCanonicalFileName))) { 39 return undefined; 40 } 41 const redirectTarget = program.getSourceFile(fileName); 42 const redirect = find(sourceFilesToSearch, f => !!f.redirectInfo && f.redirectInfo.redirectTarget === redirectTarget)!; 43 fileName = redirect.fileName; 44 Debug.assert(sourceFilesSet.has(fileName)); 45 } 46 return { fileName, highlightSpans }; 47 }); 48 } 49 50 function getSyntacticDocumentHighlights(node: Node, sourceFile: SourceFile): DocumentHighlights[] | undefined { 51 const highlightSpans = getHighlightSpans(node, sourceFile); 52 return highlightSpans && [{ fileName: sourceFile.fileName, highlightSpans }]; 53 } 54 55 function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] | undefined { 56 switch (node.kind) { 57 case SyntaxKind.IfKeyword: 58 case SyntaxKind.ElseKeyword: 59 return isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; 60 case SyntaxKind.ReturnKeyword: 61 return useParent(node.parent, isReturnStatement, getReturnOccurrences); 62 case SyntaxKind.ThrowKeyword: 63 return useParent(node.parent, isThrowStatement, getThrowOccurrences); 64 case SyntaxKind.TryKeyword: 65 case SyntaxKind.CatchKeyword: 66 case SyntaxKind.FinallyKeyword: 67 const tryStatement = node.kind === SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; 68 return useParent(tryStatement, isTryStatement, getTryCatchFinallyOccurrences); 69 case SyntaxKind.SwitchKeyword: 70 return useParent(node.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); 71 case SyntaxKind.CaseKeyword: 72 case SyntaxKind.DefaultKeyword: { 73 if (isDefaultClause(node.parent) || isCaseClause(node.parent)) { 74 return useParent(node.parent.parent.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); 75 } 76 return undefined; 77 } 78 case SyntaxKind.BreakKeyword: 79 case SyntaxKind.ContinueKeyword: 80 return useParent(node.parent, isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); 81 case SyntaxKind.ForKeyword: 82 case SyntaxKind.WhileKeyword: 83 case SyntaxKind.DoKeyword: 84 return useParent(node.parent, (n): n is IterationStatement => isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); 85 case SyntaxKind.ConstructorKeyword: 86 return getFromAllDeclarations(isConstructorDeclaration, [SyntaxKind.ConstructorKeyword]); 87 case SyntaxKind.GetKeyword: 88 case SyntaxKind.SetKeyword: 89 return getFromAllDeclarations(isAccessor, [SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]); 90 case SyntaxKind.AwaitKeyword: 91 return useParent(node.parent, isAwaitExpression, getAsyncAndAwaitOccurrences); 92 case SyntaxKind.AsyncKeyword: 93 return highlightSpans(getAsyncAndAwaitOccurrences(node)); 94 case SyntaxKind.YieldKeyword: 95 return highlightSpans(getYieldOccurrences(node)); 96 case SyntaxKind.InKeyword: 97 return undefined; 98 default: 99 return isModifierKind(node.kind) && (isDeclaration(node.parent) || isVariableStatement(node.parent)) 100 ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) 101 : undefined; 102 } 103 104 function getFromAllDeclarations<T extends Node>(nodeTest: (node: Node) => node is T, keywords: readonly SyntaxKind[]): HighlightSpan[] | undefined { 105 return useParent(node.parent, nodeTest, decl => mapDefined(decl.symbol.declarations, d => 106 nodeTest(d) ? find(d.getChildren(sourceFile), c => contains(keywords, c.kind)) : undefined)); 107 } 108 109 function useParent<T extends Node>(node: Node, nodeTest: (node: Node) => node is T, getNodes: (node: T, sourceFile: SourceFile) => readonly Node[] | undefined): HighlightSpan[] | undefined { 110 return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; 111 } 112 113 function highlightSpans(nodes: readonly Node[] | undefined): HighlightSpan[] | undefined { 114 return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); 115 } 116 } 117 118 /** 119 * Aggregates all throw-statements within this node *without* crossing 120 * into function boundaries and try-blocks with catch-clauses. 121 */ 122 function aggregateOwnedThrowStatements(node: Node): readonly ThrowStatement[] | undefined { 123 if (isThrowStatement(node)) { 124 return [node]; 125 } 126 else if (isTryStatement(node)) { 127 // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. 128 return concatenate( 129 node.catchClause ? aggregateOwnedThrowStatements(node.catchClause) : node.tryBlock && aggregateOwnedThrowStatements(node.tryBlock), 130 node.finallyBlock && aggregateOwnedThrowStatements(node.finallyBlock)); 131 } 132 // Do not cross function boundaries. 133 return isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateOwnedThrowStatements); 134 } 135 136 /** 137 * For lack of a better name, this function takes a throw statement and returns the 138 * nearest ancestor that is a try-block (whose try statement has a catch clause), 139 * function-block, or source file. 140 */ 141 function getThrowStatementOwner(throwStatement: ThrowStatement): Node | undefined { 142 let child: Node = throwStatement; 143 144 while (child.parent) { 145 const parent = child.parent; 146 147 if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { 148 return parent; 149 } 150 151 // A throw-statement is only owned by a try-statement if the try-statement has 152 // a catch clause, and if the throw-statement occurs within the try block. 153 if (isTryStatement(parent) && parent.tryBlock === child && parent.catchClause) { 154 return child; 155 } 156 157 child = parent; 158 } 159 160 return undefined; 161 } 162 163 function aggregateAllBreakAndContinueStatements(node: Node): readonly BreakOrContinueStatement[] | undefined { 164 return isBreakOrContinueStatement(node) ? [node] : isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateAllBreakAndContinueStatements); 165 } 166 167 function flatMapChildren<T>(node: Node, cb: (child: Node) => readonly T[] | T | undefined): readonly T[] { 168 const result: T[] = []; 169 node.forEachChild(child => { 170 const value = cb(child); 171 if (value !== undefined) { 172 result.push(...toArray(value)); 173 } 174 }); 175 return result; 176 } 177 178 function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { 179 const actualOwner = getBreakOrContinueOwner(statement); 180 return !!actualOwner && actualOwner === owner; 181 } 182 183 function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node | undefined { 184 return findAncestor(statement, node => { 185 switch (node.kind) { 186 case SyntaxKind.SwitchStatement: 187 if (statement.kind === SyntaxKind.ContinueStatement) { 188 return false; 189 } 190 // falls through 191 192 case SyntaxKind.ForStatement: 193 case SyntaxKind.ForInStatement: 194 case SyntaxKind.ForOfStatement: 195 case SyntaxKind.WhileStatement: 196 case SyntaxKind.DoStatement: 197 return !statement.label || isLabeledBy(node, statement.label.escapedText); 198 default: 199 // Don't cross function boundaries. 200 // TODO: GH#20090 201 return isFunctionLike(node) && "quit"; 202 } 203 }); 204 } 205 206 function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] { 207 return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier)); 208 } 209 210 function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): readonly Node[] | undefined { 211 // Types of node whose children might have modifiers. 212 const container = declaration.parent as ModuleBlock | SourceFile | Block | CaseClause | DefaultClause | ConstructorDeclaration | MethodDeclaration | FunctionDeclaration | ObjectTypeDeclaration | ObjectLiteralExpression; 213 switch (container.kind) { 214 case SyntaxKind.ModuleBlock: 215 case SyntaxKind.SourceFile: 216 case SyntaxKind.Block: 217 case SyntaxKind.CaseClause: 218 case SyntaxKind.DefaultClause: 219 // Container is either a class declaration or the declaration is a classDeclaration 220 if (modifierFlag & ModifierFlags.Abstract && isClassDeclaration(declaration)) { 221 return [...declaration.members, declaration]; 222 } 223 else { 224 return container.statements; 225 } 226 case SyntaxKind.Constructor: 227 case SyntaxKind.MethodDeclaration: 228 case SyntaxKind.FunctionDeclaration: 229 return [...container.parameters, ...(isClassLike(container.parent) ? container.parent.members : [])]; 230 case SyntaxKind.ClassDeclaration: 231 case SyntaxKind.ClassExpression: 232 case SyntaxKind.StructDeclaration: 233 case SyntaxKind.InterfaceDeclaration: 234 case SyntaxKind.TypeLiteral: 235 const nodes = container.members; 236 237 // If we're an accessibility modifier, we're in an instance member and should search 238 // the constructor's parameter list for instance members as well. 239 if (modifierFlag & (ModifierFlags.AccessibilityModifier | ModifierFlags.Readonly)) { 240 const constructor = find(container.members, isConstructorDeclaration); 241 if (constructor) { 242 return [...nodes, ...constructor.parameters]; 243 } 244 } 245 else if (modifierFlag & ModifierFlags.Abstract) { 246 return [...nodes, container]; 247 } 248 return nodes; 249 250 // Syntactically invalid positions that the parser might produce anyway 251 case SyntaxKind.ObjectLiteralExpression: 252 return undefined; 253 254 default: 255 Debug.assertNever(container, "Invalid container kind."); 256 } 257 } 258 259 function pushKeywordIf(keywordList: Push<Node>, token: Node | undefined, ...expected: SyntaxKind[]): boolean { 260 if (token && contains(expected, token.kind)) { 261 keywordList.push(token); 262 return true; 263 } 264 265 return false; 266 } 267 268 function getLoopBreakContinueOccurrences(loopNode: IterationStatement): Node[] { 269 const keywords: Node[] = []; 270 271 if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { 272 // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. 273 if (loopNode.kind === SyntaxKind.DoStatement) { 274 const loopTokens = loopNode.getChildren(); 275 276 for (let i = loopTokens.length - 1; i >= 0; i--) { 277 if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { 278 break; 279 } 280 } 281 } 282 } 283 284 forEach(aggregateAllBreakAndContinueStatements(loopNode.statement), statement => { 285 if (ownsBreakOrContinueStatement(loopNode, statement)) { 286 pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); 287 } 288 }); 289 290 return keywords; 291 } 292 293 function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): Node[] | undefined { 294 const owner = getBreakOrContinueOwner(breakOrContinueStatement); 295 296 if (owner) { 297 switch (owner.kind) { 298 case SyntaxKind.ForStatement: 299 case SyntaxKind.ForInStatement: 300 case SyntaxKind.ForOfStatement: 301 case SyntaxKind.DoStatement: 302 case SyntaxKind.WhileStatement: 303 return getLoopBreakContinueOccurrences(owner as IterationStatement); 304 case SyntaxKind.SwitchStatement: 305 return getSwitchCaseDefaultOccurrences(owner as SwitchStatement); 306 307 } 308 } 309 310 return undefined; 311 } 312 313 function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): Node[] { 314 const keywords: Node[] = []; 315 316 pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); 317 318 // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. 319 forEach(switchStatement.caseBlock.clauses, clause => { 320 pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); 321 322 forEach(aggregateAllBreakAndContinueStatements(clause), statement => { 323 if (ownsBreakOrContinueStatement(switchStatement, statement)) { 324 pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); 325 } 326 }); 327 }); 328 329 return keywords; 330 } 331 332 function getTryCatchFinallyOccurrences(tryStatement: TryStatement, sourceFile: SourceFile): Node[] { 333 const keywords: Node[] = []; 334 335 pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); 336 337 if (tryStatement.catchClause) { 338 pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); 339 } 340 341 if (tryStatement.finallyBlock) { 342 const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile)!; 343 pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); 344 } 345 346 return keywords; 347 } 348 349 function getThrowOccurrences(throwStatement: ThrowStatement, sourceFile: SourceFile): Node[] | undefined { 350 const owner = getThrowStatementOwner(throwStatement); 351 352 if (!owner) { 353 return undefined; 354 } 355 356 const keywords: Node[] = []; 357 358 forEach(aggregateOwnedThrowStatements(owner), throwStatement => { 359 keywords.push(findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!); 360 }); 361 362 // If the "owner" is a function, then we equate 'return' and 'throw' statements in their 363 // ability to "jump out" of the function, and include occurrences for both. 364 if (isFunctionBlock(owner)) { 365 forEachReturnStatement(owner as Block, returnStatement => { 366 keywords.push(findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!); 367 }); 368 } 369 370 return keywords; 371 } 372 373 function getReturnOccurrences(returnStatement: ReturnStatement, sourceFile: SourceFile): Node[] | undefined { 374 const func = getContainingFunction(returnStatement) as FunctionLikeDeclaration; 375 if (!func) { 376 return undefined; 377 } 378 379 const keywords: Node[] = []; 380 forEachReturnStatement(cast(func.body, isBlock), returnStatement => { 381 keywords.push(findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!); 382 }); 383 384 // Include 'throw' statements that do not occur within a try block. 385 forEach(aggregateOwnedThrowStatements(func.body!), throwStatement => { 386 keywords.push(findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!); 387 }); 388 389 return keywords; 390 } 391 392 function getAsyncAndAwaitOccurrences(node: Node): Node[] | undefined { 393 const func = getContainingFunction(node) as FunctionLikeDeclaration; 394 if (!func) { 395 return undefined; 396 } 397 398 const keywords: Node[] = []; 399 400 if (func.modifiers) { 401 func.modifiers.forEach(modifier => { 402 pushKeywordIf(keywords, modifier, SyntaxKind.AsyncKeyword); 403 }); 404 } 405 406 forEachChild(func, child => { 407 traverseWithoutCrossingFunction(child, node => { 408 if (isAwaitExpression(node)) { 409 pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.AwaitKeyword); 410 } 411 }); 412 }); 413 414 415 return keywords; 416 } 417 418 function getYieldOccurrences(node: Node): Node[] | undefined { 419 const func = getContainingFunction(node) as FunctionDeclaration; 420 if (!func) { 421 return undefined; 422 } 423 424 const keywords: Node[] = []; 425 426 forEachChild(func, child => { 427 traverseWithoutCrossingFunction(child, node => { 428 if (isYieldExpression(node)) { 429 pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.YieldKeyword); 430 } 431 }); 432 }); 433 434 return keywords; 435 } 436 437 // Do not cross function/class/interface/module/type boundaries. 438 function traverseWithoutCrossingFunction(node: Node, cb: (node: Node) => void) { 439 cb(node); 440 if (!isFunctionLike(node) && !isClassLike(node) && !isInterfaceDeclaration(node) && !isModuleDeclaration(node) && !isTypeAliasDeclaration(node) && !isTypeNode(node)) { 441 forEachChild(node, child => traverseWithoutCrossingFunction(child, cb)); 442 } 443 } 444 445 function getIfElseOccurrences(ifStatement: IfStatement, sourceFile: SourceFile): HighlightSpan[] { 446 const keywords = getIfElseKeywords(ifStatement, sourceFile); 447 const result: HighlightSpan[] = []; 448 449 // We'd like to highlight else/ifs together if they are only separated by whitespace 450 // (i.e. the keywords are separated by no comments, no newlines). 451 for (let i = 0; i < keywords.length; i++) { 452 if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { 453 const elseKeyword = keywords[i]; 454 const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. 455 456 let shouldCombineElseAndIf = true; 457 458 // Avoid recalculating getStart() by iterating backwards. 459 for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { 460 if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { 461 shouldCombineElseAndIf = false; 462 break; 463 } 464 } 465 466 if (shouldCombineElseAndIf) { 467 result.push({ 468 fileName: sourceFile.fileName, 469 textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), 470 kind: HighlightSpanKind.reference 471 }); 472 i++; // skip the next keyword 473 continue; 474 } 475 } 476 477 // Ordinary case: just highlight the keyword. 478 result.push(getHighlightSpanForNode(keywords[i], sourceFile)); 479 } 480 481 return result; 482 } 483 484 function getIfElseKeywords(ifStatement: IfStatement, sourceFile: SourceFile): Node[] { 485 const keywords: Node[] = []; 486 487 // Traverse upwards through all parent if-statements linked by their else-branches. 488 while (isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { 489 ifStatement = ifStatement.parent; 490 } 491 492 // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. 493 while (true) { 494 const children = ifStatement.getChildren(sourceFile); 495 pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); 496 497 // Generally the 'else' keyword is second-to-last, so we traverse backwards. 498 for (let i = children.length - 1; i >= 0; i--) { 499 if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { 500 break; 501 } 502 } 503 504 if (!ifStatement.elseStatement || !isIfStatement(ifStatement.elseStatement)) { 505 break; 506 } 507 508 ifStatement = ifStatement.elseStatement; 509 } 510 511 return keywords; 512 } 513 514 /** 515 * Whether or not a 'node' is preceded by a label of the given string. 516 * Note: 'node' cannot be a SourceFile. 517 */ 518 function isLabeledBy(node: Node, labelName: __String): boolean { 519 return !!findAncestor(node.parent, owner => !isLabeledStatement(owner) ? "quit" : owner.label.escapedText === labelName); 520 } 521 } 522} 523