/**
* 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.core.codeassist;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IProject;
import org.jrubyparser.CompatVersion;
import org.jrubyparser.StaticScope;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.CommentNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.ModuleNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.RootNode;
import org.jrubyparser.ast.SymbolNode;
import org.jrubyparser.parser.ParserResult;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.StringUtil;
import com.aptana.ruby.core.IRubyConstants;
import com.aptana.ruby.core.RubyCorePlugin;
import com.aptana.ruby.core.RubySourceParser;
import com.aptana.ruby.core.ast.ASTUtils;
import com.aptana.ruby.core.ast.ClosestSpanningNodeLocator;
import com.aptana.ruby.core.ast.INodeAcceptor;
import com.aptana.ruby.core.ast.NamespaceVisitor;
import com.aptana.ruby.core.ast.OffsetNodeLocator;
import com.aptana.ruby.core.ast.ScopedNodeLocator;
import com.aptana.ruby.core.inference.ITypeGuess;
import com.aptana.ruby.core.inference.ITypeInferrer;
import com.aptana.ruby.internal.core.inference.TypeInferrer;
public class CompletionContext
{
private int offset;
private boolean isMethodInvokation = false;
private String correctedSource;
private String partialPrefix;
private String fullPrefix;
private int replaceStart;
private boolean isAfterDoubleSemiColon = false;
private Node fRootNode;
private List<CommentNode> fCommentNodes;
private boolean inComment;
private String src;
// Cache of the enclosing scope (typically type node or root) for instance/class vars
private Node enclosingScopeNode;
private IProject project;
public CompletionContext(IProject project, String src, int offset)
{
this.project = project;
this.src = src;
if (offset < 0)
{
offset = 0;
}
this.offset = offset;
replaceStart = offset + 1;
try
{
run(src);
}
catch (RuntimeException e)
{
IdeLog.logError(RubyCorePlugin.getDefault(), e);
}
}
@SuppressWarnings("nls")
private void run(final String src)
{
StringBuilder source = new StringBuilder(src);
if (offset >= source.length())
{
offset = source.length() - 1;
replaceStart = offset + 1;
}
// Read from offset back until we hit a: space, period
// if we hit a period, use character before period as offset for
// inferrer
StringBuffer tmpPrefix = new StringBuffer();
boolean setOffset = false;
for (int i = offset; i >= 0; i--)
{
char curChar = source.charAt(i);
if (offset == i)
{ // check the first character
switch (curChar)
{
case '@':
if (((i - 1) >= 0) && (source.charAt(i - 1) == '@'))
{
source.deleteCharAt(i);
source.deleteCharAt(i - 1);
tmpPrefix.append('@');
i--;
}
else
{
source.deleteCharAt(i);
}
break;
case '.':
case '$': // if it breaks syntax, lets fix it
case ',':
// TODO What if there is a valid character after this, so syntax isn't broken?
source.deleteCharAt(i);
break;
case ':':
if (i > 0)
{
// Check character before this for :
char previous = source.charAt(i - 1);
if (previous == ':')
{
isAfterDoubleSemiColon = true;
source.deleteCharAt(i);
source.deleteCharAt(i - 1);
tmpPrefix.insert(0, IRubyConstants.NAMESPACE_DELIMETER);
partialPrefix = StringUtil.EMPTY;
i--;
if (offset >= source.length())
{
offset = source.length() - 1;
}
continue;
}
}
// FIXME this fixes some cases, but doesn't fix unfinished hash, i.e. "{:key => value, :| }
// add a letter after to form a symbol if this breaks syntax...
if (i + 1 < source.length())
{
char next = source.charAt(i + 1);
if (!Character.isLetterOrDigit(next))
{
source.insert(i + 1, 's'); // insert an s so we generate a fake ":s" symbol
}
}
else
{
source.insert(i + 1, 's'); // insert an s so we generate a fake ":s" symbol
}
break;
}
}
if (curChar == '.')
{
isMethodInvokation = true;
if (partialPrefix == null)
{
this.partialPrefix = tmpPrefix.toString();
}
if (!setOffset)
{
if (offset - 1 == i)
{
offset = i;
}
else
{
offset = i - 1;
}
setOffset = true;
}
}
else if (curChar == ':')
{
if (i > 0)
{
// Check character before this for :
char previous = source.charAt(i - 1);
if (previous == ':')
{
isAfterDoubleSemiColon = true;
if (partialPrefix == null)
{
partialPrefix = tmpPrefix.toString();
}
tmpPrefix.insert(0, ":");
if (!setOffset)
{
offset = i + 1;
setOffset = true;
}
i--;
}
}
}
// FIXME This logic is very much like RubyWordDetector in the UI!
if (Character.isWhitespace(curChar) || curChar == ',' || curChar == '(' || curChar == '[' || curChar == '{')
{
if (!setOffset)
{
offset = i + 1;
setOffset = true;
}
break;
}
tmpPrefix.insert(0, curChar);
}
this.fullPrefix = tmpPrefix.toString();
if (partialPrefix == null)
{
partialPrefix = fullPrefix;
}
if (partialPrefix != null)
{
replaceStart -= partialPrefix.length();
}
this.correctedSource = source.toString();
// TODO For memory's sake, don't store corrected source if it's the same as original!
ClosestSpanningNodeLocator spanningLocator = new ClosestSpanningNodeLocator();
Node selected = spanningLocator.find(getRootNode(), this.offset, new INodeAcceptor()
{
public boolean accepts(Node node)
{
return true;
}
});
if (selected == null)
{
if (fCommentNodes != null)
{
for (CommentNode comment : fCommentNodes)
{
if (spanningLocator.spansOffset(comment, this.offset))
{
inComment = true;
break;
}
}
}
}
}
/**
* This is when we have a receiver and a period in the prefix
*
* @return
*/
public boolean isExplicitMethodInvokation()
{
return isMethodInvokation;
}
/**
* This is when it could be a method call with an implicit self, or when it may just be a local
*
* @return
*/
public boolean isMethodInvokationOrLocal()
{
return !isExplicitMethodInvokation()
&& (emptyPrefix() || (getPartialPrefix().length() > 0 && Character.isLowerCase(getPartialPrefix()
.charAt(0))));
}
/**
* The last portion of prefix is not null, not empty and starts with an uppercase letter
*
* @return
*/
public boolean isConstant()
{
String partial = getPartialPrefix();
return partial != null && partial.length() > 0 && Character.isUpperCase(partial.charAt(0));
}
public int getReplaceStart()
{
return replaceStart;
}
/**
* Modified source which should not fail parsing.
*
* @return
*/
public String getCorrectedSource()
{
return correctedSource;
}
public boolean isBroken()
{
return !getCorrectedSource().equals(getSource());
}
public boolean hasReceiver()
{
return getFullPrefix().indexOf('.') > 1;
}
/**
* The original source
*
* @return
*/
public String getSource()
{
return src;
}
public String getFullPrefix()
{
return fullPrefix;
}
public String getPartialPrefix()
{
return partialPrefix;
}
public int getOffset()
{
return offset;
}
public boolean emptyPrefix()
{
return getFullPrefix() == null || getFullPrefix().length() == 0;
}
public boolean prefixStartsWith(String name)
{
return name != null && getPartialPrefix() != null && name.startsWith(getPartialPrefix());
}
public boolean isGlobal()
{
return !emptyPrefix() && !isExplicitMethodInvokation()
&& (getPartialPrefix().length() > 0 && getPartialPrefix().charAt(0) == '$');
}
public boolean isDoubleColon()
{
return isAfterDoubleSemiColon && !isMethodInvokation;
}
public boolean fullPrefixIsConstant()
{
String full = getFullPrefix();
if (full == null || full.length() == 0)
{
return false;
}
if (full.endsWith("\".") || full.endsWith("'.")) //$NON-NLS-1$ //$NON-NLS-2$
{
return false;
}
return Character.isUpperCase(full.charAt(0));
}
/**
* Returns whether we're inside a type definition and not inside a method definition (used to determine if we should
* only show class level methods)
*
* @return
*/
public boolean inTypeDefinition()
{
if (getRootNode() == null)
{
return false;
}
Node spanner = new ClosestSpanningNodeLocator().find(getRootNode(), getOffset(), new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node instanceof MethodDefNode || node instanceof ClassNode || node instanceof ModuleNode;
}
});
return spanner instanceof ClassNode || spanner instanceof ModuleNode;
}
public synchronized Node getRootNode()
{
if (fRootNode != null)
{
return fRootNode;
}
// TODO Use ParserPoolFactory here!
RubySourceParser parser = new RubySourceParser(CompatVersion.BOTH);
if (!isBroken())
{
try
{
ParserResult result = parser.parse(getSource());
fRootNode = result.getAST();
fCommentNodes = result.getCommentNodes();
}
catch (RuntimeException e)
{
// ignore
}
}
if (fRootNode == null)
{
try
{
ParserResult result = parser.parse(getCorrectedSource());
fRootNode = result.getAST();
fCommentNodes = result.getCommentNodes();
}
catch (RuntimeException e)
{
// ignore
}
}
return fRootNode;
}
public boolean inComment()
{
return inComment;
}
/**
* User may be trying to complete either a class or an instance variable (we only have '@')
*
* @return
*/
public boolean isInstanceOrClassVariable()
{
return getPartialPrefix() != null && getPartialPrefix().length() == 1 && getPartialPrefix().charAt(0) == '@';
}
/**
* We know for certain user is trying to complete an instance var (we have '@[a-z]+')
*
* @return
*/
public boolean isInstanceVariable()
{
return getPartialPrefix() != null && getPartialPrefix().length() > 1 && getPartialPrefix().charAt(0) == '@'
&& !isClassVariable();
}
/**
* We know for certain user is trying to complete a class var (we have '@@[a-z]*')
*
* @return
*/
public boolean isClassVariable()
{
return getPartialPrefix() != null && getPartialPrefix().startsWith("@@"); //$NON-NLS-1$
}
/**
* Grab the enclosing type's fully qualified name.
*
* @return
*/
public String getEnclosingType()
{
Node typeNode = new ClosestSpanningNodeLocator().find(getRootNode(), getOffset(), new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node instanceof ClassNode || node instanceof ModuleNode;
}
});
if (typeNode == null)
{
return "Object"; //$NON-NLS-1$
}
// Also grab the namespace at this point and prefix it here!
String namespace = getNamespace();
if (namespace != null && namespace.length() > 0)
{
return namespace + "::" + ASTUtils.getName(typeNode); //$NON-NLS-1$
}
return ASTUtils.getName(typeNode);
}
public String getNamespace()
{
return new NamespaceVisitor().getNamespace(getRootNode(), getOffset());
}
/**
* Return the enclosing type's node or the root node if not in a type, used for traversing via visitor to pick up
* variables/methods/etc
*
* @return
*/
public synchronized Node getEnclosingTypeNode()
{
if (enclosingScopeNode == null)
{
Node typeNode = new ClosestSpanningNodeLocator().find(getRootNode(), getOffset(), new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node instanceof ClassNode || node instanceof ModuleNode;
}
});
if (typeNode == null)
{
enclosingScopeNode = getRootNode();
}
else
{
enclosingScopeNode = typeNode;
}
}
return enclosingScopeNode;
}
public Set<String> getLocalsInScope()
{
if (getRootNode() == null)
{
return Collections.emptySet();
}
Set<String> locals = new TreeSet<String>();
Node enclosingMethod = new ClosestSpanningNodeLocator().find(getRootNode(), getOffset(), new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node instanceof MethodDefNode;
}
});
StaticScope scope = null;
if (enclosingMethod == null)
{
scope = ((RootNode) getRootNode()).getStaticScope();
}
else
{
MethodDefNode methodDef = (MethodDefNode) enclosingMethod;
scope = methodDef.getScope();
}
while (scope != null)
{
for (String var : scope.getAllNamesInScope())
{
locals.add(var);
}
scope = scope.getEnclosingScope();
}
return locals;
}
public Node getReceiver()
{
// FIXME We need to grab the receiver of the callnode, which will presumably be at the offset. If source was
// busted because of just a period, then we need to take the node at offset probably
return new OffsetNodeLocator().find(getRootNode(), offset);
}
public Collection<ITypeGuess> inferReceiver()
{
return getTypeInferrer().infer(getRootNode(), getReceiver());
}
protected ITypeInferrer getTypeInferrer()
{
return new TypeInferrer(project);
}
public boolean isNotParseable()
{
return getRootNode() == null;
}
public boolean isSymbol()
{
return getPartialPrefix().length() > 0 && getPartialPrefix().charAt(0) == ':';
}
public Set<String> getSymbolsInAST()
{
Set<String> symbols = new TreeSet<String>();
if (getRootNode() == null)
{
// Fallback to doing a regexp search on the source
Pattern p = Pattern.compile(":(\\w+)\\b"); //$NON-NLS-1$
Matcher m = p.matcher(src);
while (m.find())
{
symbols.add(m.group(1));
}
return symbols;
}
List<Node> symbolNodes = new ScopedNodeLocator().find(getRootNode(), new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node instanceof SymbolNode;
}
});
for (Node node : symbolNodes)
{
// Remove the "symbol" at current offset since that's what we're invoking on!
if (node.getPosition().getStartOffset() <= getOffset() && node.getPosition().getEndOffset() >= getOffset())
{
continue;
}
SymbolNode symbolNode = (SymbolNode) node;
symbols.add(symbolNode.getName());
}
return symbols;
}
}