package org.rubypeople.rdt.internal.codeassist; import java.util.List; import org.jruby.ast.ClassNode; import org.jruby.ast.CommentNode; import org.jruby.ast.MethodDefNode; import org.jruby.ast.ModuleNode; import org.jruby.ast.Node; import org.jruby.parser.RubyParserResult; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.internal.core.RubyScript; import org.rubypeople.rdt.internal.core.parser.RubyParser; import org.rubypeople.rdt.internal.ti.util.ClosestSpanningNodeLocator; import org.rubypeople.rdt.internal.ti.util.INodeAcceptor; public class CompletionContext { private IRubyScript script; 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; public CompletionContext(IRubyScript script, int offset) throws RubyModelException { this.script = script; if (offset < 0) offset = 0; this.offset = offset; replaceStart = offset + 1; try { run(); } catch (RuntimeException e) { RubyCore.log(e); } } private void run() throws RubyModelException { StringBuffer source = new StringBuffer(script.getSource()); 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, "::"); partialPrefix = ""; i--; continue; } } break; } } if (curChar == '.') { isMethodInvokation = true; if (partialPrefix == null) this.partialPrefix = tmpPrefix.toString(); 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, ":"); 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(); Node selected = ClosestSpanningNodeLocator.Instance().findClosestSpanner(getRootNode(), this.offset, new INodeAcceptor() { public boolean doesAccept(Node node) { return true; } }); if (selected == null) { if (fCommentNodes != null) { for (CommentNode comment : fCommentNodes) { if (ClosestSpanningNodeLocator.nodeSpansOffset(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() { return getPartialPrefix() != null && getPartialPrefix().length() > 0 && Character.isUpperCase(getPartialPrefix().charAt(0)); } public int getReplaceStart() { return replaceStart; } /** * Modified source which should not fail parsing. * * @return */ public String getCorrectedSource() { return correctedSource; } public boolean isBroken() { try { return !getCorrectedSource().equals(script.getSource()); } catch (RubyModelException e) { return true; } } public boolean hasReceiver() { return getFullPrefix().indexOf('.') > 1; } /** * The original source * * @return */ public String getSource() { try { return getScript().getSource(); } catch (RubyModelException e) { return ""; } } public String getFullPrefix() { return fullPrefix; } public String getPartialPrefix() { return partialPrefix; } public int getOffset() { return offset; } public IRubyScript getScript() { return script; } 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().startsWith("$"); } public boolean isDoubleSemiColon() { return isAfterDoubleSemiColon && !isMethodInvokation; } public boolean fullPrefixIsConstant() { if (getFullPrefix() == null || getFullPrefix().length() == 0) return false; if (getFullPrefix().endsWith("\".") || getFullPrefix().endsWith("'.")) return false; return Character.isUpperCase(getFullPrefix().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 = ClosestSpanningNodeLocator.Instance().findClosestSpanner(getRootNode(), getOffset(), new INodeAcceptor() { public boolean doesAccept(Node node) { return node instanceof MethodDefNode || node instanceof ClassNode || node instanceof ModuleNode; } }); return spanner instanceof ClassNode || spanner instanceof ModuleNode; } Node getRootNode() { if (fRootNode != null) return fRootNode; RubyParser parser = new RubyParser(); if (!isBroken()) { try { RubyParserResult result = parser.parse(getScript().getElementName(), getSource()); fRootNode = result.getAST(); ((RubyScript) getScript()).lastGoodAST = fRootNode; fCommentNodes = result.getCommentNodes(); } catch (RuntimeException e) { // ignore } } if (fRootNode == null) { try { RubyParserResult result = parser.parse(getCorrectedSource()); fRootNode = result.getAST(); fCommentNodes = result.getCommentNodes(); } catch (RuntimeException e) { // ignore } } if (fRootNode == null) { fRootNode = ((RubyScript) getScript()).lastGoodAST; } 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().startsWith("@") && getPartialPrefix().length() == 1; } /** * We know for certain user is trying to complete an instance var (we have '@[a-z]+') * * @return */ public boolean isInstanceVariable() { return getPartialPrefix() != null && getPartialPrefix().startsWith("@") && !isClassVariable() && getPartialPrefix().length() > 1; } /** * 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("@@"); } }