Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve NavBar experience for JavaScript files #7504

Merged
merged 10 commits into from
Mar 15, 2016
225 changes: 217 additions & 8 deletions src/services/navigationBar.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,217 @@
/// <reference path='utilities.ts' />
/// <reference path='services.ts' />

/* @internal */
namespace ts.NavigationBar {
export function getJsNavigationBarItems(sourceFile: SourceFile, compilerOptions: CompilerOptions): NavigationBarItem[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this below getNavigationBarItems? Generally we have helper functions cascading below the entry point.

const anonFnText = "<function>";
let indent = 0;

let rootName = isExternalModule(sourceFile) ?
"\"" + escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName)))) + "\""
: "<global>";

let sourceFileItem = getNavBarItem(rootName, ScriptElementKind.moduleElement, [getNodeSpan(sourceFile)]);
let topItem = sourceFileItem;

// Walk the whole file, because we want to also find function expressions - which may be in variable initializer,
// call arguments, expressions, etc...
forEachChild(sourceFile, visitNode);

function visitNode(node: Node) {
const newItem = createNavBarItem(node);

if (newItem) {
topItem.childItems.push(newItem);
}

// Add a level if traversing into a container
if (isNavBarContainer(node)) {
const lastTop = topItem;
indent++;
topItem = newItem;
forEachChild(node, visitNode);
topItem = lastTop;
indent--;

// If the last item added was an anonymous function expression, and it had no children, discard it.
if (newItem && newItem.text === anonFnText && newItem.childItems.length === 0) {
topItem.childItems.pop();
}
}
else {
forEachChild(node, visitNode);
}
}

function createNavBarItem(node: Node) : NavigationBarItem {
switch (node.kind) {
case SyntaxKind.VariableDeclaration:
// Only add to the navbar if at the top-level of the file
// Note: "const" and "let" are also SyntaxKind.VariableDeclarations
if(node.parent/*VariableDeclarationList*/.parent/*VariableStatement*/
.parent/*SourceFile*/.kind !== SyntaxKind.SourceFile) {
return undefined;
}
// If it is initialized with a function expression, handle it when we reach the function expression node
const varDecl = node as VariableDeclaration;
if (varDecl.initializer && (varDecl.initializer.kind === SyntaxKind.FunctionExpression ||
varDecl.initializer.kind === SyntaxKind.ArrowFunction )) {
return undefined;
}
// Fall through
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
const name = node.kind === SyntaxKind.Constructor ?
"constructor" : declarationNameToString((node as (Declaration)).name);

const elementKind =
node.kind === SyntaxKind.VariableDeclaration ? ScriptElementKind.variableElement :
node.kind === SyntaxKind.FunctionDeclaration ? ScriptElementKind.functionElement :
node.kind === SyntaxKind.ClassDeclaration ? ScriptElementKind.classElement :
node.kind === SyntaxKind.GetAccessor ? ScriptElementKind.memberGetAccessorElement :
node.kind === SyntaxKind.SetAccessor ? ScriptElementKind.memberSetAccessorElement :
"constructor";

return getNavBarItem(name, elementKind, [getNodeSpan(node)]);
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
return getDefineModuleItem(node) || getFunctionExpressionItem(node);
case SyntaxKind.MethodDeclaration:
const methodDecl = node as MethodDeclaration;
if (!methodDecl.name) {
return undefined;
}
return getNavBarItem(declarationNameToString(methodDecl.name),
ScriptElementKind.memberFunctionElement,
[getNodeSpan(node)]);
case SyntaxKind.ExportAssignment:
return getNavBarItem("default", ScriptElementKind.variableElement, [getNodeSpan(node)]);
case SyntaxKind.ImportClause: // e.g. 'def' in: import def from 'mod' (in ImportDeclaration)
if (!(node as ImportClause).name) {
// No default import (this node is still a parent of named & namespace imports, which are handled below)
return undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a // fall through

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

case SyntaxKind.ImportSpecifier: // e.g. 'id' in: import {id} from 'mod' (in NamedImports, in ImportClause)
case SyntaxKind.NamespaceImport: // e.g. '* as ns' in: import * as ns from 'mod' (in ImportClause)
case SyntaxKind.ExportSpecifier: // e.g. 'a' or 'b' in: export {a, foo as b} from 'mod'
// Export specifiers are only interesting if they are reexports from another module, or renamed, else they are already globals
if (node.kind === SyntaxKind.ExportSpecifier) {
if (!(node.parent.parent as ExportDeclaration).moduleSpecifier && !(node as ExportSpecifier).propertyName) {
return undefined;
}
}
const decl = node as (ImportSpecifier | ImportClause | NamespaceImport | ExportSpecifier);
if (!decl.name) {
return undefined;
}
const declName = declarationNameToString(decl.name);
return getNavBarItem(declName, ScriptElementKind.constElement, [getNodeSpan(node)]);
default:
return undefined;
}
}

function isNavBarContainer(node: Node) {
// We only want to add a new level when going into a function or a class
switch (node.kind) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just return isFunctionLike(node) || isClassLike(node)

case SyntaxKind.ClassDeclaration:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassExpression?

case SyntaxKind.Constructor:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
return true;
default:
return false;
}
}

function getNavBarItem(text: string, kind: string, spans: TextSpan[], kindModifiers = ScriptElementKindModifier.none): NavigationBarItem {
return {
text, kind, kindModifiers, spans, childItems: [], indent, bolded: false, grayed: false
}
}

