• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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