• 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            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