package org.rubypeople.rdt.internal.codeassist;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.jruby.ast.ClassNode;
import org.jruby.ast.ClassVarAsgnNode;
import org.jruby.ast.ClassVarDeclNode;
import org.jruby.ast.ClassVarNode;
import org.jruby.ast.Colon2Node;
import org.jruby.ast.ConstDeclNode;
import org.jruby.ast.ConstNode;
import org.jruby.ast.DefnNode;
import org.jruby.ast.DefsNode;
import org.jruby.ast.InstAsgnNode;
import org.jruby.ast.InstVarNode;
import org.jruby.ast.IterNode;
import org.jruby.ast.LocalAsgnNode;
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.MethodDefNode;
import org.jruby.ast.ModuleNode;
import org.jruby.ast.Node;
import org.jruby.ast.RootNode;
import org.jruby.ast.SelfNode;
import org.jruby.ast.YieldNode;
import org.jruby.ast.types.INameNode;
import org.jruby.lexer.yacc.SyntaxException;
import org.jruby.parser.StaticScope;
import org.rubypeople.rdt.core.CompletionProposal;
import org.rubypeople.rdt.core.CompletionRequestor;
import org.rubypeople.rdt.core.Flags;
import org.rubypeople.rdt.core.IMember;
import org.rubypeople.rdt.core.IMethod;
import org.rubypeople.rdt.core.IOpenable;
import org.rubypeople.rdt.core.IRubyElement;
import org.rubypeople.rdt.core.IRubyModel;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.IRubyScript;
import org.rubypeople.rdt.core.ISourceRange;
import org.rubypeople.rdt.core.IType;
import org.rubypeople.rdt.core.ITypeHierarchy;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.RubyModelException;
import org.rubypeople.rdt.core.search.CollectingSearchRequestor;
import org.rubypeople.rdt.core.search.IRubySearchConstants;
import org.rubypeople.rdt.core.search.IRubySearchScope;
import org.rubypeople.rdt.core.search.SearchMatch;
import org.rubypeople.rdt.core.search.SearchParticipant;
import org.rubypeople.rdt.core.search.SearchPattern;
import org.rubypeople.rdt.internal.core.LogicalType;
import org.rubypeople.rdt.internal.core.RubyConstant;
import org.rubypeople.rdt.internal.core.RubyElement;
import org.rubypeople.rdt.internal.core.RubyType;
import org.rubypeople.rdt.internal.core.SourceElementParser;
import org.rubypeople.rdt.internal.core.parser.InOrderVisitor;
import org.rubypeople.rdt.internal.core.parser.RubyParser;
import org.rubypeople.rdt.internal.core.search.BasicSearchEngine;
import org.rubypeople.rdt.internal.core.util.ASTUtil;
import org.rubypeople.rdt.internal.core.util.Util;
import org.rubypeople.rdt.internal.ti.BasicTypeGuess;
import org.rubypeople.rdt.internal.ti.ITypeGuess;
import org.rubypeople.rdt.internal.ti.ITypeInferrer;
import org.rubypeople.rdt.internal.ti.util.AttributeLocator;
import org.rubypeople.rdt.internal.ti.util.ClosestSpanningNodeLocator;
import org.rubypeople.rdt.internal.ti.util.INodeAcceptor;
import org.rubypeople.rdt.internal.ti.util.ScopedNodeLocator;
public class CompletionEngine
{
private static final String OBJECT = "Object";
private static final String CONSTRUCTOR_INVOKE_NAME = "new";
private static final String CONSTRUCTOR_DEFINITION_NAME = "initialize";
private CompletionRequestor fRequestor;
private CompletionContext fContext;
private Set<IType> fVisitedTypes;
/**
* temporary place to hold the original type we're completing for. Used to determine if we should be showing private
* methods.
*/
private IType fOriginalType;
public CompletionEngine(CompletionRequestor requestor)
{
this.fRequestor = requestor;
}
public void complete(IRubyScript script, int offset) throws RubyModelException
{
this.fRequestor.beginReporting();
fContext = new CompletionContext(script, offset);
if (fContext.inComment())
{
this.fRequestor.endReporting();
fContext = null;
return;
}
if (fContext.emptyPrefix())
{ // no prefix, so we could suggest anything
suggestMethodsForEnclosingType(script);
getDocumentsRubyElementsInScope();
suggestGlobals();
}
else
{
if (fContext.isDoubleSemiColon())
{
String prefix = fContext.getFullPrefix();
String typeName = prefix.substring(0, prefix.lastIndexOf("::"));
RubyElementRequestor requestor = new RubyElementRequestor(script);
Map<String, CompletionProposal> proposals = new HashMap<String, CompletionProposal>();
if (fContext.isBroken())
{
Map<IMethod, String> astMethods = addASTProposals(typeName);
for (IMethod method : astMethods.keySet())
{
if (!method.isSingleton())
continue;
CompletionProposal proposal = suggestMethod(method, astMethods.get(method), 100);
if (proposal == null)
continue;
proposals.put(proposal.getName(), proposal);
}
// FIXME Add constants!
addASTTypeConstants(typeName);
}
IType[] types = requestor.findType(typeName);
for (int i = 0; i < types.length; i++)
{
IType type = types[i];
proposals.putAll(suggestTypesConstants(type));
// Suggest nested types
proposals.putAll(suggestNestedTypes(type));
// Suggest class level methods
proposals.putAll(suggestMethods(100, type, false));
}
List<CompletionProposal> list = new ArrayList<CompletionProposal>(proposals.values());
Collections.sort(list, new CompletionProposalComparator());
for (CompletionProposal proposal : list)
{
if (proposal.getCompletion().startsWith(fContext.getPartialPrefix()))
fRequestor.accept(proposal);
}
this.fRequestor.endReporting();
fContext = null;
return;
}
if (fContext.isConstant())
{ // type or constant
suggestTypeNames();
suggestConstantNames();
return;
}
if (fContext.isGlobal())
{ // looks like a global
suggestGlobals();
return;
}
if (fContext.isInstanceVariable())
{ // looks like an instance variable
suggestInstanceVariables();
return;
}
if (fContext.isClassVariable())
{ // looks like an class variable
suggestClassVariables();
return;
}
if (fContext.isInstanceOrClassVariable())
{ // looks like an class or instance variable, can't tell 100% by prefix yet
suggestClassAndInstanceVariables();
return;
}
if (fContext.isExplicitMethodInvokation())
{
ITypeInferrer inferrer = RubyCore.getTypeInferrer();
Collection<ITypeGuess> guesses = inferrer.infer(fContext.getCorrectedSource(), fContext.getOffset());
if (guesses.isEmpty())
{
guesses = new ArrayList<ITypeGuess>();
guesses.add(new BasicTypeGuess(OBJECT, 100));
}
List<CompletionProposal> list = new ArrayList<CompletionProposal>();
RubyElementRequestor requestor = new RubyElementRequestor(script);
for (ITypeGuess guess : guesses)
{
final String name = guess.getType();
if (fContext.isBroken())
{
Map<IMethod, String> astMethods = addASTProposals(name);
for (IMethod method : astMethods.keySet())
{ // FIXME Don't suggest instance method if we're invoking on the actual typename/constant!
CompletionProposal proposal = suggestMethod(method, astMethods.get(method), 100);
if (proposal == null)
continue;
list.add(proposal);
}
}
IType[] types = requestor.findType(name);
// if we don't find type (probably because it's only in current working copy), find Object because
// all types stems from there
if (types == null || types.length == 0)
{
types = requestor.findType(OBJECT);
}
Map<String, CompletionProposal> mapAll = new HashMap<String, CompletionProposal>();
if (types != null && types.length > 0)
{
LogicalType type = new LogicalType(types);
mapAll.putAll(suggestMethods(guess.getConfidence(), type, true));
}
// if there isn't one, add a "new" constructor
if (!mapAll.containsKey("new") && fContext.fullPrefixIsConstant()
&& "new".startsWith(fContext.getPartialPrefix()))
{
CompletionProposal proposal = new CompletionProposal(CompletionProposal.METHOD_REF, "new", 100);
proposal.setDeclaringType(name);
proposal.setFlags(Flags.AccPublic | Flags.AccStatic);
proposal.setReplaceRange(fContext.getReplaceStart(), fContext.getReplaceStart() + 3);
proposal.setName("new");
mapAll.put("new", proposal);
}
list.addAll(mapAll.values());
}
// Only search for all methods matching prefix if we're unsure of type (multiple guesses, or none)
if (guesses.size() > 1 || (guesses.size() == 1 && guesses.iterator().next().getType().equals(OBJECT)))
{
list.addAll(suggestAllMethodsMatchingPrefix(script));
}
Collections.sort(list, new CompletionProposalComparator());
for (CompletionProposal proposal : list)
{
fRequestor.accept(proposal);
}
}
else
{
// FIXME If we're invoked on the class declaration (it's super class) don't do this!
// FIXME Traverse the IRubyElement model, not nodes (and don't reparse)?
if (fContext.isMethodInvokationOrLocal())
{
suggestMethodsForEnclosingType(script);
List<CompletionProposal> proposals = suggestAllMethodsMatchingPrefix(script);
for (CompletionProposal proposal : proposals)
{
fRequestor.accept(proposal);
}
}
getDocumentsRubyElementsInScope();
}
}
this.fRequestor.endReporting();
fContext = null;
}
private void suggestClassAndInstanceVariables()
{
addTypesVariables(getEnclosingTypeNode());
}
private void suggestInstanceVariables()
{
addTypesVariables(getEnclosingTypeNode());
}
private void suggestClassVariables()
{
addTypesVariables(getEnclosingTypeNode());
}
private Node getEnclosingTypeNode()
{
return ClosestSpanningNodeLocator.Instance().findClosestSpanner(getRootNode(), fContext.getOffset(),
new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ClassNode || node instanceof ModuleNode);
}
});
}
private void addASTTypeConstants(String typeName)
{
Collection<Node> typeNodes = getASTTypeNodesFromName(typeName);
for (Node typeNode : typeNodes)
{
// Get instance and class variables available in the enclosing type
List<Node> constants = ScopedNodeLocator.Instance().findNodesInScope(typeNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ConstDeclNode) || (node instanceof ClassNode)
|| (node instanceof ModuleNode);
}
});
Set<String> fields = new HashSet<String>();
if (constants != null)
{
for (Node varNode : constants)
{
if (varNode.equals(typeNode))
continue;
Node spanner = ClosestSpanningNodeLocator.Instance().findClosestSpanner(typeNode,
varNode.getPosition().getStartOffset() - 1, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return node instanceof ClassNode || node instanceof ModuleNode;
}
});
if (spanner == null || !spanner.equals(typeNode))
continue;
String name = ASTUtil.getNameReflectively(varNode);
if (!fContext.prefixStartsWith(name))
continue;
fields.add(name);
}
}
for (String field : fields)
{
CompletionProposal proposal = createProposal(fContext.getReplaceStart(),
CompletionProposal.CONSTANT_REF, field);
proposal.setDeclaringType(typeName);
proposal.setName(field);
fRequestor.accept(proposal);
}
}
}
private Map<IMethod, String> addASTProposals(final String name)
{
Map<IMethod, String> list = new HashMap<IMethod, String>();
Collection<Node> typeNodes = getASTTypeNodesFromName(name);
for (Node typeNode : typeNodes)
{
Collection<IMethod> astMethodsInScope = addASTMethodsInScope(typeNode, name);
for (IMethod method : astMethodsInScope)
{
list.put(method, name);
}
}
// Handle included methods from other modules inside same script
ASTSourceRequestor srcRequestor = new ASTSourceRequestor();
SourceElementParser srcParser = new SourceElementParser(srcRequestor);
srcParser.acceptNode(getRootNode());
List<String> mixins = srcRequestor.getMixins(name);
for (String mixin : mixins)
{
list.putAll(srcRequestor.getMethods(mixin));
}
return list;
}
private Collection<Node> getASTTypeNodesFromName(final String name)
{
final Node rootNode = getRootNode();
return ScopedNodeLocator.Instance().findNodesInScope(rootNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
if (!(node instanceof ModuleNode) && !(node instanceof ClassNode))
return false;
return ASTUtil.getFullyQualifiedTypeName(rootNode, node).equals(name);
}
});
}
private Collection<IMethod> addASTMethodsInScope(Node typeNode, String name)
{
List<IMethod> list = new ArrayList<IMethod>();
if (typeNode == null)
return list;
List<Node> methods = ScopedNodeLocator.Instance().findNodesInScope(typeNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof DefnNode) || (node instanceof DefsNode);
}
});
for (Node methodNode : methods)
{
Node scoping = findNearestScope(typeNode, methodNode.getPosition().getStartOffset() - 1);
if (scoping == null || !scoping.equals(typeNode))
continue;
MethodDefNode methodDef = (MethodDefNode) methodNode;
NodeMethod method = new NodeMethod(methodDef, fContext.getScript());
list.add(method);
}
return list;
}
private Map<String, CompletionProposal> suggestTypesConstants(IType type) throws RubyModelException
{
Map<String, CompletionProposal> proposals = new HashMap<String, CompletionProposal>();
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.CONSTANT, "*",
IRubySearchConstants.DECLARATIONS, SearchPattern.R_PATTERN_MATCH);
IRubySearchScope scope = BasicSearchEngine.createRubySearchScope(new IRubyElement[] { type });
List<SearchMatch> results = search(pattern, scope);
for (SearchMatch match : results)
{
IRubyElement element = (IRubyElement) match.getElement();
if (element.getElementType() != IRubyElement.CONSTANT)
continue; // XXX we shouldn't have to do this
// Add proposal
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.CONSTANT_REF,
element.getElementName(), element);
proposal.setType(type.getFullyQualifiedName());
proposal.setName(element.getElementName());
proposals.put(element.getElementName(), proposal);
}
return proposals;
}
private Map<String, CompletionProposal> suggestNestedTypes(IType type) throws RubyModelException
{
Map<String, CompletionProposal> proposals = new HashMap<String, CompletionProposal>();
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.TYPE, "*", IRubySearchConstants.DECLARATIONS,
SearchPattern.R_PATTERN_MATCH);
IRubySearchScope scope = BasicSearchEngine.createRubySearchScope(new IRubyElement[] { type });
List<SearchMatch> results = search(pattern, scope);
for (SearchMatch match : results)
{
IType aType = (IType) match.getElement();
String fullname = aType.getFullyQualifiedName();
if (fullname.equals(type.getFullyQualifiedName()))
continue; // don't return exact match to prefix
if (!fullname.startsWith(type.getFullyQualifiedName()))
continue; // only return those nested underneath prefix
String[] parts = Util.getTypeNameParts(fullname);
// Don't add if it's not the directly nested child (and is instead the grandchild)
if (parts.length != Util.getTypeNameParts(type.getFullyQualifiedName()).length + 1)
continue;
// Add proposal
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.TYPE_REF, aType
.getElementName());
proposal.setType(aType.getFullyQualifiedName());
proposal.setName(aType.getElementName());
proposals.put(aType.getElementName(), proposal);
}
return proposals;
}
private List<CompletionProposal> suggestAllMethodsMatchingPrefix(IRubyScript script)
{
List<CompletionProposal> list = new ArrayList<CompletionProposal>();
if (fContext.getPartialPrefix() == null || fContext.getPartialPrefix().trim().length() == 0)
return list;
IRubySearchScope scope = BasicSearchEngine
.createRubySearchScope(new IRubyElement[] { script.getRubyProject() });
SearchParticipant participant = BasicSearchEngine.getDefaultSearchParticipant();
CollectingSearchRequestor searchRequestor = new CollectingSearchRequestor();
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.METHOD, fContext.getPartialPrefix(),
IRubySearchConstants.DECLARATIONS, SearchPattern.R_PREFIX_MATCH);
try
{
new BasicSearchEngine().search(pattern, new SearchParticipant[] { participant }, scope, searchRequestor,
null);
}
catch (CoreException e)
{
RubyCore.log(e);
}
List<SearchMatch> matches = searchRequestor.getResults();
for (SearchMatch match : matches)
{
IMethod element = (IMethod) match.getElement();
IType type = element.getDeclaringType();
String typeName = "";
if (type != null)
typeName = type.getElementName();
CompletionProposal proposal = suggestMethod(element, typeName, 50); // TODO Base confidence on accuracy in
// match?
if (proposal != null)
{
list.add(proposal);
}
}
return list;
}
private void suggestMethodsForEnclosingType(IRubyScript script) throws RubyModelException
{
Object thing = script.getElementAt(fContext.getOffset());
IMember element = null;
if (thing instanceof IMember)
{
element = (IMember) thing;
}
boolean includeInstance = !fContext.inTypeDefinition();
IType[] types;
if (element == null)
{
// We're in the top level, so we're in "Object"
RubyElementRequestor requestor = new RubyElementRequestor(script);
IType[] tmpTypes = requestor.findType(OBJECT);
List<IType> filtered = new ArrayList<IType>();
for (int i = 0; i < tmpTypes.length; i++)
{
// FIXME We shouldn't be getting these types with bad fully qualified names anyhow, should we?
if (!tmpTypes[i].getFullyQualifiedName().equals(OBJECT))
continue;
filtered.add(tmpTypes[i]);
}
types = filtered.toArray(new IType[filtered.size()]);
includeInstance = false;
}
else if (element instanceof IType)
{
IType type = (IType) element;
RubyElementRequestor requestor = new RubyElementRequestor(script);
types = requestor.findType(type.getFullyQualifiedName());
}
else
{
types = new IType[] { element.getDeclaringType() };
}
if (types == null || types.length < 1)
return;
Map<String, CompletionProposal> map = new HashMap<String, CompletionProposal>();
for (int i = 0; i < types.length; i++)
{
if (types[i] == null)
continue; // FIXME SHouldn't get this ever, but we sometimes do!
map.putAll(suggestMethods(100, types[i], includeInstance));
}
List<CompletionProposal> list = sort(map);
for (CompletionProposal proposal : list)
{
fRequestor.accept(proposal);
}
}
/**
* Wrap beginning of recursion to suggest methods for a type. We keep track of types visited so that we can avoid
* inifnite loops.
*
* @param confidence
* @param type
* @param includeInstanceMethods
* @return
* @throws RubyModelException
*/
private Map<String, CompletionProposal> suggestMethods(int confidence, IType type, boolean includeInstanceMethods)
throws RubyModelException
{
if (fVisitedTypes == null)
fVisitedTypes = new HashSet<IType>();
fOriginalType = type;
// FIXME We want to avoid visiting the same types across the guesses too!
Map<String, CompletionProposal> proposals = new HashMap<String, CompletionProposal>();
IType[] superTypes = getSuperTypes(type);
for (int j = 0; j < superTypes.length; j++)
{
IType currentType = superTypes[j];
if (fVisitedTypes.contains(currentType))
continue;
fVisitedTypes.add(currentType);
IMethod[] methods = currentType.getMethods();
if (methods == null)
continue;
for (int k = 0; k < methods.length; k++)
{
if (methods[k] == null)
continue;
CompletionProposal proposal = suggestMethod(methods[k], currentType.getElementName(), confidence);
if (proposal != null && !proposals.containsKey(proposal.getName()))
{
proposals.put(proposal.getName(), proposal); // If a method name matches an existing suggestion
// (i.e. its overriden in the subclass), don't
// suggest it again!
}
}
}
fOriginalType = null;
fVisitedTypes.clear();
return proposals;
}
private IType[] getSuperTypes(IType type) throws RubyModelException
{
ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
if (hierarchy == null)
return new IType[] { type };
IType[] superTypes = hierarchy.getAllSupertypes(type);
if (superTypes == null || superTypes.length == 0)
return new IType[] { type };
IType[] modules = hierarchy.getAllSuperModules(type);
if (modules == null || modules.length == 0)
{
int length = superTypes.length;
IType[] all = new IType[length + 1]; // Apparently getAllTypes is returning types that are in files that are
// related to type hierarchy, but aren't supertypes of focus! So I
// had to switch to getAllSupertypes(focus);
all[0] = type;
System.arraycopy(superTypes, 0, all, 1, length);
return all;
}
int length = superTypes.length + modules.length;
IType[] all = new IType[length + 1]; // Apparently getAllTypes is returning types that are in files that are
// related to type hierarchy, but aren't supertypes of focus! So I had
// to switch to getAllSupertypes(focus);
all[0] = type;
System.arraycopy(superTypes, 0, all, 1, superTypes.length);
System.arraycopy(modules, 0, all, superTypes.length + 1, modules.length);
return all;
}
private List<CompletionProposal> sort(Map<String, CompletionProposal> proposals)
{
List<CompletionProposal> list = new ArrayList<CompletionProposal>(proposals.values());
Collections.sort(list, new CompletionProposalComparator());
return list;
}
private void suggestGlobals()
{
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.GLOBAL, "$*",
IRubySearchConstants.DECLARATIONS, SearchPattern.R_PATTERN_MATCH);
IRubySearchScope scope = BasicSearchEngine.createRubySearchScope(new IRubyElement[] { fContext.getScript()
.getRubyProject() });
List<SearchMatch> results = search(pattern, scope);
Set<String> names = new HashSet<String>();
for (SearchMatch match : results)
{
IRubyElement element = (IRubyElement) match.getElement();
String name = element.getElementName();
if (names.contains(name))
continue;
names.add(name);
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.GLOBAL_REF,
name, element);
proposal.setType(name);
fRequestor.accept(proposal);
}
}
private void suggestTypeNames()
{
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.TYPE, fContext.getPartialPrefix(),
IRubySearchConstants.DECLARATIONS, SearchPattern.R_CAMELCASE_MATCH);
List<SearchMatch> results = search(pattern, BasicSearchEngine.createWorkspaceScope());
Set<String> names = new HashSet<String>();
for (SearchMatch match : results)
{
IRubyElement element = (IRubyElement) match.getElement();
String name = element.getElementName();
if (names.contains(name))
continue;
names.add(name);
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.TYPE_REF, name,
element);
proposal.setType(name);
fRequestor.accept(proposal);
}
}
private List<SearchMatch> search(SearchPattern pattern, IRubySearchScope scope)
{
BasicSearchEngine engine = new BasicSearchEngine();
SearchParticipant[] participants = new SearchParticipant[] { BasicSearchEngine.getDefaultSearchParticipant() };
CollectingSearchRequestor requestor = new CollectingSearchRequestor();
try
{
engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
}
catch (CoreException e)
{
RubyCore.log(e);
}
return requestor.getResults();
}
private CompletionProposal createProposal(int replaceStart, int type, String name)
{
return createProposal(replaceStart, type, name, 100, null);
}
private CompletionProposal createProposal(int replaceStart, int type, String name, IRubyElement element)
{
return createProposal(replaceStart, type, name, 100, element);
}
private CompletionProposal createProposal(int replaceStart, int type, String name, int confidence,
IRubyElement element)
{
CompletionProposal proposal = new CompletionProposal(type, name, confidence);
proposal.setReplaceRange(replaceStart, replaceStart + name.length());
proposal.setElement(element);
return proposal;
}
private void suggestConstantNames()
{
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.CONSTANT, fContext.getPartialPrefix() + "*",
IRubySearchConstants.DECLARATIONS, SearchPattern.R_PATTERN_MATCH);
IRubySearchScope scope = BasicSearchEngine.createRubySearchScope(new IRubyElement[] { fContext.getScript() });
List<SearchMatch> results = search(pattern, scope);
for (SearchMatch match : results)
{
IRubyElement element = (IRubyElement) match.getElement();
String name = element.getElementName();
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.CONSTANT_REF,
name, element);
proposal.setType(name);
fRequestor.accept(proposal);
}
if ("ARGV".startsWith(fContext.getPartialPrefix()))
{
IRubyElement element = new RubyConstant(null, "ARGV");
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), CompletionProposal.CONSTANT_REF,
"ARGV", element);
proposal.setType("Array");
fRequestor.accept(proposal);
}
}
private CompletionProposal suggestMethod(IMethod method, String typeName, int confidence)
{
try
{
int start = fContext.getReplaceStart();
String name = method.getElementName();
int flags = Flags.AccDefault;
if (method.isSingleton())
{
flags |= Flags.AccStatic;
if (method.isConstructor())
name = CONSTRUCTOR_INVOKE_NAME;
else
{
if (name.startsWith(typeName))
{
name = name.substring(typeName.length() + 1);
}
}
}
else
{
// Don't show instance methods if the thing we're working on is a class' name!
// FIXME We do want to show if it is a constant, but not a class name
if (fContext.fullPrefixIsConstant())
return null;
}
if (!fContext.prefixStartsWith(name))
return null;
try
{
switch (method.getVisibility())
{
case IMethod.PRIVATE:
flags |= Flags.AccPrivate;
if (fOriginalType != null && !fOriginalType.getElementName().equals(typeName))
return null; // FIXME We should do a comparison of types, not names
if (fContext.hasReceiver())
return null; // can't invoke a private method on a receiver
break;
case IMethod.PUBLIC:
flags |= Flags.AccPublic; // FIXME Check if receiver is of same class as method's declaring
// type, if not, skip this method. (so we can invoke with no
// receiver inside same class, with explicit self as receiver, or
// with receiver who has same class).
break;
case IMethod.PROTECTED:
flags |= Flags.AccProtected;
break;
default:
break;
}
}
catch (RubyModelException e)
{
RubyCore.log(e);
flags |= Flags.AccPublic;
}
CompletionProposal proposal = createProposal(start, CompletionProposal.METHOD_REF, name, confidence, method);
proposal.setReplaceRange(start, start + name.length());
proposal.setFlags(flags);
proposal.setName(name);
IType declaringType = method.getDeclaringType();
String declaringName = typeName;
if (declaringType != null)
declaringName = declaringType.getFullyQualifiedName();
proposal.setDeclaringType(declaringName);
return proposal;
}
catch (RuntimeException e)
{
RubyCore.log(e);
return null;
}
}
/**
* Gets all the distinct elements in the current RubyScript
*
* @param offset
* @param replaceStart
* @return a List of the names of all the elements in the current RubyScript
*/
private void getDocumentsRubyElementsInScope()
{
// Grab enclosing scope and add variables in this scope
Collection<String> variables = addVariablesinScope(getScope(findNearestScope(getRootNode(), fContext
.getOffset())));
for (final String variable : variables)
{
if (variable.equals(fContext.getPartialPrefix()))
{
// Check if this is the node we invoked code assist on (and only reference to this name!)
if (onlyLocalVarReferenceIsInvokation())
continue;
}
CompletionProposal proposal = createProposal(fContext.getReplaceStart(),
getCompletionProposalType(variable), variable);
proposal.setName(variable);
fRequestor.accept(proposal);
}
// Add methods in this scope
Collection<IMethod> methods = addASTMethodsInScope(getEnclosingTypeOrRootNode(), "");
for (IMethod method : methods)
{
CompletionProposal proposal = suggestMethod(method, "", 100);
if (proposal == null)
continue;
fRequestor.accept(proposal);
}
// Add members from enclosing type
getMembersAvailableInsideType(getEnclosingTypeNode());
}
private boolean onlyLocalVarReferenceIsInvokation()
{
List<Node> localVarNodes = ScopedNodeLocator.Instance().findNodesInScope(getRootNode(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
if (!(node instanceof LocalVarNode || node instanceof LocalAsgnNode))
return false;
INameNode nameNode = (INameNode) node;
return nameNode.getName().equals(fContext.getPartialPrefix());
}
});
return localVarNodes != null && localVarNodes.size() == 1;
}
private Node getEnclosingTypeOrRootNode()
{
Node result = getEnclosingTypeNode();
if (result != null)
return result;
return getRootNode();
}
private Node getRootNode()
{
try
{
return fContext.getRootNode();
}
catch (SyntaxException se)
{
RubyCore.log(se);
}
return null;
}
private Node findNearestScope(Node scopeNode, int offset)
{
if (offset == -1)
return scopeNode;
Node scope = ClosestSpanningNodeLocator.Instance().findClosestSpanner(scopeNode, offset, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof DefnNode || node instanceof DefsNode || node instanceof ClassNode
|| node instanceof ModuleNode || node instanceof RootNode || node instanceof IterNode);
}
});
if (scope == null)
return scopeNode;
return scope;
}
private Set<String> addVariablesinScope(StaticScope scope)
{
Set<String> matches = new HashSet<String>();
if (scope == null)
return matches;
for (String local : scope.getVariables())
{
if (!fContext.prefixStartsWith(local))
continue;
matches.add(local);
}
// call recursivley up parent scopes
matches.addAll(addVariablesinScope(scope.getEnclosingScope()));
return matches;
}
private StaticScope getScope(Node enclosingNode)
{
if (enclosingNode == null)
return ((RootNode) getRootNode()).getStaticScope();
if (enclosingNode instanceof RootNode)
{
RootNode root = (RootNode) enclosingNode;
return root.getStaticScope();
}
try
{
Method getScopeMethod = enclosingNode.getClass().getMethod("getScope", new Class[] {});
Object scope = getScopeMethod.invoke(enclosingNode, new Object[0]);
return (StaticScope) scope;
}
catch (Exception e)
{
return null;
}
}
/**
* Gets the members available inside a type node (ModuleNode, ClassNode): - Instance variables - Class variables -
* Methods
*
* @param typeNode
* @return
*/
private void getMembersAvailableInsideType(Node typeNode)
{
if (typeNode == null)
return;
String typeName = getTypeName(typeNode);
if (typeName == null)
return;
// Get superclass and add its public members
List<Node> superclassNodes = getSuperclassNodes(typeNode);
for (Node superclassNode : superclassNodes)
{
getMembersAvailableInsideType(superclassNode);
}
// Get public members of mixins
List<String> mixinNames = getIncludedMixinNames(typeName);
for (String mixinName : mixinNames)
{
List<Node> mixinDeclarations = getTypeDeclarationNodes(mixinName);
for (Node mixinDeclaration : mixinDeclarations)
{
getMembersAvailableInsideType(mixinDeclaration);
}
}
// Get method names defined by DefnNodes and DefsNodes
List<Node> methodDefinitions = ScopedNodeLocator.Instance().findNodesInScope(typeNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof DefnNode) || (node instanceof DefsNode);
}
});
for (Node methodDefinition : methodDefinitions)
{
String name = null;
if (methodDefinition instanceof DefnNode)
{
name = ((DefnNode) methodDefinition).getName();
}
if (methodDefinition instanceof DefsNode)
{
name = ((DefsNode) methodDefinition).getName();
}
if (!fContext.prefixStartsWith(name))
continue;
NodeMethod method = new NodeMethod((MethodDefNode) methodDefinition, fContext.getScript());
suggestMethod(method, typeName, 100);
}
addTypesVariables(typeNode);
}
private String getTypeName(Node typeNode)
{
// Get type name
String typeName = null;
if (typeNode instanceof ClassNode)
{
typeName = ((Colon2Node) ((ClassNode) typeNode).getCPath()).getName();
}
if (typeNode instanceof ModuleNode)
{
typeName = ((Colon2Node) ((ModuleNode) typeNode).getCPath()).getName();
}
return typeName;
}
/**
* FIXME Break this down more so we can grab class vars, instance vars, or constants!
*
* @param typeNode
*/
private void addTypesVariables(Node typeNode)
{
if (typeNode == null)
return;
// Get instance and class variables available in the enclosing type
List<Node> instanceAndClassVars = ScopedNodeLocator.Instance().findNodesInScope(typeNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ConstDeclNode || node instanceof InstVarNode || node instanceof InstAsgnNode
|| node instanceof ClassVarNode || node instanceof ClassVarDeclNode || node instanceof ClassVarAsgnNode);
}
});
Set<String> fields = new HashSet<String>();
if (instanceAndClassVars != null)
{
// Get the unique names of instance and class variables
for (Node varNode : instanceAndClassVars)
{
String name = ASTUtil.getNameReflectively(varNode);
if (name.equals(fContext.getPartialPrefix()))
{
// don't add if this is the node we invoked code assist on!
if (varNode.getPosition().getStartOffset() <= fContext.getOffset()
&& varNode.getPosition().getEndOffset() >= fContext.getOffset())
{
continue;
}
}
fields.add(name);
}
}
// Get instance and class vars defined by [c]attr_* calls
fields.addAll(new AttributeLocator().findInstanceAttributesInScope(typeNode));
for (String field : fields)
{
if (!fContext.prefixStartsWith(field))
continue;
CompletionProposal proposal = createProposal(fContext.getReplaceStart(), getCompletionProposalType(field),
field);
proposal.setName(field);
fRequestor.accept(proposal);
}
}
private int getCompletionProposalType(String field)
{
if (field == null)
return CompletionProposal.CONSTANT_REF;
if (field.startsWith("@@"))
return CompletionProposal.CLASS_VARIABLE_REF;
if (field.startsWith("@"))
return CompletionProposal.INSTANCE_VARIABLE_REF;
if (field.startsWith("$"))
return CompletionProposal.GLOBAL_REF;
if (Character.isUpperCase(field.charAt(0)))
return CompletionProposal.CONSTANT_REF;
return CompletionProposal.LOCAL_VARIABLE_REF;
}
/**
* Finds all nodes that declare a type that is a superclass of the specified node. Example: """ class Klass;def
* meth_1;1;end;end class Klass;def meth_2;2;end;end class SubKlass < Klass;end """ Issuing getSuperClassNodes() on
* the ClassNode declaring SubKlass would return two ClassNodes; one for each definition of Klass.
*
* @param typeNode
* Node to find superclass nodes of
* @return List of ClassNode or ModuleNode
*/
private List<Node> getSuperclassNodes(Node typeNode)
{
if (typeNode instanceof ClassNode)
{
Node superNode = ((ClassNode) typeNode).getSuperNode();
if (superNode instanceof ConstNode)
{
String superclassName = ((ConstNode) superNode).getName();
return getTypeDeclarationNodes(superclassName);
}
}
return new ArrayList<Node>();
}
/** Lookup type declaration nodes */
private List<Node> getTypeDeclarationNodes(String typeName)
{
// Find the named type
RubyElementRequestor requestor = new RubyElementRequestor(fContext.getScript());
IType[] types = requestor.findType(typeName);
if (types == null || types.length == 0)
return new ArrayList<Node>(0);
IType type = types[0];
try
{
if (type instanceof RubyType)
{
// FIXME This feels a little hacky and backwards -
// RubyType.getSource() and then parse... consider reworking the
// clients to this method to accept RubyTypes or something
// similar?
// Find source and parse
RubyType rubyType = (RubyType) type;
String source = rubyType.getSource();
if (source == null)
return new ArrayList<Node>(0);
// FIXME Why does the parser balk on \r chars?
source = source.replace('\r', ' ');
Node rootNode = null;
try
{
rootNode = (new RubyParser()).parse(type.getRubyScript().getElementName(), source).getAST();
}
catch (Exception e)
{
RubyCore.log(e);
}
// Bail if the parse fails
if (rootNode == null)
{
return new ArrayList<Node>();
}
// Return any type declaration nodes in included source
return ScopedNodeLocator.Instance().findNodesInScope(rootNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ClassNode) || (node instanceof ModuleNode);
}
});
}
}
catch (RubyModelException rme)
{
rme.printStackTrace();
}
return new ArrayList<Node>(0);
}
private List<String> getIncludedMixinNames(String typeName)
{
IType rubyType = new RubyType((RubyElement) fContext.getScript(), typeName);
try
{
String[] includedModuleNames = rubyType.getIncludedModuleNames();
if (includedModuleNames != null)
{
return Arrays.asList(rubyType.getIncludedModuleNames());
}
return new ArrayList<String>(0);
}
catch (RubyModelException e)
{
return new ArrayList<String>(0);
}
}
private class NodeMethod implements IMethod
{
private MethodDefNode node;
private IRubyScript fScript;
public NodeMethod(MethodDefNode methodDefinition, IRubyScript script)
{
this.node = methodDefinition;
this.fScript = script;
}
public String[] getParameterNames() throws RubyModelException
{
return ASTUtil.getArgs(node.getArgsNode(), node.getScope());
}
public int getNumberOfParameters() throws RubyModelException
{
return getParameterNames().length;
}
public int getVisibility() throws RubyModelException
{
return IMethod.PUBLIC;
}
public boolean isConstructor()
{
return node.getName().equals(CONSTRUCTOR_DEFINITION_NAME);
}
public boolean isSingleton()
{
return isConstructor() || node instanceof DefsNode;
}
public boolean isSimilar(IMethod method)
{
// TODO Auto-generated method stub
return false;
}
public boolean exists()
{
return false;
}
public IRubyElement getAncestor(int ancestorType)
{
return null;
}
public IResource getCorrespondingResource() throws RubyModelException
{
return null;
}
public String getElementName()
{
return node.getName();
}
public int getElementType()
{
return IRubyElement.METHOD;
}
public IOpenable getOpenable()
{
return fScript;
}
public IRubyElement getParent()
{
return null;
}
public IPath getPath()
{
return null;
}
public IRubyElement getPrimaryElement()
{
return null;
}
public IResource getResource()
{
return null;
}
public IRubyModel getRubyModel()
{
return null;
}
public IRubyProject getRubyProject()
{
return null;
}
public IResource getUnderlyingResource() throws RubyModelException
{
return null;
}
public boolean isReadOnly()
{
return false;
}
public boolean isStructureKnown() throws RubyModelException
{
return false;
}
public boolean isType(int type)
{
return type == IRubyElement.METHOD;
}
public Object getAdapter(Class adapter)
{
return null;
}
public IType getDeclaringType()
{
return null;
}
public ISourceRange getNameRange() throws RubyModelException
{
return null;
}
public IRubyScript getRubyScript()
{
return fScript;
}
public IType getType(String name, int occurrenceCount)
{
return null;
}
public String getSource() throws RubyModelException
{
return null;
}
public ISourceRange getSourceRange() throws RubyModelException
{
return null;
}
public IRubyElement[] getChildren() throws RubyModelException
{
return null;
}
public boolean hasChildren() throws RubyModelException
{
return false;
}
public String getHandleIdentifier()
{
return null;
}
public boolean isPrivate() throws RubyModelException
{
return false;
}
public boolean isProtected() throws RubyModelException
{
return false;
}
public boolean isPublic() throws RubyModelException
{
return false;
}
public String[] getBlockParameters() throws RubyModelException
{
final Set<String> vars = new HashSet<String>();
InOrderVisitor visitor = new InOrderVisitor()
{
private String typeName;
@Override
public Object visitClassNode(ClassNode iVisited)
{
typeName = ASTUtil.getFullyQualifiedName(iVisited.getCPath());
return super.visitClassNode(iVisited);
}
@Override
public Object visitModuleNode(ModuleNode iVisited)
{
typeName = ASTUtil.getFullyQualifiedName(iVisited.getCPath());
return super.visitModuleNode(iVisited);
}
@Override
public Object visitYieldNode(YieldNode iVisited)
{
Node argsNode = iVisited.getArgsNode();
if (argsNode instanceof LocalVarNode)
{
vars.add(((LocalVarNode) argsNode).getName());
}
else if (argsNode instanceof SelfNode)
{
String name = null;
if (typeName == null)
{
name = "var";
}
else
{
name = typeName.toLowerCase();
if (name.indexOf("::") > -1)
{
name = name.substring(name.lastIndexOf("::") + 2);
}
}
vars.add(name);
}
return super.visitYieldNode(iVisited);
}
};
this.node.accept(visitor);
return vars.toArray(new String[vars.size()]);
}
}
}