/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.js.outline;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.ui.IEditorInput;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.editor.js.JSPlugin;
import com.aptana.ide.editor.js.parsing.nodes.JSParseNodeTypes;
import com.aptana.ide.parsing.nodes.IParseNode;
/**
* @author Kevin Lindsey
*/
public class JSContentProvider implements ITreeContentProvider
{
private static final String CONTAINER_TYPE = "/"; //$NON-NLS-1$
private static final String PROPERTY_TYPE = "."; //$NON-NLS-1$
private static final Set<String> CLASS_EXTENDERS;
private Map<String,JSOutlineItem> _itemsByScope;
/**
* static constructor
*/
static
{
CLASS_EXTENDERS = new HashSet<String>();
CLASS_EXTENDERS.add("dojo.lang.extend"); //$NON-NLS-1$
CLASS_EXTENDERS.add("Ext.extend"); //$NON-NLS-1$
CLASS_EXTENDERS.add("jQuery.extend"); //$NON-NLS-1$
CLASS_EXTENDERS.add("MochiKit.Base.update"); //$NON-NLS-1$
CLASS_EXTENDERS.add("Object.extend"); //$NON-NLS-1$
CLASS_EXTENDERS.add("qx.Class.define"); //$NON-NLS-1$
CLASS_EXTENDERS.add("qx.Interface.define"); //$NON-NLS-1$
CLASS_EXTENDERS.add("qx.Theme.define"); //$NON-NLS-1$
CLASS_EXTENDERS.add("qx.Mixin.define"); //$NON-NLS-1$
}
/**
* JSContentProvider
*/
public JSContentProvider()
{
this._itemsByScope = new HashMap<String,JSOutlineItem>();
}
/**
* addValue
*
* @param reference
* @param value
* @param parent
*/
private void addValue(List<JSOutlineItem> elements, Reference reference, IParseNode value)
{
this.addValue(elements, reference, value, null);
}
/**
* addValue
*
* @param reference
* @param value
* @param parent
*/
private void addValue(List<JSOutlineItem> elements, Reference reference, IParseNode value, IParseNode parent)
{
boolean processed = false;
switch (value.getTypeIndex())
{
case JSParseNodeTypes.FUNCTION:
this.processFunction(elements, value, reference);
processed = true;
break;
case JSParseNodeTypes.INVOKE:
IParseNode child = value.getChild(0);
if (child.getTypeIndex() == JSParseNodeTypes.FUNCTION)
{
this.processFunction(elements, child, reference);
processed = true;
}
else
{
value = child;
}
break;
default:
break;
}
if (processed == false)
{
int type = this.getOutlineType(value);
int count = 0;
if (value.getTypeIndex() == JSParseNodeTypes.OBJECT_LITERAL)
{
count = value.getChildCount();
}
if (parent == null)
{
parent = value.getParent();
}
// keep track of this item's scope so we can add virtual children later, if needed
String path = reference.toString();
if (this._itemsByScope.containsKey(path) == false)
{
JSOutlineItem item = new JSOutlineItem(reference.getName(), type, parent, value, count);
this._itemsByScope.put(path, item);
elements.add(item);
}
}
}
/**
* addVirtualChild
*
* @param elements
* @param reference
* @param node
* @param target
*/
private void addVirtualChild(List<JSOutlineItem> elements, Reference reference, IParseNode node, IParseNode target)
{
String key = reference.getScope();
JSOutlineItem item;
if (this._itemsByScope.containsKey(key))
{
item = this._itemsByScope.get(key);
}
else
{
// get outline node type
int type = (node.getTypeIndex() == JSParseNodeTypes.FUNCTION) ? JSOutlineItemType.FUNCTION
: JSOutlineItemType.PROPERTY;
// create outline item
item = new JSOutlineItem(node.getText(), type, node, node);
// cache associated by scope
this._itemsByScope.put(key, item);
// add item to our result list
elements.add(item);
}
item.addVirtualChild(target);
}
/**
* @see org.eclipse.jface.viewers.IContentProvider#dispose()
*/
public void dispose()
{
}
/**
* getChildCount
*
* @param node
* @return child count
*/
private int getChildCount(IParseNode node)
{
int result = 0;
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
switch (child.getTypeIndex())
{
case JSParseNodeTypes.ASSIGN:
IParseNode lhs = child.getChild(0);
IParseNode rhs = child.getChild(1);
int lhsTypeIndex = lhs.getTypeIndex();
int rhsTypeIndex = rhs.getTypeIndex();
boolean identifierOrProperty = (lhsTypeIndex == JSParseNodeTypes.IDENTIFIER || lhsTypeIndex == JSParseNodeTypes.GET_PROPERTY);
boolean ofInterest = (rhsTypeIndex == JSParseNodeTypes.FUNCTION
|| rhsTypeIndex == JSParseNodeTypes.OBJECT_LITERAL || rhsTypeIndex == JSParseNodeTypes.INVOKE);
// if (identifierOrProperty) // && rhs.getTypeIndex() == JSParseNodeTypes.FUNCTION)
if (identifierOrProperty && ofInterest)
{
result++;
}
break;
case JSParseNodeTypes.FUNCTION:
case JSParseNodeTypes.VAR:
result++;
break;
case JSParseNodeTypes.IF:
case JSParseNodeTypes.TRY:
case JSParseNodeTypes.CATCH:
case JSParseNodeTypes.STATEMENTS:
result += this.getChildCount(child);
break;
case JSParseNodeTypes.RETURN:
if (child.getChildCount() > 0)
{
IParseNode grandchild = child.getChild(0);
if (grandchild.getTypeIndex() == JSParseNodeTypes.OBJECT_LITERAL)
{
result++;
}
}
break;
case JSParseNodeTypes.INVOKE:
if (child.getChildCount() > 0)
{
IParseNode grandchild = child.getChild(0);
if (grandchild.getTypeIndex() == JSParseNodeTypes.FUNCTION)
{
result++;
}
}
break;
default:
break;
}
}
return result;
}
/**
* @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
*/
public Object[] getChildren(Object parentElement)
{
Object[] result = null;
if (parentElement instanceof JSOutlineItem)
{
JSOutlineItem item = (JSOutlineItem) parentElement;
Object[] elements = this.getElements(item.getReferenceNodes());
IEditorInput input=item.getEditorInput();
if (input != null)
{
for (int a = 0; a < elements.length; a++)
{
Object object = elements[a];
// patching path if needed setting it from parent
if (object instanceof JSOutlineItem)
{
JSOutlineItem element = (JSOutlineItem) object;
if (element.getEditorInput() == null)
{
element.setResolveInformation(input);
element.setParent(item);
}
}
}
}
result = elements;
}
return result;
}
/**
* @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
*/
public Object[] getElements(Object inputElement)
{
List<JSOutlineItem> elements = new ArrayList<JSOutlineItem>();
if (inputElement instanceof IParseNode[])
{
IParseNode[] nodes = (IParseNode[]) inputElement;
for (int i = 0; i < nodes.length; i++)
{
this.processNode(elements, nodes[i]);
}
}
else if (inputElement instanceof IParseNode)
{
this._itemsByScope.clear();
// process node
this.processNode(elements, (IParseNode) inputElement);
}
return elements.toArray(new Object[elements.size()]);
}
/**
* getOutlineType
*
* @param node
* @return
*/
private int getOutlineType(IParseNode node)
{
int result;
switch (node.getTypeIndex())
{
case JSParseNodeTypes.ARRAY_LITERAL:
result = JSOutlineItemType.ARRAY;
break;
case JSParseNodeTypes.TRUE:
case JSParseNodeTypes.FALSE:
result = JSOutlineItemType.BOOLEAN;
break;
case JSParseNodeTypes.FUNCTION:
result = JSOutlineItemType.FUNCTION;
break;
case JSParseNodeTypes.NULL:
result = JSOutlineItemType.NULL;
break;
case JSParseNodeTypes.NUMBER:
result = JSOutlineItemType.NUMBER;
break;
case JSParseNodeTypes.OBJECT_LITERAL:
result = JSOutlineItemType.OBJECT_LITERAL;
break;
case JSParseNodeTypes.REGULAR_EXPRESSION:
result = JSOutlineItemType.REGEX;
break;
case JSParseNodeTypes.STRING:
result = JSOutlineItemType.STRING;
break;
default:
result = JSOutlineItemType.PROPERTY;
break;
}
return result;
}
/**
* @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
*/
public Object getParent(Object element)
{
Object result = null;
if (element instanceof JSOutlineItem)
{
JSOutlineItem item = (JSOutlineItem) element;
result = item.getReferenceNode().getParent();
}
return result;
}
/**
* @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
*/
public boolean hasChildren(Object element)
{
boolean result = false;
if (element instanceof JSOutlineItem)
{
JSOutlineItem item = (JSOutlineItem) element;
result = (item.getChildCount() > 0);
}
return result;
}
/**
* @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object,
* java.lang.Object)
*/
public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
{
}
/**
* processAssignNode
*
* @param elements
* @param child
*/
private void processAssignment(List<JSOutlineItem> elements, IParseNode lhs, IParseNode rhs)
{
int rhsTypeIndex = rhs.getTypeIndex();
switch (lhs.getTypeIndex())
{
case JSParseNodeTypes.STRING:
if (rhsTypeIndex == JSParseNodeTypes.FUNCTION || rhsTypeIndex == JSParseNodeTypes.OBJECT_LITERAL)
{
String text = lhs.getSource();
Reference reference = new Reference(lhs.getParent(), text.substring(1, text.length() - 1),
CONTAINER_TYPE);
this.addValue(elements, reference, rhs);
}
break;
case JSParseNodeTypes.IDENTIFIER:
if (rhsTypeIndex == JSParseNodeTypes.FUNCTION || rhsTypeIndex == JSParseNodeTypes.OBJECT_LITERAL)
{
Reference reference = new Reference(lhs.getParent(), lhs.getText(), CONTAINER_TYPE);
this.addValue(elements, reference, rhs);
}
else if (rhsTypeIndex == JSParseNodeTypes.INVOKE && rhs.getChildCount() == 2)
{
IParseNode child = rhs.getChild(0);
Reference reference = new Reference(lhs.getParent(), lhs.getText(), CONTAINER_TYPE);
this.addValue(elements, reference, child);
}
break;
case JSParseNodeTypes.GET_PROPERTY:
IParseNode target = null;
// travel down the left-side get-property nodes
while (lhs.getTypeIndex() == JSParseNodeTypes.GET_PROPERTY)
{
target = lhs.getChild(1);
lhs = lhs.getChild(0);
}
// only process get-property expressions that begin with an identifier or 'this'
if (lhs.getTypeIndex() == JSParseNodeTypes.IDENTIFIER || lhs.getTypeIndex() == JSParseNodeTypes.THIS)
{
String scopeString = Reference.createScopeString(lhs.getParent());
Reference reference;
if (this._itemsByScope.containsKey(scopeString))
{
reference = new Reference(scopeString, target.getText(), CONTAINER_TYPE);
addVirtualChild(elements, reference, lhs, target);
}
else
{
IParseNode node = lhs;
reference = new Reference(node, lhs.getText(), CONTAINER_TYPE);
this.addValue(elements, reference, target);
JSOutlineItem item = this._itemsByScope.get(scopeString);
item.addVirtualChild(target);
}
}
break;
default:
break;
}
}
/**
* processFunctionNode
*
* @param elements
* @param node
*/
private void processFunction(List<JSOutlineItem> elements, IParseNode node, Reference reference)
{
IParseNode parameters = node.getChild(0);
IParseNode body = node.getChild(1);
String name;
if (node.hasAttribute("name")) //$NON-NLS-1$
{
name = node.getAttribute("name"); //$NON-NLS-1$
}
else
{
if (reference != null)
{
name = reference.getName();
}
else
{
name = "<literal>"; //$NON-NLS-1$
}
}
String label = name + "(" + parameters.getSource() + ")"; //$NON-NLS-1$ //$NON-NLS-2$
int count = this.getChildCount(body);
// keep track of this item's scope so we can add virtual children later, if needed
if (reference == null)
{
reference = new Reference(node, name, CONTAINER_TYPE);
}
String fullpath = reference.toString();
if (this._itemsByScope.containsKey(fullpath) == false)
{
JSOutlineItem item = new JSOutlineItem(label, JSOutlineItemType.FUNCTION, node, body, count);
this._itemsByScope.put(fullpath, item);
elements.add(item);
}
else
{
}
}
/**
* processIdentifier
*
* @param elements
* @param node
*/
private void processIdentifier(List<JSOutlineItem> elements, IParseNode node)
{
IParseNode parent = node.getParent();
if (parent.hasChildren())
{
IParseNode rhs = parent.getChild(1);
if (rhs == node)
{
// get grandparent
IParseNode grandparent = parent.getParent();
Reference reference;
if (grandparent != null)
{
IParseNode target = grandparent.getChild(1);
switch (grandparent.getTypeIndex())
{
case JSParseNodeTypes.ARGUMENTS:
// support dojo.lang.extend, MochiKit.Base.update, and Object.extend
target = grandparent.getChild(grandparent.getChildCount() - 1);
reference = new Reference(parent, rhs.getText(), PROPERTY_TYPE);
String parentFullPath = reference.toString();
// process all key/value pairs
for (int i = 0; i < target.getChildCount(); i++)
{
IParseNode keyValuePair = target.getChild(i);
String keyString = keyValuePair.getChild(0).getSource();
Reference keyValueReference = new Reference(parentFullPath, keyString, PROPERTY_TYPE);
this.addVirtualChild(elements, keyValueReference, node, keyValuePair);
}
break;
case JSParseNodeTypes.ASSIGN:
reference = new Reference(parent, rhs.getText(), PROPERTY_TYPE);
this.addValue(elements, reference, target);
break;
case JSParseNodeTypes.GET_PROPERTY:
IParseNode property = grandparent.getChild(1);
reference = new Reference(grandparent, property.getText(), PROPERTY_TYPE);
this.addVirtualChild(elements, reference, node, target);
break;
default:
break;
}
}
}
}
}
/**
* processInvoke
*
* @param elements
* @param node
*/
private void processInvoke(List<JSOutlineItem> elements, IParseNode node)
{
IParseNode lhs = node.getChild(0);
String source = lhs.getSource();
if (CLASS_EXTENDERS.contains(source))
{
IParseNode args = node.getChild(1);
if (args.getTypeIndex() == JSParseNodeTypes.ARGUMENTS && args.getChildCount() == 2)
{
IParseNode arg1 = args.getChild(0);
IParseNode arg2 = args.getChild(1);
if (arg2.getTypeIndex() == JSParseNodeTypes.OBJECT_LITERAL)
{
switch (arg1.getTypeIndex())
{
case JSParseNodeTypes.STRING:
case JSParseNodeTypes.IDENTIFIER:
case JSParseNodeTypes.GET_PROPERTY:
this.processAssignment(elements, arg1, arg2);
break;
default:
break;
}
}
}
else if (args.getTypeIndex() == JSParseNodeTypes.ARGUMENTS && args.getChildCount() == 3)
{
// EXT case
IParseNode arg1 = args.getChild(0);
IParseNode arg3 = args.getChild(2);
if (arg3.getTypeIndex() == JSParseNodeTypes.OBJECT_LITERAL)
{
switch (arg1.getTypeIndex())
{
case JSParseNodeTypes.STRING:
case JSParseNodeTypes.IDENTIFIER:
case JSParseNodeTypes.GET_PROPERTY:
this.processAssignment(elements, arg1, arg3);
break;
default:
break;
}
}
}
}
else if (lhs.getTypeIndex() == JSParseNodeTypes.GROUP)
{
// See if we are in a self-invoking function
IParseNode[] nodes = lhs.getChildren();
for (int i = 0; i < nodes.length; i++)
{
IParseNode node2 = nodes[i];
if (node2.getTypeIndex() == JSParseNodeTypes.FUNCTION && !node.hasAttribute("name")) //$NON-NLS-1$
{
IParseNode[] grandChildren = node2.getChildren();
for (int j = 0; j < grandChildren.length; j++)
{
IParseNode node3 = grandChildren[j];
this.processNode(elements, node3);
}
}
else
{
this.processNode(elements, node2);
}
}
}
}
/**
* processNameValuePair
*
* @param elements
* @param node
*/
private void processNameValuePair(List<JSOutlineItem> elements, IParseNode node)
{
IParseNode property = node.getChild(0);
IParseNode value = node.getChild(1);
String name = property.getText();
int type = this.getOutlineType(value);
if (property.getTypeIndex() == JSParseNodeTypes.STRING)
{
name = name.substring(1, name.length());
}
switch (value.getTypeIndex())
{
case JSParseNodeTypes.FUNCTION:
this.processFunction(elements, value, new Reference(value, name, "")); //$NON-NLS-1$
break;
case JSParseNodeTypes.OBJECT_LITERAL:
elements.add(new JSOutlineItem(name, type, node, value, value.getChildCount()));
break;
default:
elements.add(new JSOutlineItem(name, type, node, value));
break;
}
}
/**
* processNode
*
* @param elements
* @param node
*/
private void processNode(List<JSOutlineItem> elements, IParseNode node)
{
try
{
switch (node.getTypeIndex())
{
case JSParseNodeTypes.ASSIGN:
this.processAssignment(elements, node.getChild(0), node.getChild(1));
break;
case JSParseNodeTypes.FUNCTION:
this.processFunction(elements, node, null);
break;
case JSParseNodeTypes.GROUP:
if (node.getChildCount() > 0)
{
this.processNode(elements, node.getChild(0));
}
break;
case JSParseNodeTypes.IDENTIFIER:
this.processIdentifier(elements, node);
break;
case JSParseNodeTypes.INVOKE:
this.processInvoke(elements, node);
break;
case JSParseNodeTypes.NAME_VALUE_PAIR:
this.processNameValuePair(elements, node);
break;
case JSParseNodeTypes.OBJECT_LITERAL:
for (int i = 0; i < node.getChildCount(); i++)
{
this.processNode(elements, node.getChild(i));
}
break;
case JSParseNodeTypes.RETURN:
if (node.getChildCount() > 0)
{
IParseNode child = node.getChild(0);
if (child.getTypeIndex() == JSParseNodeTypes.OBJECT_LITERAL)
{
for (int i = 0; i < child.getChildCount(); i++)
{
this.processNode(elements, child.getChild(i));
}
}
}
break;
case JSParseNodeTypes.STATEMENTS:
// process named functions first
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
if (child.getTypeIndex() == JSParseNodeTypes.FUNCTION && child.hasAttribute("name")) //$NON-NLS-1$
{
this.processNode(elements, child);
}
}
// process var declarations
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
if (child.getTypeIndex() == JSParseNodeTypes.VAR)
{
this.processNode(elements, child);
}
}
// process var assignments, identifiers, and name/value pairs
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
int typeIndex = child.getTypeIndex();
if (typeIndex == JSParseNodeTypes.ASSIGN || typeIndex == JSParseNodeTypes.IDENTIFIER
|| typeIndex == JSParseNodeTypes.NAME_VALUE_PAIR
|| typeIndex == JSParseNodeTypes.INVOKE || typeIndex == JSParseNodeTypes.RETURN)
{
this.processNode(elements, child);
}
}
// process if statements
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
int typeIndex = child.getTypeIndex();
if (typeIndex == JSParseNodeTypes.IF)
{
this.processNode(elements, child);
}
}
// process try/catch statements
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
int typeIndex = child.getTypeIndex();
if (typeIndex == JSParseNodeTypes.TRY || typeIndex == JSParseNodeTypes.CATCH)
{
this.processNode(elements, child);
}
}
break;
case JSParseNodeTypes.IF:
case JSParseNodeTypes.TRY:
case JSParseNodeTypes.CATCH:
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode child = node.getChild(i);
this.processNode(elements, child);
}
case JSParseNodeTypes.THIS:
this.processIdentifier(elements, node);
break;
case JSParseNodeTypes.VAR:
this.processVar(elements, node);
break;
default:
break;
}
}
catch (Exception e)
{
IdeLog.logError(JSPlugin.getDefault(), Messages.JSContentProvider_Error_Processing_Parse_Node, e);
}
}
/**
* processVarNode
*
* @param elements
* @param node
*/
private void processVar(List<JSOutlineItem> elements, IParseNode node)
{
// process all declarations
for (int i = 0; i < node.getChildCount(); i++)
{
IParseNode declaration = node.getChild(i);
IParseNode identifier = declaration.getChild(0);
IParseNode assignedValue = declaration.getChild(1);
if (assignedValue.getTypeIndex() != JSParseNodeTypes.EMPTY)
{
while (assignedValue.getTypeIndex() == JSParseNodeTypes.ASSIGN)
{
assignedValue = assignedValue.getChild(1);
}
}
Reference reference = new Reference(node, identifier.getText(), CONTAINER_TYPE);
this.addValue(elements, reference, assignedValue, node);
// elements.add(new JSOutlineItem(identifier.getText(), JSOutlineItemType.PROPERTY, identifier,
// assignedValue));
}
}
}