function getDefineModuleItem(node: Node): NavigationBarItem {
if (node.kind !== SyntaxKind.FunctionExpression && node.kind !== SyntaxKind.ArrowFunction) {
return undefined;
}

// No match if this is not a call expression to an identifier named 'define'
if (node.parent.kind !== SyntaxKind.CallExpression) {
return undefined;
}
const callExpr = node.parent as CallExpression;
if (callExpr.expression.kind !== SyntaxKind.Identifier || callExpr.expression.getText() !== 'define') {
return undefined;
}

// Return a module of either the given text in the first argument, or of the source file path
let defaultName = node.getSourceFile().fileName;
if (callExpr.arguments[0].kind === SyntaxKind.StringLiteral) {
defaultName = ((callExpr.arguments[0]) as StringLiteral).text;
}
return getNavBarItem(defaultName, ScriptElementKind.moduleElement, [getNodeSpan(node.parent)]);
}

function getFunctionExpressionItem(node: Node): NavigationBarItem {
if (node.kind !== SyntaxKind.FunctionExpression && node.kind !== SyntaxKind.ArrowFunction) {
return undefined;
}

const fnExpr = node as FunctionExpression | ArrowFunction;
let fnName: string;
if (fnExpr.name && getFullWidth(fnExpr.name) > 0) {
// The function expression has an identifier, so use that as the name
fnName = declarationNameToString(fnExpr.name);
}
else {
// See if it is a var initializer. If so, use the var name.
if (fnExpr.parent.kind === SyntaxKind.VariableDeclaration) {
fnName = declarationNameToString((fnExpr.parent as VariableDeclaration).name);
}
// See if it is of the form "<expr> = function(){...}". If so, use the text from the left-hand side.
else if (fnExpr.parent.kind === SyntaxKind.BinaryExpression &&
(fnExpr.parent as BinaryExpression).operatorToken.kind === SyntaxKind.FirstAssignment) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't you just using EqualsToken?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would I?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because FirstAssignment is prone to change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FirstAssigment is a sentinel value. It may or may not be EqualsToken in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, what if the LHS is a binding pattern? Why not just restrict this to identifiers, property accesses, and element accesses using string literals?

fnName = (fnExpr.parent as BinaryExpression).left.getText();
if (fnName.length > 20) {
fnName = fnName.substring(0,20) + "...";
}
}
// See if it is a property assignment, and if so use the property name
else if (fnExpr.parent.kind === SyntaxKind.PropertyAssignment &&
(fnExpr.parent as PropertyAssignment).name) {
fnName = (fnExpr.parent as PropertyAssignment).name.getText();
}
else {
fnName = anonFnText;
}
}
return getNavBarItem(fnName, ScriptElementKind.functionElement, [getNodeSpan(node)]);
}

function getNodeSpan(node: Node) {
return node.kind === SyntaxKind.SourceFile
? createTextSpanFromBounds(node.getFullStart(), node.getEnd())
: createTextSpanFromBounds(node.getStart(), node.getEnd());
}

return sourceFileItem.childItems;
}

export function getNavigationBarItems(sourceFile: SourceFile, compilerOptions: CompilerOptions): ts.NavigationBarItem[] {
// TODO: Handle JS files differently in 'navbar' calls for now, but ideally we should unify
// the 'navbar' and 'navto' logic for TypeScript and JavaScript.
if (isSourceFileJavaScript(sourceFile)) {
return getJsNavigationBarItems(sourceFile, compilerOptions);
}

// If the source file has any child items, then it included in the tree
// and takes lexical ownership of all other top-level items.
let hasGlobalNode = false;
Expand Down Expand Up @@ -130,7 +339,7 @@ namespace ts.NavigationBar {

return topLevelNodes;
}

function sortNodes(nodes: Node[]): Node[] {
return nodes.slice(0).sort((n1: Declaration, n2: Declaration) => {
if (n1.name && n2.name) {
Expand All @@ -147,7 +356,7 @@ namespace ts.NavigationBar {
}
});
}

function addTopLevelNodes(nodes: Node[], topLevelNodes: Node[]): void {
nodes = sortNodes(nodes);

Expand Down Expand Up @@ -178,8 +387,8 @@ namespace ts.NavigationBar {

function isTopLevelFunctionDeclaration(functionDeclaration: FunctionLikeDeclaration) {
if (functionDeclaration.kind === SyntaxKind.FunctionDeclaration) {
// A function declaration is 'top level' if it contains any function declarations
// within it.
// A function declaration is 'top level' if it contains any function declarations
// within it.
if (functionDeclaration.body && functionDeclaration.body.kind === SyntaxKind.Block) {
// Proper function declarations can only have identifier names
if (forEach((<Block>functionDeclaration.body).statements,
Expand All @@ -198,7 +407,7 @@ namespace ts.NavigationBar {

return false;
}

function getItemsWorker(nodes: Node[], createItem: (n: Node) => ts.NavigationBarItem): ts.NavigationBarItem[] {
let items: ts.NavigationBarItem[] = [];

Expand Down Expand Up @@ -395,19 +604,19 @@ namespace ts.NavigationBar {
let result: string[] = [];

result.push(moduleDeclaration.name.text);

while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) {
moduleDeclaration = <ModuleDeclaration>moduleDeclaration.body;

result.push(moduleDeclaration.name.text);
}
}

return result.join(".");
}

function createModuleItem(node: ModuleDeclaration): NavigationBarItem {
let moduleName = getModuleName(node);

let childItems = getItemsWorker(getChildNodes((<Block>getInnermostModule(node).body).statements), createChildItem);

return getNavigationBarItem(moduleName,
Expand Down