• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.NavigationBar {
3    /**
4     * Matches all whitespace characters in a string. Eg:
5     *
6     * "app.
7     *
8     * onactivated"
9     *
10     * matches because of the newline, whereas
11     *
12     * "app.onactivated"
13     *
14     * does not match.
15     */
16    const whiteSpaceRegex = /\s+/g;
17
18    /**
19     * Maximum amount of characters to return
20     * The amount was chosen arbitrarily.
21     */
22    const maxLength = 150;
23
24    // Keep sourceFile handy so we don't have to search for it every time we need to call `getText`.
25    let curCancellationToken: CancellationToken;
26    let curSourceFile: SourceFile;
27
28    /**
29     * For performance, we keep navigation bar parents on a stack rather than passing them through each recursion.
30     * `parent` is the current parent and is *not* stored in parentsStack.
31     * `startNode` sets a new parent and `endNode` returns to the previous parent.
32     */
33    let parentsStack: NavigationBarNode[] = [];
34    let parent: NavigationBarNode;
35
36    const trackedEs5ClassesStack: (ESMap<string, boolean> | undefined)[] = [];
37    let trackedEs5Classes: ESMap<string, boolean> | undefined;
38
39    // NavigationBarItem requires an array, but will not mutate it, so just give it this for performance.
40    let emptyChildItemArray: NavigationBarItem[] = [];
41
42    /**
43     * Represents a navigation bar item and its children.
44     * The returned NavigationBarItem is more complicated and doesn't include 'parent', so we use these to do work before converting.
45     */
46    interface NavigationBarNode {
47        node: Node;
48        name: DeclarationName | undefined;
49        additionalNodes: Node[] | undefined;
50        parent: NavigationBarNode | undefined; // Present for all but root node
51        children: NavigationBarNode[] | undefined;
52        indent: number; // # of parents
53    }
54
55    export function getNavigationBarItems(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationBarItem[] {
56        curCancellationToken = cancellationToken;
57        curSourceFile = sourceFile;
58        try {
59            return map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem);
60        }
61        finally {
62            reset();
63        }
64    }
65
66    export function getNavigationTree(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationTree {
67        curCancellationToken = cancellationToken;
68        curSourceFile = sourceFile;
69        try {
70            return convertToTree(rootNavigationBarNode(sourceFile));
71        }
72        finally {
73            reset();
74        }
75    }
76
77    function reset() {
78        curSourceFile = undefined!;
79        curCancellationToken = undefined!;
80        parentsStack = [];
81        parent = undefined!;
82        emptyChildItemArray = [];
83    }
84
85    function nodeText(node: Node): string {
86        return cleanText(node.getText(curSourceFile));
87    }
88
89    function navigationBarNodeKind(n: NavigationBarNode): SyntaxKind {
90        return n.node.kind;
91    }
92
93    function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void {
94        if (parent.children) {
95            parent.children.push(child);
96        }
97        else {
98            parent.children = [child];
99        }
100    }
101
102    function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode {
103        Debug.assert(!parentsStack.length);
104        const root: NavigationBarNode = { node: sourceFile, name: undefined, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 };
105        parent = root;
106        for (const statement of sourceFile.statements) {
107            addChildrenRecursively(statement);
108        }
109        endNode();
110        Debug.assert(!parent && !parentsStack.length);
111        return root;
112    }
113
114    function addLeafNode(node: Node, name?: DeclarationName): void {
115        pushChild(parent, emptyNavigationBarNode(node, name));
116    }
117
118    function emptyNavigationBarNode(node: Node, name?: DeclarationName): NavigationBarNode {
119        return {
120            node,
121            name: name || (isDeclaration(node) || isExpression(node) ? getNameOfDeclaration(node) : undefined),
122            additionalNodes: undefined,
123            parent,
124            children: undefined,
125            indent: parent.indent + 1
126        };
127    }
128
129    function addTrackedEs5Class(name: string) {
130        if (!trackedEs5Classes) {
131            trackedEs5Classes = new Map();
132        }
133        trackedEs5Classes.set(name, true);
134    }
135    function endNestedNodes(depth: number): void {
136        for (let i = 0; i < depth; i++) endNode();
137    }
138    function startNestedNodes(targetNode: Node, entityName: BindableStaticNameExpression) {
139        const names: (PropertyNameLiteral | WellKnownSymbolExpression)[] = [];
140        while (!isPropertyNameLiteral(entityName)) {
141            const name = getNameOrArgument(entityName);
142            const nameText = getElementOrPropertyAccessName(entityName);
143            entityName = entityName.expression;
144            if (nameText === "prototype" || isPrivateIdentifier(name)) continue;
145            names.push(name);
146        }
147        names.push(entityName);
148        for (let i = names.length - 1; i > 0; i--) {
149            const name = names[i];
150            startNode(targetNode, name);
151        }
152        return [names.length - 1, names[0]] as const;
153    }
154
155    /**
156     * Add a new level of NavigationBarNodes.
157     * This pushes to the stack, so you must call `endNode` when you are done adding to this node.
158     */
159    function startNode(node: Node, name?: DeclarationName): void {
160        const navNode: NavigationBarNode = emptyNavigationBarNode(node, name);
161        pushChild(parent, navNode);
162
163        // Save the old parent
164        parentsStack.push(parent);
165        trackedEs5ClassesStack.push(trackedEs5Classes);
166        trackedEs5Classes = undefined;
167        parent = navNode;
168    }
169
170    /** Call after calling `startNode` and adding children to it. */
171    function endNode(): void {
172        if (parent.children) {
173            mergeChildren(parent.children, parent);
174            sortChildren(parent.children);
175        }
176        parent = parentsStack.pop()!;
177        trackedEs5Classes = trackedEs5ClassesStack.pop();
178    }
179
180    function addNodeWithRecursiveChild(node: Node, child: Node | undefined, name?: DeclarationName): void {
181        startNode(node, name);
182        addChildrenRecursively(child);
183        endNode();
184    }
185
186    function addNodeWithRecursiveInitializer(node: VariableDeclaration | PropertyAssignment | BindingElement | PropertyDeclaration): void {
187        if (node.initializer && isFunctionOrClassExpression(node.initializer)) {
188            startNode(node);
189            forEachChild(node.initializer, addChildrenRecursively);
190            endNode();
191        }
192        else {
193            addNodeWithRecursiveChild(node, node.initializer);
194        }
195    }
196
197    /** Look for navigation bar items in node's subtree, adding them to the current `parent`. */
198    function addChildrenRecursively(node: Node | undefined): void {
199        curCancellationToken.throwIfCancellationRequested();
200
201        if (!node || isToken(node)) {
202            return;
203        }
204
205        switch (node.kind) {
206            case SyntaxKind.Constructor:
207                // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it.
208                const ctr = <ConstructorDeclaration>node;
209                addNodeWithRecursiveChild(ctr, ctr.body);
210
211                // Parameter properties are children of the class, not the constructor.
212                for (const param of ctr.parameters) {
213                    if (isParameterPropertyDeclaration(param, ctr)) {
214                        addLeafNode(param);
215                    }
216                }
217                break;
218
219            case SyntaxKind.MethodDeclaration:
220            case SyntaxKind.GetAccessor:
221            case SyntaxKind.SetAccessor:
222            case SyntaxKind.MethodSignature:
223                if (!hasDynamicName((<ClassElement | TypeElement>node))) {
224                    addNodeWithRecursiveChild(node, (<FunctionLikeDeclaration>node).body);
225                }
226                break;
227
228            case SyntaxKind.PropertyDeclaration:
229                if (!hasDynamicName(<ClassElement>node)) {
230                    addNodeWithRecursiveInitializer(<PropertyDeclaration>node);
231                }
232                break;
233            case SyntaxKind.PropertySignature:
234                if (!hasDynamicName(<TypeElement>node)) {
235                    addLeafNode(node);
236                }
237                break;
238
239            case SyntaxKind.ImportClause:
240                const importClause = <ImportClause>node;
241                // Handle default import case e.g.:
242                //    import d from "mod";
243                if (importClause.name) {
244                    addLeafNode(importClause.name);
245                }
246
247                // Handle named bindings in imports e.g.:
248                //    import * as NS from "mod";
249                //    import {a, b as B} from "mod";
250                const { namedBindings } = importClause;
251                if (namedBindings) {
252                    if (namedBindings.kind === SyntaxKind.NamespaceImport) {
253                        addLeafNode(namedBindings);
254                    }
255                    else {
256                        for (const element of namedBindings.elements) {
257                            addLeafNode(element);
258                        }
259                    }
260                }
261                break;
262
263            case SyntaxKind.ShorthandPropertyAssignment:
264                addNodeWithRecursiveChild(node, (<ShorthandPropertyAssignment>node).name);
265                break;
266            case SyntaxKind.SpreadAssignment:
267                const { expression } = <SpreadAssignment>node;
268                // Use the expression as the name of the SpreadAssignment, otherwise show as <unknown>.
269                isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node);
270                break;
271            case SyntaxKind.BindingElement:
272            case SyntaxKind.PropertyAssignment:
273            case SyntaxKind.VariableDeclaration: {
274                const child = <VariableDeclaration | PropertyAssignment | BindingElement>node;
275                if (isBindingPattern(child.name)) {
276                    addChildrenRecursively(child.name);
277                }
278                else {
279                    addNodeWithRecursiveInitializer(child);
280                }
281                break;
282            }
283            case SyntaxKind.FunctionDeclaration:
284                const nameNode = (<FunctionLikeDeclaration>node).name;
285                // If we see a function declaration track as a possible ES5 class
286                if (nameNode && isIdentifier(nameNode)) {
287                    addTrackedEs5Class(nameNode.text);
288                }
289                addNodeWithRecursiveChild(node, (<FunctionLikeDeclaration>node).body);
290                break;
291            case SyntaxKind.ArrowFunction:
292            case SyntaxKind.FunctionExpression:
293                addNodeWithRecursiveChild(node, (<FunctionLikeDeclaration>node).body);
294                break;
295
296            case SyntaxKind.EnumDeclaration:
297                startNode(node);
298                for (const member of (<EnumDeclaration>node).members) {
299                    if (!isComputedProperty(member)) {
300                        addLeafNode(member);
301                    }
302                }
303                endNode();
304                break;
305
306            case SyntaxKind.ClassDeclaration:
307            case SyntaxKind.ClassExpression:
308            case SyntaxKind.StructDeclaration:
309            case SyntaxKind.InterfaceDeclaration:
310                startNode(node);
311                for (const member of (<InterfaceDeclaration>node).members) {
312                    addChildrenRecursively(member);
313                }
314                endNode();
315                break;
316
317            case SyntaxKind.ModuleDeclaration:
318                addNodeWithRecursiveChild(node, getInteriorModule(<ModuleDeclaration>node).body);
319                break;
320
321            case SyntaxKind.ExportAssignment: {
322                const expression = (<ExportAssignment>node).expression;
323                const child = isObjectLiteralExpression(expression) || isCallExpression(expression) ? expression :
324                    isArrowFunction(expression) || isFunctionExpression(expression) ? expression.body : undefined;
325                if (child) {
326                    startNode(node);
327                    addChildrenRecursively(child);
328                    endNode();
329                }
330                else {
331                    addLeafNode(node);
332                }
333                break;
334            }
335            case SyntaxKind.ExportSpecifier:
336            case SyntaxKind.ImportEqualsDeclaration:
337            case SyntaxKind.IndexSignature:
338            case SyntaxKind.CallSignature:
339            case SyntaxKind.ConstructSignature:
340            case SyntaxKind.TypeAliasDeclaration:
341                addLeafNode(node);
342                break;
343
344            case SyntaxKind.CallExpression:
345            case SyntaxKind.BinaryExpression: {
346                const special = getAssignmentDeclarationKind(node as BinaryExpression);
347                switch (special) {
348                    case AssignmentDeclarationKind.ExportsProperty:
349                    case AssignmentDeclarationKind.ModuleExports:
350                        addNodeWithRecursiveChild(node, (node as BinaryExpression).right);
351                        return;
352                    case AssignmentDeclarationKind.Prototype:
353                    case AssignmentDeclarationKind.PrototypeProperty: {
354                        const binaryExpression = (node as BinaryExpression);
355                        const assignmentTarget = binaryExpression.left as PropertyAccessExpression;
356
357                        const prototypeAccess = special === AssignmentDeclarationKind.PrototypeProperty ?
358                            assignmentTarget.expression as PropertyAccessExpression :
359                            assignmentTarget;
360
361                        let depth = 0;
362                        let className: PropertyNameLiteral | WellKnownSymbolExpression;
363                        // If we see a prototype assignment, start tracking the target as a class
364                        // This is only done for simple classes not nested assignments.
365                        if (isIdentifier(prototypeAccess.expression)) {
366                            addTrackedEs5Class(prototypeAccess.expression.text);
367                            className = prototypeAccess.expression;
368                        }
369                        else {
370                            [depth, className] = startNestedNodes(binaryExpression, prototypeAccess.expression as EntityNameExpression);
371                        }
372                        if (special === AssignmentDeclarationKind.Prototype) {
373                            if (isObjectLiteralExpression(binaryExpression.right)) {
374                                if (binaryExpression.right.properties.length > 0) {
375                                    startNode(binaryExpression, className);
376                                        forEachChild(binaryExpression.right, addChildrenRecursively);
377                                    endNode();
378                                }
379                            }
380                        }
381                        else if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) {
382                            addNodeWithRecursiveChild(node,
383                                binaryExpression.right,
384                                className);
385                        }
386                        else {
387                            startNode(binaryExpression, className);
388                                addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name);
389                            endNode();
390                        }
391                        endNestedNodes(depth);
392                        return;
393                    }
394                    case AssignmentDeclarationKind.ObjectDefinePropertyValue:
395                    case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: {
396                        const defineCall = node as BindableObjectDefinePropertyCall;
397                        const className = special === AssignmentDeclarationKind.ObjectDefinePropertyValue ?
398                            defineCall.arguments[0] :
399                            (defineCall.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression;
400
401                        const memberName = defineCall.arguments[1];
402                        const [depth, classNameIdentifier] = startNestedNodes(node, className);
403                            startNode(node, classNameIdentifier);
404                                startNode(node, setTextRange(factory.createIdentifier(memberName.text), memberName));
405                                    addChildrenRecursively((node as CallExpression).arguments[2]);
406                                endNode();
407                            endNode();
408                        endNestedNodes(depth);
409                        return;
410                    }
411                    case AssignmentDeclarationKind.Property: {
412                        const binaryExpression = (node as BinaryExpression);
413                        const assignmentTarget = binaryExpression.left as PropertyAccessExpression | BindableElementAccessExpression;
414                        const targetFunction = assignmentTarget.expression;
415                        if (isIdentifier(targetFunction) && getElementOrPropertyAccessName(assignmentTarget) !== "prototype" &&
416                            trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) {
417                            if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) {
418                                addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction);
419                            }
420                            else if (isBindableStaticAccessExpression(assignmentTarget)) {
421                                startNode(binaryExpression, targetFunction);
422                                    addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, getNameOrArgument(assignmentTarget));
423                                endNode();
424                            }
425                            return;
426                        }
427                        break;
428                    }
429                    case AssignmentDeclarationKind.ThisProperty:
430                    case AssignmentDeclarationKind.None:
431                    case AssignmentDeclarationKind.ObjectDefinePropertyExports:
432                        break;
433                    default:
434                        Debug.assertNever(special);
435                }
436            }
437            // falls through
438
439            default:
440                if (hasJSDocNodes(node)) {
441                    forEach(node.jsDoc, jsDoc => {
442                        forEach(jsDoc.tags, tag => {
443                            if (isJSDocTypeAlias(tag)) {
444                                addLeafNode(tag);
445                            }
446                        });
447                    });
448                }
449
450                forEachChild(node, addChildrenRecursively);
451        }
452    }
453
454    /** Merge declarations of the same kind. */
455    function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void {
456        const nameToItems = new Map<string, NavigationBarNode | NavigationBarNode[]>();
457        filterMutate(children, (child, index) => {
458            const declName = child.name || getNameOfDeclaration(<Declaration>child.node);
459            const name = declName && nodeText(declName);
460            if (!name) {
461                // Anonymous items are never merged.
462                return true;
463            }
464
465            const itemsWithSameName = nameToItems.get(name);
466            if (!itemsWithSameName) {
467                nameToItems.set(name, child);
468                return true;
469            }
470
471            if (itemsWithSameName instanceof Array) {
472                for (const itemWithSameName of itemsWithSameName) {
473                    if (tryMerge(itemWithSameName, child, index, node)) {
474                        return false;
475                    }
476                }
477                itemsWithSameName.push(child);
478                return true;
479            }
480            else {
481                const itemWithSameName = itemsWithSameName;
482                if (tryMerge(itemWithSameName, child, index, node)) {
483                    return false;
484                }
485                nameToItems.set(name, [itemWithSameName, child]);
486                return true;
487            }
488        });
489    }
490    const isEs5ClassMember: Record<AssignmentDeclarationKind, boolean> = {
491        [AssignmentDeclarationKind.Property]: true,
492        [AssignmentDeclarationKind.PrototypeProperty]: true,
493        [AssignmentDeclarationKind.ObjectDefinePropertyValue]: true,
494        [AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true,
495        [AssignmentDeclarationKind.None]: false,
496        [AssignmentDeclarationKind.ExportsProperty]: false,
497        [AssignmentDeclarationKind.ModuleExports]: false,
498        [AssignmentDeclarationKind.ObjectDefinePropertyExports]: false,
499        [AssignmentDeclarationKind.Prototype]: true,
500        [AssignmentDeclarationKind.ThisProperty]: false,
501    };
502    function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined {
503        function isPossibleConstructor(node: Node) {
504            return isFunctionExpression(node) || isFunctionDeclaration(node) || isVariableDeclaration(node);
505        }
506        const bAssignmentDeclarationKind = isBinaryExpression(b.node) || isCallExpression(b.node) ?
507            getAssignmentDeclarationKind(b.node) :
508            AssignmentDeclarationKind.None;
509
510        const aAssignmentDeclarationKind = isBinaryExpression(a.node) || isCallExpression(a.node) ?
511            getAssignmentDeclarationKind(a.node) :
512            AssignmentDeclarationKind.None;
513
514        // We treat this as an es5 class and merge the nodes in in one of several cases
515        if ((isEs5ClassMember[bAssignmentDeclarationKind] && isEs5ClassMember[aAssignmentDeclarationKind]) // merge two class elements
516            || (isPossibleConstructor(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // ctor function & member
517            || (isPossibleConstructor(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & ctor function
518            || (isClassDeclaration(a.node) && isSynthesized(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member
519            || (isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated)
520            || (isClassDeclaration(a.node) && isSynthesized(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor
521            || (isClassDeclaration(b.node) && isPossibleConstructor(a.node) && isSynthesized(a.node)) // ctor & class (generated)
522            ) {
523
524            let lastANode = a.additionalNodes && lastOrUndefined(a.additionalNodes) || a.node;
525
526            if ((!isClassDeclaration(a.node) && !isClassDeclaration(b.node)) // If neither outline node is a class
527                || isPossibleConstructor(a.node) || isPossibleConstructor(b.node) // If either function is a constructor function
528                ) {
529                const ctorFunction = isPossibleConstructor(a.node) ? a.node :
530                    isPossibleConstructor(b.node) ? b.node :
531                    undefined;
532
533                if (ctorFunction !== undefined) {
534                    const ctorNode = setTextRange(
535                        factory.createConstructorDeclaration(/* decorators */ undefined, /* modifiers */ undefined, [], /* body */ undefined),
536                        ctorFunction);
537                    const ctor = emptyNavigationBarNode(ctorNode);
538                    ctor.indent = a.indent + 1;
539                    ctor.children = a.node === ctorFunction ? a.children : b.children;
540                    a.children = a.node === ctorFunction ? concatenate([ctor], b.children || [b]) : concatenate(a.children || [{ ...a }], [ctor]);
541                }
542                else {
543                    if (a.children || b.children) {
544                        a.children = concatenate(a.children || [{ ...a }], b.children || [b]);
545                        if (a.children) {
546                            mergeChildren(a.children, a);
547                            sortChildren(a.children);
548                        }
549                    }
550                }
551
552                lastANode = a.node = setTextRange(factory.createClassDeclaration(
553                    /* decorators */ undefined,
554                    /* modifiers */ undefined,
555                    a.name as Identifier || factory.createIdentifier("__class__"),
556                    /* typeParameters */ undefined,
557                    /* heritageClauses */ undefined,
558                    []
559                ), a.node);
560            }
561            else {
562                a.children = concatenate(a.children, b.children);
563                if (a.children) {
564                    mergeChildren(a.children, a);
565                }
566            }
567
568            const bNode = b.node;
569            // We merge if the outline node previous to b (bIndex - 1) is already part of the current class
570            // We do this so that statements between class members that do not generate outline nodes do not split up the class outline:
571            // Ex This should produce one outline node C:
572            //    function C() {}; a = 1; C.prototype.m = function () {}
573            // Ex This will produce 3 outline nodes: C, a, C
574            //    function C() {}; let a = 1; C.prototype.m = function () {}
575            if (parent.children![bIndex - 1].node.end === lastANode.end) {
576                setTextRange(lastANode, { pos: lastANode.pos, end: bNode.end });
577            }
578            else {
579                if (!a.additionalNodes) a.additionalNodes = [];
580                a.additionalNodes.push(setTextRange(factory.createClassDeclaration(
581                    /* decorators */ undefined,
582                    /* modifiers */ undefined,
583                    a.name as Identifier || factory.createIdentifier("__class__"),
584                    /* typeParameters */ undefined,
585                    /* heritageClauses */ undefined,
586                    []
587                ), b.node));
588            }
589            return true;
590        }
591        return bAssignmentDeclarationKind === AssignmentDeclarationKind.None ? false : true;
592    }
593
594    function tryMerge(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean {
595        // const v = false as boolean;
596        if (tryMergeEs5Class(a, b, bIndex, parent)) {
597            return true;
598        }
599        if (shouldReallyMerge(a.node, b.node, parent)) {
600            merge(a, b);
601            return true;
602        }
603        return false;
604    }
605
606    /** a and b have the same name, but they may not be mergeable. */
607    function shouldReallyMerge(a: Node, b: Node, parent: NavigationBarNode): boolean {
608        if (a.kind !== b.kind || a.parent !== b.parent && !(isOwnChild(a, parent) && isOwnChild(b, parent))) {
609            return false;
610        }
611        switch (a.kind) {
612            case SyntaxKind.PropertyDeclaration:
613            case SyntaxKind.MethodDeclaration:
614            case SyntaxKind.GetAccessor:
615            case SyntaxKind.SetAccessor:
616                return hasSyntacticModifier(a, ModifierFlags.Static) === hasSyntacticModifier(b, ModifierFlags.Static);
617            case SyntaxKind.ModuleDeclaration:
618                return areSameModule(<ModuleDeclaration>a, <ModuleDeclaration>b);
619            default:
620                return true;
621        }
622    }
623
624    function isSynthesized(node: Node) {
625        return !!(node.flags & NodeFlags.Synthesized);
626    }
627
628    // We want to merge own children like `I` in in `module A { interface I {} } module A { interface I {} }`
629    // We don't want to merge unrelated children like `m` in `const o = { a: { m() {} }, b: { m() {} } };`
630    function isOwnChild(n: Node, parent: NavigationBarNode): boolean {
631        const par = isModuleBlock(n.parent) ? n.parent.parent : n.parent;
632        return par === parent.node || contains(parent.additionalNodes, par);
633    }
634
635    // We use 1 NavNode to represent 'A.B.C', but there are multiple source nodes.
636    // Only merge module nodes that have the same chain. Don't merge 'A.B.C' with 'A'!
637    function areSameModule(a: ModuleDeclaration, b: ModuleDeclaration): boolean {
638        // TODO: GH#18217
639        return a.body!.kind === b.body!.kind && (a.body!.kind !== SyntaxKind.ModuleDeclaration || areSameModule(<ModuleDeclaration>a.body, <ModuleDeclaration>b.body));
640    }
641
642    /** Merge source into target. Source should be thrown away after this is called. */
643    function merge(target: NavigationBarNode, source: NavigationBarNode): void {
644        target.additionalNodes = target.additionalNodes || [];
645        target.additionalNodes.push(source.node);
646        if (source.additionalNodes) {
647            target.additionalNodes.push(...source.additionalNodes);
648        }
649
650        target.children = concatenate(target.children, source.children);
651        if (target.children) {
652            mergeChildren(target.children, target);
653            sortChildren(target.children);
654        }
655    }
656
657    /** Recursively ensure that each NavNode's children are in sorted order. */
658    function sortChildren(children: NavigationBarNode[]): void {
659        children.sort(compareChildren);
660    }
661
662    function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) {
663        return compareStringsCaseSensitiveUI(tryGetName(child1.node)!, tryGetName(child2.node)!) // TODO: GH#18217
664            || compareValues(navigationBarNodeKind(child1), navigationBarNodeKind(child2));
665    }
666
667    /**
668     * This differs from getItemName because this is just used for sorting.
669     * We only sort nodes by name that have a more-or-less "direct" name, as opposed to `new()` and the like.
670     * So `new()` can still come before an `aardvark` method.
671     */
672    function tryGetName(node: Node): string | undefined {
673        if (node.kind === SyntaxKind.ModuleDeclaration) {
674            return getModuleName(<ModuleDeclaration>node);
675        }
676
677        const declName = getNameOfDeclaration(<Declaration>node);
678        if (declName && isPropertyName(declName)) {
679            const propertyName = getPropertyNameForPropertyNameNode(declName);
680            return propertyName && unescapeLeadingUnderscores(propertyName);
681        }
682        switch (node.kind) {
683            case SyntaxKind.FunctionExpression:
684            case SyntaxKind.ArrowFunction:
685            case SyntaxKind.ClassExpression:
686                return getFunctionOrClassName(<FunctionExpression | ArrowFunction | ClassExpression>node);
687            default:
688                return undefined;
689        }
690    }
691
692    function getItemName(node: Node, name: Node | undefined): string {
693        if (node.kind === SyntaxKind.ModuleDeclaration) {
694            return cleanText(getModuleName(<ModuleDeclaration>node));
695        }
696
697        if (name) {
698            const text = isIdentifier(name) ? name.text
699                : isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]`
700                : nodeText(name);
701            if (text.length > 0) {
702                return cleanText(text);
703            }
704        }
705
706        switch (node.kind) {
707            case SyntaxKind.SourceFile:
708                const sourceFile = <SourceFile>node;
709                return isExternalModule(sourceFile)
710                    ? `"${escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName))))}"`
711                    : "<global>";
712            case SyntaxKind.ExportAssignment:
713                return isExportAssignment(node) && node.isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default;
714
715            case SyntaxKind.ArrowFunction:
716            case SyntaxKind.FunctionDeclaration:
717            case SyntaxKind.FunctionExpression:
718            case SyntaxKind.ClassDeclaration:
719            case SyntaxKind.ClassExpression:
720            case SyntaxKind.StructDeclaration:
721                if (getSyntacticModifierFlags(node) & ModifierFlags.Default) {
722                    return "default";
723                }
724                // We may get a string with newlines or other whitespace in the case of an object dereference
725                // (eg: "app\n.onactivated"), so we should remove the whitespace for readability in the
726                // navigation bar.
727                return getFunctionOrClassName(<ArrowFunction | FunctionExpression | ClassExpression>node);
728            case SyntaxKind.Constructor:
729                return "constructor";
730            case SyntaxKind.ConstructSignature:
731                return "new()";
732            case SyntaxKind.CallSignature:
733                return "()";
734            case SyntaxKind.IndexSignature:
735                return "[]";
736            default:
737                return "<unknown>";
738        }
739    }
740
741    /** Flattens the NavNode tree to a list of items to appear in the primary navbar menu. */
742    function primaryNavBarMenuItems(root: NavigationBarNode): NavigationBarNode[] {
743        // The primary (middle) navbar menu displays the general code navigation hierarchy, similar to the navtree.
744        // The secondary (right) navbar menu displays the child items of whichever primary item is selected.
745        // Some less interesting items without their own child navigation items (e.g. a local variable declaration) only show up in the secondary menu.
746        const primaryNavBarMenuItems: NavigationBarNode[] = [];
747        function recur(item: NavigationBarNode) {
748            if (shouldAppearInPrimaryNavBarMenu(item)) {
749                primaryNavBarMenuItems.push(item);
750                if (item.children) {
751                    for (const child of item.children) {
752                        recur(child);
753                    }
754                }
755            }
756        }
757        recur(root);
758        return primaryNavBarMenuItems;
759
760        /** Determines if a node should appear in the primary navbar menu. */
761        function shouldAppearInPrimaryNavBarMenu(item: NavigationBarNode): boolean {
762            // Items with children should always appear in the primary navbar menu.
763            if (item.children) {
764                return true;
765            }
766
767            // Some nodes are otherwise important enough to always include in the primary navigation menu.
768            switch (navigationBarNodeKind(item)) {
769                case SyntaxKind.ClassDeclaration:
770                case SyntaxKind.ClassExpression:
771                case SyntaxKind.StructDeclaration:
772                case SyntaxKind.EnumDeclaration:
773                case SyntaxKind.InterfaceDeclaration:
774                case SyntaxKind.ModuleDeclaration:
775                case SyntaxKind.SourceFile:
776                case SyntaxKind.TypeAliasDeclaration:
777                case SyntaxKind.JSDocTypedefTag:
778                case SyntaxKind.JSDocCallbackTag:
779                    return true;
780
781                case SyntaxKind.ArrowFunction:
782                case SyntaxKind.FunctionDeclaration:
783                case SyntaxKind.FunctionExpression:
784                    return isTopLevelFunctionDeclaration(item);
785
786                default:
787                    return false;
788            }
789            function isTopLevelFunctionDeclaration(item: NavigationBarNode): boolean {
790                if (!(<FunctionDeclaration>item.node).body) {
791                    return false;
792                }
793
794                switch (navigationBarNodeKind(item.parent!)) {
795                    case SyntaxKind.ModuleBlock:
796                    case SyntaxKind.SourceFile:
797                    case SyntaxKind.MethodDeclaration:
798                    case SyntaxKind.Constructor:
799                        return true;
800                    default:
801                        return false;
802                }
803            }
804        }
805    }
806
807    function convertToTree(n: NavigationBarNode): NavigationTree {
808        return {
809            text: getItemName(n.node, n.name),
810            kind: getNodeKind(n.node),
811            kindModifiers: getModifiers(n.node),
812            spans: getSpans(n),
813            nameSpan: n.name && getNodeSpan(n.name),
814            childItems: map(n.children, convertToTree)
815        };
816    }
817
818    function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem {
819        return {
820            text: getItemName(n.node, n.name),
821            kind: getNodeKind(n.node),
822            kindModifiers: getModifiers(n.node),
823            spans: getSpans(n),
824            childItems: map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray,
825            indent: n.indent,
826            bolded: false,
827            grayed: false
828        };
829
830        function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem {
831            return {
832                text: getItemName(n.node, n.name),
833                kind: getNodeKind(n.node),
834                kindModifiers: getNodeModifiers(n.node),
835                spans: getSpans(n),
836                childItems: emptyChildItemArray,
837                indent: 0,
838                bolded: false,
839                grayed: false
840            };
841        }
842    }
843
844    function getSpans(n: NavigationBarNode): TextSpan[] {
845        const spans = [getNodeSpan(n.node)];
846        if (n.additionalNodes) {
847            for (const node of n.additionalNodes) {
848                spans.push(getNodeSpan(node));
849            }
850        }
851        return spans;
852    }
853
854    function getModuleName(moduleDeclaration: ModuleDeclaration): string {
855        // We want to maintain quotation marks.
856        if (isAmbientModule(moduleDeclaration)) {
857            return getTextOfNode(moduleDeclaration.name);
858        }
859
860        // Otherwise, we need to aggregate each identifier to build up the qualified name.
861        const result = [getTextOfIdentifierOrLiteral(moduleDeclaration.name)];
862        while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) {
863            moduleDeclaration = <ModuleDeclaration>moduleDeclaration.body;
864            result.push(getTextOfIdentifierOrLiteral(moduleDeclaration.name));
865        }
866        return result.join(".");
867    }
868
869    /**
870     * For 'module A.B.C', we want to get the node for 'C'.
871     * We store 'A' as associated with a NavNode, and use getModuleName to traverse down again.
872     */
873    function getInteriorModule(decl: ModuleDeclaration): ModuleDeclaration {
874        return decl.body && isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl;
875    }
876
877    function isComputedProperty(member: EnumMember): boolean {
878        return !member.name || member.name.kind === SyntaxKind.ComputedPropertyName;
879    }
880
881    function getNodeSpan(node: Node): TextSpan {
882        return node.kind === SyntaxKind.SourceFile ? createTextSpanFromRange(node) : createTextSpanFromNode(node, curSourceFile);
883    }
884
885    function getModifiers(node: Node): string {
886        if (node.parent && node.parent.kind === SyntaxKind.VariableDeclaration) {
887            node = node.parent;
888        }
889        return getNodeModifiers(node);
890    }
891
892    function getFunctionOrClassName(node: FunctionExpression | FunctionDeclaration | ArrowFunction | ClassLikeDeclaration): string {
893        const { parent } = node;
894        if (node.name && getFullWidth(node.name) > 0) {
895            return cleanText(declarationNameToString(node.name));
896        }
897        // See if it is a var initializer. If so, use the var name.
898        else if (isVariableDeclaration(parent)) {
899            return cleanText(declarationNameToString(parent.name));
900        }
901        // See if it is of the form "<expr> = function(){...}". If so, use the text from the left-hand side.
902        else if (isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken) {
903            return nodeText(parent.left).replace(whiteSpaceRegex, "");
904        }
905        // See if it is a property assignment, and if so use the property name
906        else if (isPropertyAssignment(parent)) {
907            return nodeText(parent.name);
908        }
909        // Default exports are named "default"
910        else if (getSyntacticModifierFlags(node) & ModifierFlags.Default) {
911            return "default";
912        }
913        else if (isClassLike(node)) {
914            return "<class>";
915        }
916        else if (isCallExpression(parent)) {
917            let name = getCalledExpressionName(parent.expression);
918            if (name !== undefined) {
919                name = cleanText(name);
920
921                if (name.length > maxLength) {
922                    return `${name} callback`;
923                }
924
925                const args = cleanText(mapDefined(parent.arguments, a => isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", "));
926                return `${name}(${args}) callback`;
927            }
928        }
929        return "<function>";
930    }
931
932    // See also 'tryGetPropertyAccessOrIdentifierToString'
933    function getCalledExpressionName(expr: Expression): string | undefined {
934        if (isIdentifier(expr)) {
935            return expr.text;
936        }
937        else if (isPropertyAccessExpression(expr)) {
938            const left = getCalledExpressionName(expr.expression);
939            const right = expr.name.text;
940            return left === undefined ? right : `${left}.${right}`;
941        }
942        else {
943            return undefined;
944        }
945    }
946
947    function isFunctionOrClassExpression(node: Node): node is ArrowFunction | FunctionExpression | ClassExpression {
948        switch (node.kind) {
949            case SyntaxKind.ArrowFunction:
950            case SyntaxKind.FunctionExpression:
951            case SyntaxKind.ClassExpression:
952                return true;
953            default:
954                return false;
955        }
956    }
957
958    function cleanText(text: string): string {
959        // Truncate to maximum amount of characters as we don't want to do a big replace operation.
960        text = text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
961
962        // Replaces ECMAScript line terminators and removes the trailing `\` from each line:
963        // \n - Line Feed
964        // \r - Carriage Return
965        // \u2028 - Line separator
966        // \u2029 - Paragraph separator
967        return text.replace(/\\?(\r?\n|\r|\u2028|\u2029)/g, "");
968    }
969}
970