Skip to content

Commit 6a706cf

Browse files
committed
Merge pull request #7504 from Microsoft/jsNavBar
Improve NavBar experience for JavaScript files
2 parents 4927091 + 6a8fb3e commit 6a706cf

File tree

1 file changed

+218
-9
lines changed

1 file changed

+218
-9
lines changed

src/services/navigationBar.ts

Lines changed: 218 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
/// <reference path='utilities.ts' />
12
/// <reference path='services.ts' />
23

34
/* @internal */
45
namespace ts.NavigationBar {
56
export function getNavigationBarItems(sourceFile: SourceFile, compilerOptions: CompilerOptions): ts.NavigationBarItem[] {
7+
// TODO: Handle JS files differently in 'navbar' calls for now, but ideally we should unify
8+
// the 'navbar' and 'navto' logic for TypeScript and JavaScript.
9+
if (isSourceFileJavaScript(sourceFile)) {
10+
return getJsNavigationBarItems(sourceFile, compilerOptions);
11+
}
12+
613
// If the source file has any child items, then it included in the tree
714
// and takes lexical ownership of all other top-level items.
815
let hasGlobalNode = false;
@@ -130,7 +137,7 @@ namespace ts.NavigationBar {
130137

131138
return topLevelNodes;
132139
}
133-
140+
134141
function sortNodes(nodes: Node[]): Node[] {
135142
return nodes.slice(0).sort((n1: Declaration, n2: Declaration) => {
136143
if (n1.name && n2.name) {
@@ -147,7 +154,7 @@ namespace ts.NavigationBar {
147154
}
148155
});
149156
}
150-
157+
151158
function addTopLevelNodes(nodes: Node[], topLevelNodes: Node[]): void {
152159
nodes = sortNodes(nodes);
153160

@@ -178,8 +185,8 @@ namespace ts.NavigationBar {
178185

179186
function isTopLevelFunctionDeclaration(functionDeclaration: FunctionLikeDeclaration) {
180187
if (functionDeclaration.kind === SyntaxKind.FunctionDeclaration) {
181-
// A function declaration is 'top level' if it contains any function declarations
182-
// within it.
188+
// A function declaration is 'top level' if it contains any function declarations
189+
// within it.
183190
if (functionDeclaration.body && functionDeclaration.body.kind === SyntaxKind.Block) {
184191
// Proper function declarations can only have identifier names
185192
if (forEach((<Block>functionDeclaration.body).statements,
@@ -198,7 +205,7 @@ namespace ts.NavigationBar {
198205

199206
return false;
200207
}
201-
208+
202209
function getItemsWorker(nodes: Node[], createItem: (n: Node) => ts.NavigationBarItem): ts.NavigationBarItem[] {
203210
let items: ts.NavigationBarItem[] = [];
204211

@@ -395,19 +402,19 @@ namespace ts.NavigationBar {
395402
let result: string[] = [];
396403

397404
result.push(moduleDeclaration.name.text);
398-
405+
399406
while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) {
400407
moduleDeclaration = <ModuleDeclaration>moduleDeclaration.body;
401408

402409
result.push(moduleDeclaration.name.text);
403-
}
410+
}
404411

405412
return result.join(".");
406413
}
407414

408415
function createModuleItem(node: ModuleDeclaration): NavigationBarItem {
409416
let moduleName = getModuleName(node);
410-
417+
411418
let childItems = getItemsWorker(getChildNodes((<Block>getInnermostModule(node).body).statements), createChildItem);
412419

413420
return getNavigationBarItem(moduleName,
@@ -534,4 +541,206 @@ namespace ts.NavigationBar {
534541
return getTextOfNodeFromSourceText(sourceFile.text, node);
535542
}
536543
}
537-
}
544+
545+
export function getJsNavigationBarItems(sourceFile: SourceFile, compilerOptions: CompilerOptions): NavigationBarItem[] {
546+
const anonFnText = "<function>";
547+
const anonClassText = "<class>";
548+
let indent = 0;
549+
550+
let rootName = isExternalModule(sourceFile) ?
551+
"\"" + escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName)))) + "\""
552+
: "<global>";
553+
554+
let sourceFileItem = getNavBarItem(rootName, ScriptElementKind.moduleElement, [getNodeSpan(sourceFile)]);
555+
let topItem = sourceFileItem;
556+
557+
// Walk the whole file, because we want to also find function expressions - which may be in variable initializer,
558+
// call arguments, expressions, etc...
559+
forEachChild(sourceFile, visitNode);
560+
561+
function visitNode(node: Node) {
562+
const newItem = createNavBarItem(node);
563+
564+
if (newItem) {
565+
topItem.childItems.push(newItem);
566+
}
567+
568+
// Add a level if traversing into a container
569+
if (newItem && (isFunctionLike(node) || isClassLike(node))) {
570+
const lastTop = topItem;
571+
indent++;
572+
topItem = newItem;
573+
forEachChild(node, visitNode);
574+
topItem = lastTop;
575+
indent--;
576+
577+
// If the last item added was an anonymous function expression, and it had no children, discard it.
578+
if (newItem && newItem.text === anonFnText && newItem.childItems.length === 0) {
579+
topItem.childItems.pop();
580+
}
581+
}
582+
else {
583+
forEachChild(node, visitNode);
584+
}
585+
}
586+
587+
function createNavBarItem(node: Node) : NavigationBarItem {
588+
switch (node.kind) {
589+
case SyntaxKind.VariableDeclaration:
590+
// Only add to the navbar if at the top-level of the file
591+
// Note: "const" and "let" are also SyntaxKind.VariableDeclarations
592+
if(node.parent/*VariableDeclarationList*/.parent/*VariableStatement*/
593+
.parent/*SourceFile*/.kind !== SyntaxKind.SourceFile) {
594+
return undefined;
595+
}
596+
// If it is initialized with a function expression, handle it when we reach the function expression node
597+
const varDecl = node as VariableDeclaration;
598+
if (varDecl.initializer && (varDecl.initializer.kind === SyntaxKind.FunctionExpression ||
599+
varDecl.initializer.kind === SyntaxKind.ArrowFunction ||
600+
varDecl.initializer.kind === SyntaxKind.ClassExpression)) {
601+
return undefined;
602+
}
603+
// Fall through
604+
case SyntaxKind.FunctionDeclaration:
605+
case SyntaxKind.ClassDeclaration:
606+
case SyntaxKind.Constructor:
607+
case SyntaxKind.GetAccessor:
608+
case SyntaxKind.SetAccessor:
609+
// "export default function().." looks just like a regular function/class declaration, except with the 'default' flag
610+
const name = node.flags && (node.flags & NodeFlags.Default) && !(node as (Declaration)).name ? "default" :
611+
node.kind === SyntaxKind.Constructor ? "constructor" :
612+
declarationNameToString((node as (Declaration)).name);
613+
return getNavBarItem(name, getScriptKindForElementKind(node.kind), [getNodeSpan(node)]);
614+
case SyntaxKind.FunctionExpression:
615+
case SyntaxKind.ArrowFunction:
616+
case SyntaxKind.ClassExpression:
617+
return getDefineModuleItem(node) || getFunctionOrClassExpressionItem(node);
618+
case SyntaxKind.MethodDeclaration:
619+
const methodDecl = node as MethodDeclaration;
620+
return getNavBarItem(declarationNameToString(methodDecl.name),
621+
ScriptElementKind.memberFunctionElement,
622+
[getNodeSpan(node)]);
623+
case SyntaxKind.ExportAssignment:
624+
// e.g. "export default <expr>"
625+
return getNavBarItem("default", ScriptElementKind.variableElement, [getNodeSpan(node)]);
626+
case SyntaxKind.ImportClause: // e.g. 'def' in: import def from 'mod' (in ImportDeclaration)
627+
if (!(node as ImportClause).name) {
628+
// No default import (this node is still a parent of named & namespace imports, which are handled below)
629+
return undefined;
630+
}
631+
// fall through
632+
case SyntaxKind.ImportSpecifier: // e.g. 'id' in: import {id} from 'mod' (in NamedImports, in ImportClause)
633+
case SyntaxKind.NamespaceImport: // e.g. '* as ns' in: import * as ns from 'mod' (in ImportClause)
634+
case SyntaxKind.ExportSpecifier: // e.g. 'a' or 'b' in: export {a, foo as b} from 'mod'
635+
// Export specifiers are only interesting if they are reexports from another module, or renamed, else they are already globals
636+
if (node.kind === SyntaxKind.ExportSpecifier) {
637+
if (!(node.parent.parent as ExportDeclaration).moduleSpecifier && !(node as ExportSpecifier).propertyName) {
638+
return undefined;
639+
}
640+
}
641+
const decl = node as (ImportSpecifier | ImportClause | NamespaceImport | ExportSpecifier);
642+
if (!decl.name) {
643+
return undefined;
644+
}
645+
const declName = declarationNameToString(decl.name);
646+
return getNavBarItem(declName, ScriptElementKind.constElement, [getNodeSpan(node)]);
647+
default:
648+
return undefined;
649+
}
650+
}
651+
652+
function getNavBarItem(text: string, kind: string, spans: TextSpan[], kindModifiers = ScriptElementKindModifier.none): NavigationBarItem {
653+
return {
654+
text, kind, kindModifiers, spans, childItems: [], indent, bolded: false, grayed: false
655+
}
656+
}
657+
658+
function getDefineModuleItem(node: Node): NavigationBarItem {
659+
if (node.kind !== SyntaxKind.FunctionExpression && node.kind !== SyntaxKind.ArrowFunction) {
660+
return undefined;
661+
}
662+
663+
// No match if this is not a call expression to an identifier named 'define'
664+
if (node.parent.kind !== SyntaxKind.CallExpression) {
665+
return undefined;
666+
}
667+
const callExpr = node.parent as CallExpression;
668+
if (callExpr.expression.kind !== SyntaxKind.Identifier || callExpr.expression.getText() !== 'define') {
669+
return undefined;
670+
}
671+
672+
// Return a module of either the given text in the first argument, or of the source file path
673+
let defaultName = node.getSourceFile().fileName;
674+
if (callExpr.arguments[0].kind === SyntaxKind.StringLiteral) {
675+
defaultName = ((callExpr.arguments[0]) as StringLiteral).text;
676+
}
677+
return getNavBarItem(defaultName, ScriptElementKind.moduleElement, [getNodeSpan(node.parent)]);
678+
}
679+
680+
function getFunctionOrClassExpressionItem(node: Node): NavigationBarItem {
681+
if (node.kind !== SyntaxKind.FunctionExpression &&
682+
node.kind !== SyntaxKind.ArrowFunction &&
683+
node.kind !== SyntaxKind.ClassExpression) {
684+
return undefined;
685+
}
686+
687+
const fnExpr = node as FunctionExpression | ArrowFunction | ClassExpression;
688+
let fnName: string;
689+
if (fnExpr.name && getFullWidth(fnExpr.name) > 0) {
690+
// The expression has an identifier, so use that as the name
691+
fnName = declarationNameToString(fnExpr.name);
692+
}
693+
else {
694+
// See if it is a var initializer. If so, use the var name.
695+
if (fnExpr.parent.kind === SyntaxKind.VariableDeclaration) {
696+
fnName = declarationNameToString((fnExpr.parent as VariableDeclaration).name);
697+
}
698+
// See if it is of the form "<expr> = function(){...}". If so, use the text from the left-hand side.
699+
else if (fnExpr.parent.kind === SyntaxKind.BinaryExpression &&
700+
(fnExpr.parent as BinaryExpression).operatorToken.kind === SyntaxKind.EqualsToken) {
701+
fnName = (fnExpr.parent as BinaryExpression).left.getText();
702+
if (fnName.length > 20) {
703+
fnName = fnName.substring(0, 17) + "...";
704+
}
705+
}
706+
// See if it is a property assignment, and if so use the property name
707+
else if (fnExpr.parent.kind === SyntaxKind.PropertyAssignment &&
708+
(fnExpr.parent as PropertyAssignment).name) {
709+
fnName = (fnExpr.parent as PropertyAssignment).name.getText();
710+
}
711+
else {
712+
fnName = node.kind === SyntaxKind.ClassExpression ? anonClassText : anonFnText;
713+
}
714+
}
715+
const scriptKind = node.kind === SyntaxKind.ClassExpression ? ScriptElementKind.classElement : ScriptElementKind.functionElement;
716+
return getNavBarItem(fnName, scriptKind, [getNodeSpan(node)]);
717+
}
718+
719+
function getNodeSpan(node: Node) {
720+
return node.kind === SyntaxKind.SourceFile
721+
? createTextSpanFromBounds(node.getFullStart(), node.getEnd())
722+
: createTextSpanFromBounds(node.getStart(), node.getEnd());
723+
}
724+
725+
function getScriptKindForElementKind(kind: SyntaxKind) {
726+
switch (kind) {
727+
case SyntaxKind.VariableDeclaration:
728+
return ScriptElementKind.variableElement;
729+
case SyntaxKind.FunctionDeclaration:
730+
return ScriptElementKind.functionElement;
731+
case SyntaxKind.ClassDeclaration:
732+
return ScriptElementKind.classElement;
733+
case SyntaxKind.Constructor:
734+
return ScriptElementKind.constructorImplementationElement;
735+
case SyntaxKind.GetAccessor:
736+
return ScriptElementKind.memberGetAccessorElement;
737+
case SyntaxKind.SetAccessor:
738+
return ScriptElementKind.memberSetAccessorElement;
739+
default:
740+
return "unknown";
741+
}
742+
}
743+
744+
return sourceFileItem.childItems;
745+
}
746+
}

0 commit comments

Comments
 (0)