aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Rennie2013-11-22 13:55:19 (EST)
committerGerrit Code Review @ Eclipse.org2013-11-22 14:49:13 (EST)
commite81382db17a7360c3cdd2b5358aa8b5bbeb44d58 (patch)
treebae0e4cb2d97f54d305489fe761554a527f5500d
parent09ca7237d4093a8a5563eccdb658c22a94e5c0e2 (diff)
downloadorg.eclipse.orion.client-e81382db17a7360c3cdd2b5358aa8b5bbeb44d58.zip
org.eclipse.orion.client-e81382db17a7360c3cdd2b5358aa8b5bbeb44d58.tar.gz
org.eclipse.orion.client-e81382db17a7360c3cdd2b5358aa8b5bbeb44d58.tar.bz2
Bug 422080 - Improve JavaScript outline (was: Improve the JSDoc outline)refs/changes/53/18753/4
Change-Id: If633844a12513f8113105789f5c595c251c5202b Signed-off-by: Mike Rennie <Michael_Rennie@ca.ibm.com>
-rw-r--r--bundles/org.eclipse.orion.client.javascript/web/javascript/outliner.js243
-rw-r--r--bundles/org.eclipse.orion.client.javascript/web/javascript/plugins/javascriptPlugin.js25
-rw-r--r--bundles/org.eclipse.orion.client.javascript/web/javascript/signatures.js191
3 files changed, 345 insertions, 114 deletions
diff --git a/bundles/org.eclipse.orion.client.javascript/web/javascript/outliner.js b/bundles/org.eclipse.orion.client.javascript/web/javascript/outliner.js
index b148133..3011e0f 100644
--- a/bundles/org.eclipse.orion.client.javascript/web/javascript/outliner.js
+++ b/bundles/org.eclipse.orion.client.javascript/web/javascript/outliner.js
@@ -11,156 +11,205 @@
*******************************************************************************/
/*global define*/
define([
-'orion/objects'
-], function(Objects) {
+'orion/objects',
+'javascript/signatures',
+'estraverse/estraverse'
+], function(Objects, Signatures, Estraverse) {
/**
- * @name javascript.JSDocOutliner
- * @description creates a new instance of the outliner
- * @constructor
- * @public
+ * @name javascript.Visitor
+ * @description The AST visitor passed into estraverse
+ * @constrcutor
+ * @private
+ * @since 5.0
*/
- function JSDocOutliner() {
+ function Visitor() {
}
- Objects.mixin(JSDocOutliner.prototype, /** @lends javascript.JSDocOutliner.prototype*/ {
-
+ Objects.mixin(Visitor.prototype, /** @lends javascript.Visitor.prototype */ {
+ outline: [],
+ scope: [],
+
/**
- * @name computeOutline
- * @description callback from the <code>orion.edit.outliner</code> service to create
- * an outline
+ * @name enter
+ * @description Callback from estraverse when a node is starting to be visited
* @function
- * @public
- * @memberof javascript.JSDocOutliner.prototype
- * @param {orion.edit.EditorContext} editorContext The editor context
- * @param {Object} options The options
- * @returns {orion.Promise} to compute the outline
+ * @private
+ * @memberof javascript.Visitor.prototype
+ * @param {Object} node The AST node currently being visited
+ * @returns The status if we should continue visiting
*/
- computeOutline: function(editorContext, options) {
- this.sourceCounter = 1;
+ enter: function(node) {
+ var item;
var that = this;
- var astoptions = {loc:true, comment:true, tolerant:true, tokens:false, range:false, raw:false};
- return editorContext.getAST(astoptions).then(function(ast) {
- if(ast && ast.comments) {
- var elements = [];
- ast.comments.forEach(function(node) {
- if(node.type === "Block") {
- //Outline element {label, className, line, children, href}
- elements.push({
- start : node.range[0] > 0 ? node.range[0] : 1,
- end : node.range[1],
- label : that._getNameFrom(node, ['@name']),
- //className : that._getNameFrom(node, ['@class', '@memberof']),
- children : that._getTagChildren(node)
- });
+ if(node.type === 'FunctionDeclaration') {
+ item = this.addElement(Signatures.computeSignature(node));
+ if(item) {
+ this.scope.push(item);
+ }
+ }
+ else if(node.type === 'FunctionExpression') {
+ if(!node.seen) {
+ item = this.addElement(Signatures.computeSignature(node));
+ if(item) {
+ this.scope.push(item);
+ }
+ }
+ else {
+ //scrub the flag from the AST node
+ delete node.seen;
+ }
+ }
+ else if(node.type === 'ObjectExpression') {
+ if(!node.seen) {
+ item = this.addElement(Signatures.computeSignature(node));
+ if(item) {
+ this.scope.push(item);
+ }
+ }
+ else {
+ //scrub the flag from the AST node
+ delete node.seen;
+ }
+ if(node.properties) {
+ node.properties.forEach(function(property) {
+ if(property.value) {
+ if(property.value.type === 'FunctionExpression' || property.value.type === 'ObjectExpression') {
+ item = that.addElement(Signatures.computeSignature(property));
+ if(item) {
+ that.scope.push(item);
+ }
+ property.value.seen = 1;
+ }
+ else {
+ that.addElement(Signatures.computeSignature(property));
+ }
}
});
- return elements;
}
- });
+ }
+ else if(node.type === 'VariableDeclaration') {
+ if(node.declarations) {
+ node.declarations.forEach(function(declaration) {
+ if(declaration.init) {
+ if(declaration.init.type === 'ObjectExpression') {
+ item = that.addElement(Signatures.computeSignature(declaration));
+ if(item) {
+ that.scope.push(item);
+ }
+ declaration.init.seen = 1;
+ }
+ }
+ });
+ }
+ }
},
/**
- * @name _getNameFrom
- * @description tries to compute a name for the element using the given array of names to look for
+ * @name leave
+ * @description Callback from estraverse when visitation of a node has completed
* @function
* @private
- * @memberof javascript.JSDocOutliner
- * @param {Object} node The AST doc node
- * @param {Array|String} tags An array of strings of the names of tags to look for to try and find a name
- * @returns the name to use for the given node if it could be computed, or 'Doc Node #N' if a name could not be computed
+ * @memberof javascript.Visitor.prototype
+ * @param {Object} node The AST node that ended its visitation
*/
- _getNameFrom: function(node, tags) {
- var that = this;
- var length = tags.length;
- for(var i = 0; i < length; i++) {
- var val = node.value;
- var tag = tags[i]+' ';
- var index = val.indexOf(tag);
- if(index > -1) {
- //hack, just assume a name does not have spaces
- var start = index+tag.length;
- var end = val.indexOf(' ', start);
- if(end > -1) {
- return val.substring(start, end);
- }
- }
+ leave: function(node) {
+ if(node.type === 'ObjectExpression' || node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
+ this.scope.pop();
}
- return "Doc node #"+that.sourceCounter++;
},
/**
- * @name _getTagChildren
- * @description Computes all of the tags of a doc node as a child array
+ * @name addElement
+ * @description Appends the given signature object to the running outline
* @function
* @private
- * @memberof javascript.JSDocOutliner
- * @param {Object} node The AST doc node
- * @returns {Array|Object} Returns the array of child tags
+ * @memberof javascript.Visitor.prototype
+ * @param {Object} sig The signature object
*/
- _getTagChildren: function(node) {
- var val = node.value;
- //hack assume all tags start with '@' and end in a space
- var kids = [];
- var idx = 0, atidx = 0;
- var length = val.length;
- var intag = false;
- while(idx < length) {
- var char = val.charAt(idx);
- if(char === '@') {
- intag = true;
- atidx = idx;
+ addElement: function(sig) {
+ if(sig) {
+ var item = {
+ label: sig.sig,
+ start: sig.range[0],
+ end: sig.range[1]
+ };
+ if(this.scope.length < 1) {
+ this.outline.push(item);
}
- else if(intag && (char === ' ' || char === '\n' || char === '\r')) {
- intag = false;
- kids.push({
- label : val.substring(atidx, idx),
- start: atidx + node.range[0]+2, //hack - the value of the node chops off the first two chars '/*'
- end: idx + node.range[0]+2
- });
+ else {
+ var parent = this.scope[this.scope.length-1];
+ if(!parent.children) {
+ parent.children = [];
+ }
+ parent.children.push(item);
}
- idx++;
- }
- if(kids.length === 0) {
- //Returning an emppty array causes twisties to be shown with no children, return null
- return null;
+ return item;
}
- return kids;
}
});
- JSDocOutliner.prototype.contructor = JSDocOutliner;
+ Visitor.prototype.constructor = Visitor;
/**
- * @name javascript.JsOutliner
+ * @name javascript.JSOutliner
* @description creates a new instance of the outliner
* @constructor
* @public
*/
- function JsOutliner() {
+ function JSOutliner() {
}
- Objects.mixin(JsOutliner.prototype, /** @lends javascript.JsOutliner.prototype*/ {
+ Objects.mixin(JSOutliner.prototype, /** @lends javascript.JSOutliner.prototype*/ {
+ visitor: null,
+
+ /**
+ * @name getVisitor
+ * @description Delegate function to get the visitor
+ * @function
+ * @private
+ * @memberof javascript.JSOutliner.prototype
+ * @returns The instance of {Visitor} to use
+ */
+ getVisitor: function() {
+ if(!this.visitor) {
+ this.visitor = new Visitor();
+ this.visitor.enter = this.visitor.enter.bind(this.visitor);
+ this.visitor.leave = this.visitor.leave.bind(this.visitor);
+ }
+ this.visitor.outline = [];
+ return this.visitor;
+ },
+
/**
* @name computeOutline
* @description callback from the <code>orion.edit.outliner</code> service to create
* an outline
* @function
* @public
- * @memberof javascript.JsOutliner.prototype
+ * @memberof javascript.JSOutliner.prototype
* @param {orion.edit.EditorContext} editorContext The editor context
* @param {Object} options The options
- * @returns {}
+ * @returns {orion.Promise} to compute the outline
*/
computeOutline: function(editorContext, options) {
- return [];
+ var that = this;
+ var astoptions = {loc:true, comment:true, tolerant:true, tokens:false, range:false, raw:false};
+ return editorContext.getAST(astoptions).then(function(ast) {
+ if(ast) {
+ var visitor = that.getVisitor();
+ Estraverse.traverse(ast, visitor);
+ return visitor.outline;
+ }
+ return [];
+ });
}
});
- JsOutliner.prototype.contructor = JsOutliner;
+ JSOutliner.prototype.contructor = JSOutliner;
return {
- JSDocOutliner: JSDocOutliner,
- JsOutliner: JsOutliner};
+ JSOutliner: JSOutliner
+ };
}); \ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.javascript/web/javascript/plugins/javascriptPlugin.js b/bundles/org.eclipse.orion.client.javascript/web/javascript/plugins/javascriptPlugin.js
index 6746567..802a71a 100644
--- a/bundles/org.eclipse.orion.client.javascript/web/javascript/plugins/javascriptPlugin.js
+++ b/bundles/org.eclipse.orion.client.javascript/web/javascript/plugins/javascriptPlugin.js
@@ -18,7 +18,7 @@ define([
'esprima/esprima',
'orion/serialize',
'orion/i18nUtil'
-], function(PluginProvider, Outliner, Occurrences, EsprimaAssist, _, Serialize, i18nUtil) {
+], function(PluginProvider, Outliner, Occurrences, EsprimaAssist, Esprima, Serialize, i18nUtil) {
/**
* Plug-in headers
@@ -47,33 +47,24 @@ define([
/**
* Register the jsdoc-based outline
*/
- provider.registerServiceProvider("orion.edit.outliner", new Outliner.JSDocOutliner(),
- { contentType: ["application/javascript"],
- name: "JSDoc outline",
- id: "orion.javascript.outliner.jsdoc"
- });
-
- /**
- * Register the raw source-based outline
- */
- /**provider.registerServiceProvider("orion.edit.outliner", new Outliner.JsOutliner(),
+ provider.registerServiceProvider("orion.edit.outliner", new Outliner.JSOutliner(),
{ contentType: ["application/javascript"],
name: "Source outline",
+ title: "JavaScript source outline",
id: "orion.javascript.outliner.source"
- });*/
-
+ });
+
/**
* Register the AST provider
*/
provider.registerService("orion.core.astprovider",
{ computeAST: function(context) {
var ast = esprima.parse(context.text, {
- loc: true,
range: true,
- raw: true,
- tokens: true,
+ tolerant: true,
comment: true,
- tolerant: true
+ loc: true,
+ tokens: true
});
if (ast.errors) {
ast.errors = ast.errors.map(Serialize.serializeError);
diff --git a/bundles/org.eclipse.orion.client.javascript/web/javascript/signatures.js b/bundles/org.eclipse.orion.client.javascript/web/javascript/signatures.js
new file mode 100644
index 0000000..4e2e98e
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.javascript/web/javascript/signatures.js
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2013 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+/*global define*/
+define([
+], function() {
+
+ var Signatures = {
+
+ /**
+ * @name computeSignature
+ * @description Computes a signature object from the given AST node. The object holds two properties:
+ * <code>sig</code> - the human readable signature and <code>range</code>
+ * @function
+ * @public
+ * @memberof javascript.Signatures.prototype
+ * @param {Object} astnode The AST node to parse and compute the signature from
+ * @returns {Object} The computed signature object or <code>null</code> if the computation fails
+ */
+ computeSignature: function(astnode) {
+ if(astnode) {
+ var val = this.getNameFrom(astnode);
+ return {
+ sig: val,
+ range: this.getSignatureSourceRangeFrom(astnode)
+ };
+ }
+ return null;
+ },
+
+ /**
+ * @name getParamsFrom
+ * @description Retrieves the parameters from the given AST node iff it a function declaration. If there is an attached doc node
+ * it will be consulted to help compute the types of the parameters
+ * @function
+ * @public
+ * @memberof javascript.Signatures.prototype
+ * @param {Object} astnode The AST node to compute the parameters from
+ * @returns {Array} An array of parameter names suitable for display, in the order they are defined in source. If no parameters
+ * can be computed an empty array is returned, never <code>null</code>
+ */
+ getParamsFrom: function(astnode) {
+ if(astnode) {
+ var params = astnode.params;
+ //TODO with the attached doc node we can augment this infos
+ if(params && params.length > 0) {
+ var length = params.length;
+ var value = '';
+ for(var i = 0; i < length; i++) {
+ if(params[i].name) {
+ value += params[i].name;
+ }
+ else {
+ value += 'Object';
+ }
+ if(i < length -1) {
+ value += ', ';
+ }
+ }
+ return value;
+ }
+ }
+ },
+
+ /**
+ * @name getNameFrom
+ * @description Returns the name to display for the given AST node. If there is an attached doc node it
+ * will be consulted to help compute the name to display
+ * @function
+ * @public
+ * @memberof javascript.Signatures.prototype
+ * @param {Object} astnode The AST node to compute the name from
+ * @returns {String} The computed name to display for the node or <code>null</code> if one could not be computed
+ */
+ getNameFrom: function(astnode) {
+ var name = "Anonyous " + astnode.type;
+ if(astnode && astnode.type) {
+ if(astnode.type === 'FunctionDeclaration') {
+ //TODO with the attached doc node we can augment this infos
+ if(astnode.id && astnode.id.name) {
+ name = astnode.id.name+'(';
+ var fparams = this.getParamsFrom(astnode);
+ if(fparams) {
+ name += fparams;
+ }
+ name += ')';
+ return name;
+ }
+ }
+ else if(astnode.type === 'FunctionExpression') {
+ name = 'function(';
+ var feparams = this.getParamsFrom(astnode);
+ if(feparams) {
+ name += feparams;
+ }
+ name += ')';
+ return name;
+ }
+ else if(astnode.type === 'ObjectExpression') {
+ name = 'closure {...}';
+ }
+ else if(astnode.type === 'Property') {
+ if(astnode.value) {
+ if(astnode.value.type === 'FunctionExpression') {
+ if(astnode.key && astnode.key.name) {
+ name = astnode.key.name + '(';
+ }
+ else {
+ name = 'function(';
+ }
+ var pparams = this.getParamsFrom(astnode.value);
+ if(pparams) {
+ name += pparams;
+ }
+ name += ')';
+ }
+ else if(astnode.value.type === 'ObjectExpression') {
+ if(astnode.key && astnode.key.name) {
+ name = astnode.key.name + ' {...}';
+ }
+ }
+ else if(astnode.key && astnode.key.name) {
+ name = astnode.key.name;
+ }
+ }
+ }
+ else if(astnode.type === 'VariableDeclarator') {
+ if(astnode.init) {
+ if(astnode.init.type === 'ObjectExpression') {
+ if(astnode.id && astnode.id.name) {
+ name = 'var '+astnode.id.name+ ' = {...}';
+ }
+ }
+ else if(astnode.init.type === 'FunctionDeclaration') {
+ if(astnode.id && astnode.id.name) {
+ name = this.getNameFrom(astnode.init);
+ }
+ }
+ }
+ }
+ }
+ return name;
+ },
+
+ /**
+ * @name getSignatureSourceRangeFrom
+ * @description Computes the signature source range (start, end) for the given node
+ * @function
+ * @ppublic
+ * @memberof javascript.Signatures.prototype
+ * @param {Object} astnode The AST node to compute the range from
+ * @returns {Array} The computed signature source range as an array [start, end] or <code>[-1, -1]</code> if it could not
+ * be computed
+ */
+ getSignatureSourceRangeFrom: function(astnode) {
+ var range = [0, 0];
+ if(astnode) {
+ if(astnode.type === 'Property') {
+ if(astnode.key && astnode.key.range) {
+ range = astnode.key.range;
+ }
+ }
+ else if(astnode.id && astnode.id.range) {
+ range = astnode.id.range;
+ }
+ else if(astnode.range) {
+ range = astnode.range;
+ if(astnode.type === 'FunctionExpression') {
+ range[1] = range[0]+8;
+ }
+ }
+ if(range[0] < 1) {
+ //TODO hack since passing in a range starting with 0 causes no selection to be made
+ range[0] = 1;
+ }
+ }
+ return range;
+ }
+
+ };
+
+ return Signatures;
+}); \ No newline at end of file