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