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