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