/* @internal */ namespace ts.DocumentHighlights { export function getDocumentHighlights(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: SourceFile[]): DocumentHighlights[] { const node = getTouchingWord(sourceFile, position); if (!node) { return undefined; } return getSemanticDocumentHighlights(node) || getSyntacticDocumentHighlights(node); function getHighlightSpanForNode(node: Node): HighlightSpan { const start = node.getStart(); const end = node.getEnd(); return { fileName: sourceFile.fileName, textSpan: createTextSpanFromBounds(start, end), kind: HighlightSpanKind.none }; } function getSemanticDocumentHighlights(node: Node): DocumentHighlights[] { if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.ThisKeyword || node.kind === SyntaxKind.ThisType || node.kind === SyntaxKind.SuperKeyword || node.kind === SyntaxKind.StringLiteral || isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { const referencedSymbols = FindAllReferences.getReferencedSymbolsForNode(typeChecker, cancellationToken, node, sourceFilesToSearch, /*findInStrings*/ false, /*findInComments*/ false, /*implementations*/false); return convertReferencedSymbols(referencedSymbols); } return undefined; function convertReferencedSymbols(referencedSymbols: ReferencedSymbol[]): DocumentHighlights[] { if (!referencedSymbols) { return undefined; } const fileNameToDocumentHighlights = createMap(); const result: DocumentHighlights[] = []; for (const referencedSymbol of referencedSymbols) { for (const referenceEntry of referencedSymbol.references) { const fileName = referenceEntry.fileName; let documentHighlights = fileNameToDocumentHighlights[fileName]; if (!documentHighlights) { documentHighlights = { fileName, highlightSpans: [] }; fileNameToDocumentHighlights[fileName] = documentHighlights; result.push(documentHighlights); } documentHighlights.highlightSpans.push({ textSpan: referenceEntry.textSpan, kind: referenceEntry.isWriteAccess ? HighlightSpanKind.writtenReference : HighlightSpanKind.reference }); } } return result; } } function getSyntacticDocumentHighlights(node: Node): DocumentHighlights[] { const fileName = sourceFile.fileName; const highlightSpans = getHighlightSpans(node); if (!highlightSpans || highlightSpans.length === 0) { return undefined; } return [{ fileName, highlightSpans }]; // returns true if 'node' is defined and has a matching 'kind'. function hasKind(node: Node, kind: SyntaxKind) { return node !== undefined && node.kind === kind; } // Null-propagating 'parent' function. function parent(node: Node): Node { return node && node.parent; } function getHighlightSpans(node: Node): HighlightSpan[] { if (node) { switch (node.kind) { case SyntaxKind.IfKeyword: case SyntaxKind.ElseKeyword: if (hasKind(node.parent, SyntaxKind.IfStatement)) { return getIfElseOccurrences(node.parent); } break; case SyntaxKind.ReturnKeyword: if (hasKind(node.parent, SyntaxKind.ReturnStatement)) { return getReturnOccurrences(node.parent); } break; case SyntaxKind.ThrowKeyword: if (hasKind(node.parent, SyntaxKind.ThrowStatement)) { return getThrowOccurrences(node.parent); } break; case SyntaxKind.CatchKeyword: if (hasKind(parent(parent(node)), SyntaxKind.TryStatement)) { return getTryCatchFinallyOccurrences(node.parent.parent); } break; case SyntaxKind.TryKeyword: case SyntaxKind.FinallyKeyword: if (hasKind(parent(node), SyntaxKind.TryStatement)) { return getTryCatchFinallyOccurrences(node.parent); } break; case SyntaxKind.SwitchKeyword: if (hasKind(node.parent, SyntaxKind.SwitchStatement)) { return getSwitchCaseDefaultOccurrences(node.parent); } break; case SyntaxKind.CaseKeyword: case SyntaxKind.DefaultKeyword: if (hasKind(parent(parent(parent(node))), SyntaxKind.SwitchStatement)) { return getSwitchCaseDefaultOccurrences(node.parent.parent.parent); } break; case SyntaxKind.BreakKeyword: case SyntaxKind.ContinueKeyword: if (hasKind(node.parent, SyntaxKind.BreakStatement) || hasKind(node.parent, SyntaxKind.ContinueStatement)) { return getBreakOrContinueStatementOccurrences(node.parent); } break; case SyntaxKind.ForKeyword: if (hasKind(node.parent, SyntaxKind.ForStatement) || hasKind(node.parent, SyntaxKind.ForInStatement) || hasKind(node.parent, SyntaxKind.ForOfStatement)) { return getLoopBreakContinueOccurrences(node.parent); } break; case SyntaxKind.WhileKeyword: case SyntaxKind.DoKeyword: if (hasKind(node.parent, SyntaxKind.WhileStatement) || hasKind(node.parent, SyntaxKind.DoStatement)) { return getLoopBreakContinueOccurrences(node.parent); } break; case SyntaxKind.ConstructorKeyword: if (hasKind(node.parent, SyntaxKind.Constructor)) { return getConstructorOccurrences(node.parent); } break; case SyntaxKind.GetKeyword: case SyntaxKind.SetKeyword: if (hasKind(node.parent, SyntaxKind.GetAccessor) || hasKind(node.parent, SyntaxKind.SetAccessor)) { return getGetAndSetOccurrences(node.parent); } break; default: if (isModifierKind(node.kind) && node.parent && (isDeclaration(node.parent) || node.parent.kind === SyntaxKind.VariableStatement)) { return getModifierOccurrences(node.kind, node.parent); } } } return undefined; } /** * Aggregates all throw-statements within this node *without* crossing * into function boundaries and try-blocks with catch-clauses. */ function aggregateOwnedThrowStatements(node: Node): ThrowStatement[] { const statementAccumulator: ThrowStatement[] = []; aggregate(node); return statementAccumulator; function aggregate(node: Node): void { if (node.kind === SyntaxKind.ThrowStatement) { statementAccumulator.push(node); } else if (node.kind === SyntaxKind.TryStatement) { const tryStatement = node; if (tryStatement.catchClause) { aggregate(tryStatement.catchClause); } else { // Exceptions thrown within a try block lacking a catch clause // are "owned" in the current context. aggregate(tryStatement.tryBlock); } if (tryStatement.finallyBlock) { aggregate(tryStatement.finallyBlock); } } // Do not cross function boundaries. else if (!isFunctionLike(node)) { forEachChild(node, aggregate); } } } /** * For lack of a better name, this function takes a throw statement and returns the * nearest ancestor that is a try-block (whose try statement has a catch clause), * function-block, or source file. */ function getThrowStatementOwner(throwStatement: ThrowStatement): Node { let child: Node = throwStatement; while (child.parent) { const parent = child.parent; if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { return parent; } // A throw-statement is only owned by a try-statement if the try-statement has // a catch clause, and if the throw-statement occurs within the try block. if (parent.kind === SyntaxKind.TryStatement) { const tryStatement = parent; if (tryStatement.tryBlock === child && tryStatement.catchClause) { return child; } } child = parent; } return undefined; } function aggregateAllBreakAndContinueStatements(node: Node): BreakOrContinueStatement[] { const statementAccumulator: BreakOrContinueStatement[] = []; aggregate(node); return statementAccumulator; function aggregate(node: Node): void { if (node.kind === SyntaxKind.BreakStatement || node.kind === SyntaxKind.ContinueStatement) { statementAccumulator.push(node); } // Do not cross function boundaries. else if (!isFunctionLike(node)) { forEachChild(node, aggregate); } } } function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { const actualOwner = getBreakOrContinueOwner(statement); return actualOwner && actualOwner === owner; } function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node { for (let node = statement.parent; node; node = node.parent) { switch (node.kind) { case SyntaxKind.SwitchStatement: if (statement.kind === SyntaxKind.ContinueStatement) { continue; } // Fall through. case SyntaxKind.ForStatement: case SyntaxKind.ForInStatement: case SyntaxKind.ForOfStatement: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: if (!statement.label || isLabeledBy(node, statement.label.text)) { return node; } break; default: // Don't cross function boundaries. if (isFunctionLike(node)) { return undefined; } break; } } return undefined; } function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): HighlightSpan[] { const container = declaration.parent; // Make sure we only highlight the keyword when it makes sense to do so. if (isAccessibilityModifier(modifier)) { if (!(container.kind === SyntaxKind.ClassDeclaration || container.kind === SyntaxKind.ClassExpression || (declaration.kind === SyntaxKind.Parameter && hasKind(container, SyntaxKind.Constructor)))) { return undefined; } } else if (modifier === SyntaxKind.StaticKeyword) { if (!(container.kind === SyntaxKind.ClassDeclaration || container.kind === SyntaxKind.ClassExpression)) { return undefined; } } else if (modifier === SyntaxKind.ExportKeyword || modifier === SyntaxKind.DeclareKeyword) { if (!(container.kind === SyntaxKind.ModuleBlock || container.kind === SyntaxKind.SourceFile)) { return undefined; } } else if (modifier === SyntaxKind.AbstractKeyword) { if (!(container.kind === SyntaxKind.ClassDeclaration || declaration.kind === SyntaxKind.ClassDeclaration)) { return undefined; } } else { // unsupported modifier return undefined; } const keywords: Node[] = []; const modifierFlag: ModifierFlags = getFlagFromModifier(modifier); let nodes: Node[]; switch (container.kind) { case SyntaxKind.ModuleBlock: case SyntaxKind.SourceFile: // Container is either a class declaration or the declaration is a classDeclaration if (modifierFlag & ModifierFlags.Abstract) { nodes = ((declaration).members).concat(declaration); } else { nodes = (container).statements; } break; case SyntaxKind.Constructor: nodes = ((container).parameters).concat( (container.parent).members); break; case SyntaxKind.ClassDeclaration: case SyntaxKind.ClassExpression: nodes = (container).members; // If we're an accessibility modifier, we're in an instance member and should search // the constructor's parameter list for instance members as well. if (modifierFlag & ModifierFlags.AccessibilityModifier) { const constructor = forEach((container).members, member => { return member.kind === SyntaxKind.Constructor && member; }); if (constructor) { nodes = nodes.concat(constructor.parameters); } } else if (modifierFlag & ModifierFlags.Abstract) { nodes = nodes.concat(container); } break; default: Debug.fail("Invalid container kind."); } forEach(nodes, node => { if (getModifierFlags(node) & modifierFlag) { forEach(node.modifiers, child => pushKeywordIf(keywords, child, modifier)); } }); return map(keywords, getHighlightSpanForNode); function getFlagFromModifier(modifier: SyntaxKind) { switch (modifier) { case SyntaxKind.PublicKeyword: return ModifierFlags.Public; case SyntaxKind.PrivateKeyword: return ModifierFlags.Private; case SyntaxKind.ProtectedKeyword: return ModifierFlags.Protected; case SyntaxKind.StaticKeyword: return ModifierFlags.Static; case SyntaxKind.ExportKeyword: return ModifierFlags.Export; case SyntaxKind.DeclareKeyword: return ModifierFlags.Ambient; case SyntaxKind.AbstractKeyword: return ModifierFlags.Abstract; default: Debug.fail(); } } } function pushKeywordIf(keywordList: Node[], token: Node, ...expected: SyntaxKind[]): boolean { if (token && contains(expected, token.kind)) { keywordList.push(token); return true; } return false; } function getGetAndSetOccurrences(accessorDeclaration: AccessorDeclaration): HighlightSpan[] { const keywords: Node[] = []; tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.GetAccessor); tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.SetAccessor); return map(keywords, getHighlightSpanForNode); function tryPushAccessorKeyword(accessorSymbol: Symbol, accessorKind: SyntaxKind): void { const accessor = getDeclarationOfKind(accessorSymbol, accessorKind); if (accessor) { forEach(accessor.getChildren(), child => pushKeywordIf(keywords, child, SyntaxKind.GetKeyword, SyntaxKind.SetKeyword)); } } } function getConstructorOccurrences(constructorDeclaration: ConstructorDeclaration): HighlightSpan[] { const declarations = constructorDeclaration.symbol.getDeclarations(); const keywords: Node[] = []; forEach(declarations, declaration => { forEach(declaration.getChildren(), token => { return pushKeywordIf(keywords, token, SyntaxKind.ConstructorKeyword); }); }); return map(keywords, getHighlightSpanForNode); } function getLoopBreakContinueOccurrences(loopNode: IterationStatement): HighlightSpan[] { const keywords: Node[] = []; if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. if (loopNode.kind === SyntaxKind.DoStatement) { const loopTokens = loopNode.getChildren(); for (let i = loopTokens.length - 1; i >= 0; i--) { if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { break; } } } } const breaksAndContinues = aggregateAllBreakAndContinueStatements(loopNode.statement); forEach(breaksAndContinues, statement => { if (ownsBreakOrContinueStatement(loopNode, statement)) { pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); } }); return map(keywords, getHighlightSpanForNode); } function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): HighlightSpan[] { const owner = getBreakOrContinueOwner(breakOrContinueStatement); if (owner) { switch (owner.kind) { case SyntaxKind.ForStatement: case SyntaxKind.ForInStatement: case SyntaxKind.ForOfStatement: case SyntaxKind.DoStatement: case SyntaxKind.WhileStatement: return getLoopBreakContinueOccurrences(owner); case SyntaxKind.SwitchStatement: return getSwitchCaseDefaultOccurrences(owner); } } return undefined; } function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): HighlightSpan[] { const keywords: Node[] = []; pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. forEach(switchStatement.caseBlock.clauses, clause => { pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); const breaksAndContinues = aggregateAllBreakAndContinueStatements(clause); forEach(breaksAndContinues, statement => { if (ownsBreakOrContinueStatement(switchStatement, statement)) { pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); } }); }); return map(keywords, getHighlightSpanForNode); } function getTryCatchFinallyOccurrences(tryStatement: TryStatement): HighlightSpan[] { const keywords: Node[] = []; pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); if (tryStatement.catchClause) { pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); } if (tryStatement.finallyBlock) { const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); } return map(keywords, getHighlightSpanForNode); } function getThrowOccurrences(throwStatement: ThrowStatement): HighlightSpan[] { const owner = getThrowStatementOwner(throwStatement); if (!owner) { return undefined; } const keywords: Node[] = []; forEach(aggregateOwnedThrowStatements(owner), throwStatement => { pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); }); // If the "owner" is a function, then we equate 'return' and 'throw' statements in their // ability to "jump out" of the function, and include occurrences for both. if (isFunctionBlock(owner)) { forEachReturnStatement(owner, returnStatement => { pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); }); } return map(keywords, getHighlightSpanForNode); } function getReturnOccurrences(returnStatement: ReturnStatement): HighlightSpan[] { const func = getContainingFunction(returnStatement); // If we didn't find a containing function with a block body, bail out. if (!(func && hasKind(func.body, SyntaxKind.Block))) { return undefined; } const keywords: Node[] = []; forEachReturnStatement(func.body, returnStatement => { pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); }); // Include 'throw' statements that do not occur within a try block. forEach(aggregateOwnedThrowStatements(func.body), throwStatement => { pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); }); return map(keywords, getHighlightSpanForNode); } function getIfElseOccurrences(ifStatement: IfStatement): HighlightSpan[] { const keywords: Node[] = []; // Traverse upwards through all parent if-statements linked by their else-branches. while (hasKind(ifStatement.parent, SyntaxKind.IfStatement) && (ifStatement.parent).elseStatement === ifStatement) { ifStatement = ifStatement.parent; } // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. while (ifStatement) { const children = ifStatement.getChildren(); pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); // Generally the 'else' keyword is second-to-last, so we traverse backwards. for (let i = children.length - 1; i >= 0; i--) { if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { break; } } if (!hasKind(ifStatement.elseStatement, SyntaxKind.IfStatement)) { break; } ifStatement = ifStatement.elseStatement; } const result: HighlightSpan[] = []; // We'd like to highlight else/ifs together if they are only separated by whitespace // (i.e. the keywords are separated by no comments, no newlines). for (let i = 0; i < keywords.length; i++) { if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { const elseKeyword = keywords[i]; const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. let shouldCombindElseAndIf = true; // Avoid recalculating getStart() by iterating backwards. for (let j = ifKeyword.getStart() - 1; j >= elseKeyword.end; j--) { if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { shouldCombindElseAndIf = false; break; } } if (shouldCombindElseAndIf) { result.push({ fileName: fileName, textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), kind: HighlightSpanKind.reference }); i++; // skip the next keyword continue; } } // Ordinary case: just highlight the keyword. result.push(getHighlightSpanForNode(keywords[i])); } return result; } } } /** * Whether or not a 'node' is preceded by a label of the given string. * Note: 'node' cannot be a SourceFile. */ function isLabeledBy(node: Node, labelName: string) { for (let owner = node.parent; owner.kind === SyntaxKind.LabeledStatement; owner = owner.parent) { if ((owner).label.text === labelName) { return true; } } return false; } }