-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Changes from 7 commits
cae42fd
080f639
f119cde
9a5cb29
b3a35c5
71f604a
5c10301
f85911d
fe4efe8
6a8fb3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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[] { | ||
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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about just |
||
case SyntaxKind.ClassDeclaration: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't you just using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would I? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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) { | ||
|
@@ -147,7 +356,7 @@ namespace ts.NavigationBar { | |
} | ||
}); | ||
} | ||
|
||
function addTopLevelNodes(nodes: Node[], topLevelNodes: Node[]): void { | ||
nodes = sortNodes(nodes); | ||
|
||
|
@@ -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, | ||
|
@@ -198,7 +407,7 @@ namespace ts.NavigationBar { | |
|
||
return false; | ||
} | ||
|
||
function getItemsWorker(nodes: Node[], createItem: (n: Node) => ts.NavigationBarItem): ts.NavigationBarItem[] { | ||
let items: ts.NavigationBarItem[] = []; | ||
|
||
|
@@ -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, | ||
|
There was a problem hiding this comment.
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.