/**
* Aptana Studio
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
* Please see the license.html included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ruby.internal.core.codeassist;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.Colon2MethodNode;
import org.jrubyparser.ast.Colon2Node;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.StrNode;
import org.jrubyparser.lexer.SyntaxException;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.IOUtil;
import com.aptana.core.util.StringUtil;
import com.aptana.index.core.Index;
import com.aptana.index.core.QueryResult;
import com.aptana.index.core.SearchPattern;
import com.aptana.parsing.ParserPoolFactory;
import com.aptana.parsing.lexer.IRange;
import com.aptana.parsing.lexer.Range;
import com.aptana.ruby.core.IRubyConstants;
import com.aptana.ruby.core.IRubyElement;
import com.aptana.ruby.core.RubyCorePlugin;
import com.aptana.ruby.core.ast.ASTUtils;
import com.aptana.ruby.core.ast.ClosestSpanningNodeLocator;
import com.aptana.ruby.core.ast.FirstPrecursorNodeLocator;
import com.aptana.ruby.core.ast.INodeAcceptor;
import com.aptana.ruby.core.ast.NamespaceVisitor;
import com.aptana.ruby.core.codeassist.CodeResolver;
import com.aptana.ruby.core.codeassist.ResolutionTarget;
import com.aptana.ruby.core.codeassist.ResolveContext;
import com.aptana.ruby.core.index.IRubyIndexConstants;
import com.aptana.ruby.core.index.RubyIndexUtil;
import com.aptana.ruby.core.inference.ITypeGuess;
import com.aptana.ruby.internal.core.NamedMember;
import com.aptana.ruby.internal.core.RubyScript;
import com.aptana.ruby.internal.core.inference.TypeInferrer;
import com.aptana.ruby.launching.RubyLaunchingPlugin;
public class RubyCodeResolver extends CodeResolver
{
private static final String RB_SUFFIX = ".rb"; //$NON-NLS-1$
private static final String LOAD = "load"; //$NON-NLS-1$
private static final String REQUIRE = "require"; //$NON-NLS-1$
private static final String NAMESPACE_DELIMITER = "::"; //$NON-NLS-1$
private ResolveContext fContext;
@Override
public void resolve(ResolveContext context)
{
this.fContext = context;
try
{
Node atOffset = context.getSelectedNode();
if (atOffset == null)
{
return;
}
switch (atOffset.getNodeType())
{
case VCALLNODE:
case FCALLNODE:
addAll(noReceiverMethodCallLink((INameNode) atOffset));
break;
case CALLNODE:
addAll(methodCallLink((CallNode) atOffset));
break;
case COLON3NODE:
case CONSTNODE:
addAll(constNode(atOffset));
break;
case COLON2NODE:
addAll(typeName((Colon2Node) atOffset));
break;
case LOCALVARNODE:
addAll(localVariableDeclaration(atOffset));
break;
case INSTVARNODE:
addAll(instanceVariableDeclaration(atOffset));
break;
case CLASSVARNODE:
addAll(classVariableDeclaration(atOffset));
break;
case STRNODE:
addAll(requireOrLoad(atOffset));
break;
default:
// System.out.println(atOffset);
break;
}
}
catch (SyntaxException se)
{
// ignore
}
finally
{
this.fContext = null;
}
}
private void addAll(Collection<ResolutionTarget> targets)
{
if (targets != null)
{
fContext.addResolved(targets);
}
}
private Collection<ResolutionTarget> noReceiverMethodCallLink(INameNode atOffset)
{
String methodName = atOffset.getName();
// TODO Try and infer the type of "self" and search up the hierarchy for matching methods first before we do a
// global search!
return findMethods(methodName);
}
/**
* Generate a hyperlink to the first preceding assignment to the same local variable.
*
* @param atOffset
* @return
*/
private Collection<ResolutionTarget> localVariableDeclaration(Node atOffset)
{
return variableDeclaration(atOffset, NodeType.LOCALASGNNODE);
}
/**
* Generate a hyperlink to the first preceding assignment to the same instance variable.
*
* @param atOffset
* @return
*/
private Collection<ResolutionTarget> instanceVariableDeclaration(Node atOffset)
{
return variableDeclaration(atOffset, NodeType.INSTASGNNODE);
}
/**
* Generate a hyperlink to the first preceding assignment to the same class variable.
*
* @param atOffset
* @return
*/
private Collection<ResolutionTarget> classVariableDeclaration(Node atOffset)
{
return variableDeclaration(atOffset, NodeType.CLASSVARASGNNODE);
}
/**
* Common code for finding first preceding assignment to a variable.
*
* @param atOffset
* @param nodeType
* @return
*/
private Collection<ResolutionTarget> variableDeclaration(Node atOffset, final NodeType nodeType)
{
Node decl = new FirstPrecursorNodeLocator().find(getRoot(), atOffset.getPosition().getStartOffset() - 1,
new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node.getNodeType() == nodeType;
}
});
if (decl != null)
{
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
links.add(new ResolutionTarget(this.fContext.getURI(), new Range(decl.getPosition().getStartOffset(), decl
.getPosition().getEndOffset())));
return links;
}
return Collections.emptyList();
}
/**
* Generate hyperlinks for a constant reference. Could be referring to a constant that is declared, or to a type
* name.
*
* @param atOffset
* @return
*/
private Collection<ResolutionTarget> constNode(Node atOffset)
{
String namespace = new NamespaceVisitor().getNamespace(getRoot(), atOffset.getPosition().getStartOffset());
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
String constantName = ((INameNode) atOffset).getName();
links.addAll(findConstant(constantName));
if (namespace.length() > 0)
{
constantName = namespace + NAMESPACE_DELIMITER + constantName;
}
links.addAll(findType(constantName));
return links;
}
private Collection<ResolutionTarget> typeName(Colon2Node atOffset)
{
if (atOffset instanceof Colon2MethodNode)
{
return noReceiverMethodCallLink(atOffset);
}
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
String fullyQualifiedTypeName = ASTUtils.getFullyQualifiedName(atOffset);
links.addAll(findType(fullyQualifiedTypeName));
return links;
}
/**
* Given a constant name, search the project index for declarations of constants matching that name and then
* generate hyperlinks to those declarations.
*
* @param constantName
* @return
*/
private Collection<ResolutionTarget> findConstant(String constantName)
{
Index index = getIndex();
if (index == null)
{
return Collections.emptyList();
}
// TODO Search AST in current file first?
List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.CONSTANT_DECL }, constantName
+ IRubyIndexConstants.SEPARATOR, SearchPattern.PREFIX_MATCH | SearchPattern.CASE_SENSITIVE);
return getMatchingElementHyperlinks(results, constantName, IRubyElement.CONSTANT);
}
private Index getIndex()
{
return RubyIndexUtil.getIndex(getProject());
}
/**
* Offset and length in destination file of hyperlink. Point to name region for type/method declarations.
*
* @param p
* @return
*/
private IRange createRange(IRubyElement p)
{
if (p instanceof NamedMember)
{
NamedMember nm = (NamedMember) p;
return nm.getNameNode().getNameRange();
}
return new Range(p.getStartingOffset(), p.getEndingOffset());
}
/*
* doc is an URI, parse it and traverse the AST to find the constant!
*/
private RubyScript parseURI(String doc)
{
try
{
IFileStore store = EFS.getStore(URI.create(doc));
return (RubyScript) ParserPoolFactory.parse(IRubyConstants.CONTENT_TYPE_RUBY,
IOUtil.read(store.openInputStream(EFS.NONE, new NullProgressMonitor()))).getRootNode();
}
catch (Exception e)
{
IdeLog.logError(RubyCorePlugin.getDefault(), e);
}
return null;
}
private Collection<ResolutionTarget> methodCallLink(CallNode callNode)
{
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
String methodName = callNode.getName();
if ("new".equals(methodName)) //$NON-NLS-1$
{
Node receiver = callNode.getReceiverNode();
Collection<ITypeGuess> guesses = new TypeInferrer(getProject()).infer(getRoot(), receiver);
for (ITypeGuess guess : guesses)
{
// TODO Find the "initialize" sub-method of the type if it exists!
links.addAll(findType(guess.getType()));
}
}
else
{
links.addAll(findMethods(methodName));
}
return links;
}
private Node getRoot() throws SyntaxException
{
return fContext.getAST();
}
private Collection<ResolutionTarget> findMethods(String methodName)
{
// TODO Handle narrowing by type...
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
// Search all indices
for (Index index : RubyIndexUtil.allIndices(getProject()))
{
if (index == null)
{
continue;
}
List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.METHOD_DECL }, methodName
+ IRubyIndexConstants.SEPARATOR, SearchPattern.PREFIX_MATCH | SearchPattern.CASE_SENSITIVE);
links.addAll(getMatchingElementHyperlinks(results, methodName, IRubyElement.METHOD));
}
return links;
}
private Collection<ResolutionTarget> findType(String typeName)
{
// Handle qualified type name!
String namespace = StringUtil.EMPTY;
int separatorIndex = typeName.indexOf(NAMESPACE_DELIMITER);
if (separatorIndex != -1)
{
namespace = typeName.substring(0, separatorIndex);
typeName = typeName.substring(separatorIndex + 2);
}
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
// Search all indices
for (Index index : RubyIndexUtil.allIndices(getProject()))
{
if (index == null)
{
continue;
}
List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.TYPE_DECL }, typeName
+ IRubyIndexConstants.SEPARATOR + namespace + IRubyIndexConstants.SEPARATOR,
SearchPattern.PREFIX_MATCH | SearchPattern.CASE_SENSITIVE);
// TODO Exit early if we find matches?
// FIXME Sort by a priority. We should prefer filenames that match the type name, parent folders
// matching parent namespaces.
links.addAll(getMatchingElementHyperlinks(results, typeName, IRubyElement.TYPE));
}
return links;
}
protected IProject getProject()
{
URI uri = fContext.getURI();
if ("file".equals(uri.getScheme())) //$NON-NLS-1$
{
IPath path = Path.fromOSString(uri.getPath());
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path);
if (file != null)
{
return file.getProject();
}
}
return null;
}
private Collection<ResolutionTarget> getMatchingElementHyperlinks(List<QueryResult> results, String elementName,
int elementType)
{
if (results == null)
{
return Collections.emptyList();
}
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
for (QueryResult result : results)
{
Set<String> docs = result.getDocuments();
for (String doc : docs)
{
RubyScript root = parseURI(doc);
if (root != null)
{
List<IRubyElement> possible = root.getChildrenOfTypeRecursive(elementType);
for (IRubyElement p : possible)
{
if (elementName.equals(p.getName()))
{
links.add(new ResolutionTarget(URI.create(doc), createRange(p)));
}
}
}
}
}
return links;
}
/**
* Generate hyperlinks for a require/load string.
*
* @param atOffset
* @return
*/
private Collection<ResolutionTarget> requireOrLoad(Node atOffset)
{
// First check if we're actually calling require or load!
Node spanningMethod = new ClosestSpanningNodeLocator().find(getRoot(), atOffset.getPosition().getStartOffset(),
new INodeAcceptor()
{
public boolean accepts(Node node)
{
return NodeType.FCALLNODE == node.getNodeType();
}
});
if (spanningMethod == null)
{
return Collections.emptyList();
}
String methodName = ((FCallNode) spanningMethod).getName();
if (!REQUIRE.equals(methodName) && !LOAD.equals(methodName))
{
return Collections.emptyList();
}
// OK, string is argument to require/load. Let's resolve it to a file...
StrNode string = (StrNode) atOffset;
String value = string.getValue();
if (!value.endsWith(RB_SUFFIX))
{
value = value + RB_SUFFIX;
}
List<ResolutionTarget> links = new ArrayList<ResolutionTarget>();
// Grab the list of loadpaths and search them for the relative path in the require!
for (IPath path : RubyLaunchingPlugin.getLoadpaths(getProject()))
{
IPath possible = path.append(value);
if (possible.toFile().exists())
{
links.add(new ResolutionTarget(possible.toFile().toURI(), new Range(0, 0)));
return links;
}
}
// Not on our normal loadpath, try to see if it's inside one of the gems...
for (IPath path : RubyLaunchingPlugin.getGemPaths(getProject()))
{
// FIXME Can't we shortcut by matching up the first portion of path to gem name somehow?
for (File gemDir : path.toFile().listFiles())
{
IPath possible = Path.fromOSString(gemDir.getAbsolutePath()).append("lib").append(value); //$NON-NLS-1$
if (possible.toFile().exists())
{
links.add(new ResolutionTarget(possible.toFile().toURI(), new Range(0, 0)));
}
}
}
return links;
}
}