/** * 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.editor.ruby.internal.contentassist; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.eclipse.core.runtime.PerformanceStats; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.swt.graphics.Image; import org.eclipse.ui.texteditor.HippieProposalProcessor; import org.jruby.Ruby; import org.jrubyparser.ast.ClassNode; import org.jrubyparser.ast.ClassVarAsgnNode; import org.jrubyparser.ast.ClassVarDeclNode; import org.jrubyparser.ast.Colon3Node; import org.jrubyparser.ast.ConstNode; import org.jrubyparser.ast.DefnNode; import org.jrubyparser.ast.InstAsgnNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.ModuleNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.RootNode; import com.aptana.core.util.StringUtil; import com.aptana.editor.common.AbstractThemeableEditor; import com.aptana.editor.common.CommonContentAssistProcessor; import com.aptana.editor.common.contentassist.CommonCompletionProposal; import com.aptana.editor.common.contentassist.CompletionProposalComparator; import com.aptana.editor.ruby.RubyEditorPlugin; import com.aptana.index.core.Index; import com.aptana.index.core.QueryResult; import com.aptana.index.core.SearchPattern; import com.aptana.ruby.core.IRubyConstants; 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.ScopedNodeLocator; import com.aptana.ruby.core.codeassist.CompletionContext; import com.aptana.ruby.core.index.IRubyIndexConstants; import com.aptana.ruby.core.index.RubyIndexUtil; import com.aptana.ruby.core.inference.ITypeGuess; import com.aptana.scripting.model.ContentAssistElement; /** * @author cwilliams */ public class RubyContentAssistProcessor extends CommonContentAssistProcessor { // TODO Move this up the hierarchy? private static final ICompletionProposal[] NO_PROPOSALS = new ICompletionProposal[0]; private static final String NAMESPACE_DELIMITER = IRubyConstants.NAMESPACE_DELIMETER; public static final Image KEYWORD_IMAGE = RubyEditorPlugin.getImage("icons/keyword.png"); public static final Image PROPERTY_IMAGE = RubyEditorPlugin.getImage("icons/property.png"); /** * Performance events */ private static final String CALC_SUPER_TYPE_EVENT = RubyEditorPlugin.PLUGIN_ID + "/perf/content_assist/calc_hierarchy"; //$NON-NLS-1$ private static final String METHOD_PROPOSALS_FOR_TYPE_EVENT = RubyEditorPlugin.PLUGIN_ID + "/perf/content_assist/type_methods"; //$NON-NLS-1$ /** * Static list of ruby keywords */ @SuppressWarnings("nls") private static final String[] KEYWORDS = new String[] { "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined", "do", "else", "elsif", "END", "end", "ensure", "false", "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", "until", "when", "while", "yield" }; private CompletionContext fContext; public RubyContentAssistProcessor(AbstractThemeableEditor editor) { super(editor); } protected java.util.Collection<? extends ICompletionProposal> addRubleCAProposals(ITextViewer viewer, int offset, Ruby ruby, ContentAssistElement ce) { // HACK to force not to use the ruby ruble CA in case it's still around... if ("Type Inference code assist".equals(ce.getDisplayName())) //$NON-NLS-1$ { return Collections.emptyList(); } return super.addRubleCAProposals(viewer, offset, ruby, ce); }; @Override protected ICompletionProposal[] doComputeCompletionProposals(ITextViewer viewer, int offset, char activationChar, boolean autoActivated) { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); fContext = createCompletionContext(viewer, offset); try { if (fContext.inComment()) { return NO_PROPOSALS; } // order matters. we can handle symbols even if we can't parse else if (fContext.isSymbol()) { proposals.addAll(suggestSymbols()); } else if (fContext.isNotParseable()) { proposals.addAll(suggestKeywords()); } else if (fContext.emptyPrefix()) { // JDT shows locals in method then fields and constants for enclosing type, then methods for // enclosing type proposals.addAll(suggestLocalVariables()); proposals.addAll(suggestInstanceVariables()); proposals.addAll(suggestClassVariables()); proposals.addAll(suggestMethodsForEnclosingType()); } else if (fContext.isConstant()) { // JDT suggests constants, then types proposals.addAll(suggestConstantsInNamespace()); proposals.addAll(suggestTypeNames()); } else if (fContext.isGlobal()) { proposals.addAll(suggestGlobals()); } else if (fContext.isInstanceVariable()) { proposals.addAll(suggestInstanceVariables()); } else if (fContext.isClassVariable()) { proposals.addAll(suggestClassVariables()); } else if (fContext.isInstanceOrClassVariable()) { proposals.addAll(suggestInstanceVariables()); proposals.addAll(suggestClassVariables()); } else if (fContext.isDoubleColon()) { // This is either a qualified type name, method call, or qualified constant name // When after last "::", if empty or uppercase, it's a type or constant if (fContext.getPartialPrefix().length() == 0 || Character.isUpperCase(fContext.getPartialPrefix().charAt(0))) { proposals.addAll(suggestTypesInNamespace()); proposals.addAll(suggestConstantsInNamespace()); } proposals.addAll(suggestMethodsOnReceiver()); } else if (fContext.isExplicitMethodInvokation()) { proposals.addAll(suggestMethodsOnReceiver()); } else if (fContext.isMethodInvokationOrLocal()) { proposals.addAll(suggestKeywords()); // JDT suggests locals, then methods proposals.addAll(suggestLocalVariables()); proposals.addAll(suggestMethodsForEnclosingType()); // Add word completions to round it out // Don't suggest duplicates of methods/locals suggested! // Collection<? extends ICompletionProposal> wordCompletions = suggestWordCompletions(viewer, offset); // wordCompletions = removeDuplicates(proposals, wordCompletions); // proposals.addAll(wordCompletions); } if(proposals==null||proposals.size()<1){ return computeWordCompletionProposals(offset, PROPERTY_IMAGE, null); } sortByDisplayName(proposals); return proposals.toArray(new ICompletionProposal[proposals.size()]); } finally { fContext = null; } } protected CompletionContext createCompletionContext(ITextViewer viewer, int offset) { return new CompletionContext(getProject(), viewer.getDocument().get(), offset - 1); } private Collection<? extends ICompletionProposal> suggestMethodsOnReceiver() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Collection<ITypeGuess> guesses = fContext.inferReceiver(); Map<String, Boolean> typeNames = new HashMap<String, Boolean>(); Set<String> receiverTypes = new HashSet<String>(); for (ITypeGuess guess : guesses) { String typeName = guess.getType(); receiverTypes.add(typeName); typeNames.put(typeName, guess.isModule()); // Include supertypes typeNames.putAll(calculateSuperTypes(typeName)); } // Based on what the receiver is (if it's a type name) we should toggle instance/singleton // methods! boolean receiverIsType = receiverIsType(); proposals.addAll(suggestMethodsForTypes(receiverTypes, typeNames, receiverIsType, !receiverIsType, receiverIsType, receiverIsType)); if (receiverIsType) { // TODO Insert the class name as the "location"? proposals.add(createProposal("new", PROPERTY_IMAGE)); //$NON-NLS-1$ } return proposals; } private Collection<? extends ICompletionProposal> suggestKeywords() { List<ICompletionProposal> keywords = new ArrayList<ICompletionProposal>(); String prefix = fContext.getPartialPrefix(); for (String keyword : KEYWORDS) { if (keyword.startsWith(prefix)) { keywords.add(createProposal(keyword, KEYWORD_IMAGE)); } } return keywords; } private Collection<? extends ICompletionProposal> suggestSymbols() { // TODO We currently only suggest symbols in the same file. Should we look at all symbols in the project? List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); for (String symbolName : fContext.getSymbolsInAST()) { CommonCompletionProposal proposal = createProposal( ":" + symbolName, PROPERTY_IMAGE); //$NON-NLS-1$ proposals.add(proposal); } return proposals; } private boolean receiverIsType() { String constantName = null; String namespace = StringUtil.EMPTY; String typeName = StringUtil.EMPTY; Node receiver = fContext.getReceiver(); if (receiver instanceof Colon3Node || receiver instanceof ConstNode) { // FIXME If the receiver as text equals the inferred type, then it's a type... That's probably way // quicker... String fullName = ASTUtils.getFullyQualifiedName(receiver); constantName = fullName; int namespaceIndex = fullName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER); if (namespaceIndex != -1) { typeName = fullName.substring(0, namespaceIndex); constantName = fullName.substring(namespaceIndex + 2); namespaceIndex = typeName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER); if (namespaceIndex != -1) { namespace = typeName.substring(0, namespaceIndex); typeName = typeName.substring(namespaceIndex + 2); } } } else { return false; } // Check the indices to see if this is a constant or a type! If constant, we need to infer that // constant decl! final String key = constantName + IRubyIndexConstants.SEPARATOR + typeName + IRubyIndexConstants.SEPARATOR + namespace; for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.CONSTANT_DECL }, key, SearchPattern.EXACT_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null || results.isEmpty()) { continue; } // Found at least one match, assume that the receiver isn't a constant... return false; } return true; } /** * Remove word completions that already exist as local or method proposals. * * @param proposals * @param wordCompletions * @return */ private Collection<? extends ICompletionProposal> removeDuplicates(List<ICompletionProposal> proposals, Collection<? extends ICompletionProposal> wordCompletions) { if (wordCompletions == null || wordCompletions.isEmpty()) { return wordCompletions; } List<ICompletionProposal> uniques = new ArrayList<ICompletionProposal>(); Set<String> displayStrings = new HashSet<String>(); for (ICompletionProposal proposal : proposals) { displayStrings.add(proposal.getDisplayString()); } for (ICompletionProposal woProposal : wordCompletions) { if (!displayStrings.contains(woProposal.getDisplayString())) { uniques.add(woProposal); } } return uniques; } /** * Suggests possible types that live under a namespace matching the full prefix. * * @return */ @SuppressWarnings("nls") private Collection<? extends ICompletionProposal> suggestTypesInNamespace() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); String fullPrefix = fContext.getFullPrefix(); String namespace = fullPrefix.substring(0, fullPrefix.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER)); String key = "^[^/]+?" + IRubyIndexConstants.SEPARATOR + namespace + "[^/]*?" + IRubyIndexConstants.SEPARATOR + ".+$"; Map<String, Boolean> proposalToIsClass = new HashMap<String, Boolean>(); for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.TYPE_DECL }, key, SearchPattern.REGEX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { String aNamespace = getNamespace(result.getWord()); if (namespace.equals(aNamespace)) { // Exact namespace match. Suggest the simple type name String typeName = getTypeName(result.getWord()); proposalToIsClass.put(typeName, true); } else if (aNamespace.startsWith(fullPrefix)) { // Suggest next segment of aNamespace int previousDelim = aNamespace.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER, fullPrefix.length()); if (previousDelim == -1) { previousDelim = 0; } else { previousDelim += 2; } int nextDelim = aNamespace.indexOf(IRubyConstants.NAMESPACE_DELIMETER, fullPrefix.length()); if (nextDelim == -1) { nextDelim = aNamespace.length(); } String nextSegment = aNamespace.substring(previousDelim, nextDelim); proposalToIsClass.put(nextSegment, false); } } } // Enforce unique proposals by using map for (Map.Entry<String, Boolean> entry : proposalToIsClass.entrySet()) { proposals.add(createProposal(entry.getKey(), PROPERTY_IMAGE)); } return proposals; } /** * Suggests constants living under the given namespace in prefix. * * @return */ @SuppressWarnings("nls") private Collection<? extends ICompletionProposal> suggestConstantsInNamespace() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); String typeName = IRubyConstants.OBJECT; String namespace = StringUtil.EMPTY; String fullPrefix = fContext.getFullPrefix(); if (!fullPrefix.startsWith(IRubyConstants.NAMESPACE_DELIMETER)) { // FIXME We also want to search without the implicit namespace! // tack on current namespace to beginning, since we're not explicitly forcing toplevel... String implicitNamespace = fContext.getNamespace(); if (implicitNamespace.length() > 0) { fullPrefix = implicitNamespace + IRubyConstants.NAMESPACE_DELIMETER + fullPrefix; } } int lastNS = fullPrefix.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER); if (lastNS > 0) { typeName = fullPrefix.substring(0, lastNS); } lastNS = typeName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER); if (lastNS > 0) { namespace = typeName.substring(0, lastNS); typeName = typeName.substring(lastNS + 2); } StringBuilder builder = new StringBuilder(); builder.append('^'); // begin matching at start of key builder.append(fContext.getPartialPrefix()).append("[^/]*?"); // match prefix plus any normal chars for constant // name builder.append(IRubyIndexConstants.SEPARATOR); // builder.append('('); // Match defining type... builder.append(typeName); // defining type name builder.append(IRubyIndexConstants.SEPARATOR); builder.append(namespace); // or match in toplevel // builder.append('|'); // builder.append(IRubyIndexConstants.OBJECT); // builder.append(IRubyIndexConstants.SEPARATOR); // // no namespace in toplevel // builder.append(')'); builder.append('$'); // end matching at end of key // We search the given namespace and enclosing type name, but we also include constants in the top level String key = builder.toString(); for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.CONSTANT_DECL }, key, SearchPattern.REGEX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { String indexKey = result.getWord(); proposals.add(createProposal(indexKey.substring(0, indexKey.indexOf(IRubyIndexConstants.SEPARATOR)), PROPERTY_IMAGE)); } } return proposals; } protected void sortByDisplayName(List<ICompletionProposal> proposals) { // Sort by display string, ignoring case Collections.sort(proposals, new Comparator<ICompletionProposal>() { public int compare(ICompletionProposal o1, ICompletionProposal o2) { return o1.getDisplayString().compareToIgnoreCase(o2.getDisplayString()); } }); } protected Collection<? extends ICompletionProposal> suggestWordCompletions(ITextViewer viewer, int offset) { ICompletionProposal[] hippieProposals = new HippieProposalProcessor() .computeCompletionProposals(viewer, offset); if (hippieProposals == null || hippieProposals.length == 0) { return Collections.emptyList(); } return Arrays.asList(hippieProposals); } private Collection<? extends ICompletionProposal> suggestLocalVariables() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); String prefix = fContext.getPartialPrefix(); for (String localName : fContext.getLocalsInScope()) { if (localName.startsWith(prefix)) { CommonCompletionProposal proposal = createProposal(localName, PROPERTY_IMAGE); proposals.add(proposal); } } return proposals; } private Collection<? extends ICompletionProposal> suggestInstanceVariables() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Node enclosing = fContext.getEnclosingTypeNode(); List<Node> instAssignments = new ScopedNodeLocator().find(enclosing, new INodeAcceptor() { public boolean accepts(Node node) { return node instanceof InstAsgnNode; } }); for (Node assign : instAssignments) { String instanceVarName = ASTUtils.getName(assign); CommonCompletionProposal proposal = createProposal(instanceVarName, PROPERTY_IMAGE); proposals.add(proposal); } return proposals; } private Collection<? extends ICompletionProposal> suggestMethodsForEnclosingType() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Node enclosing = fContext.getEnclosingTypeNode(); String enclosingTypeName = fContext.getEnclosingType(); // If we're inside an instance method or toplevel, include instance method proposals boolean includeInstance = false; if (enclosing instanceof RootNode) { includeInstance = true; } else { MethodDefNode enclosingMethod = (MethodDefNode) new ClosestSpanningNodeLocator().find( fContext.getRootNode(), fContext.getOffset(), new INodeAcceptor() { public boolean accepts(Node node) { return node instanceof MethodDefNode; } }); if (enclosingMethod instanceof DefnNode) { includeInstance = true; } } // Use AST for proposals List<Node> methodDefNodes = new ScopedNodeLocator().find(enclosing, new INodeAcceptor() { public boolean accepts(Node node) { return node instanceof MethodDefNode; } }); for (Node methodDefNode : methodDefNodes) { // TODO Determine visibility of the method! String methodName = ASTUtils.getName(methodDefNode); if (!methodName.startsWith(fContext.getPartialPrefix())) { continue; } // Verify method and current location are in same scope... Node enclosingScopeNode = new ClosestSpanningNodeLocator().find(fContext.getRootNode(), methodDefNode .getPosition().getStartOffset(), new INodeAcceptor() { public boolean accepts(Node node) { return node instanceof ClassNode || node instanceof ModuleNode || node instanceof RootNode; } }); if (!enclosingScopeNode.equals(enclosing)) { continue; } CommonCompletionProposal proposal = createProposal(methodName, PROPERTY_IMAGE, enclosingTypeName); proposals.add(proposal); } // Calculate the type hierarchy Map<String, Boolean> allTypes = new HashMap<String, Boolean>(); allTypes.put(enclosingTypeName, enclosing instanceof ModuleNode); if (enclosing instanceof ClassNode) { ClassNode classNode = (ClassNode) enclosing; Node superNode = classNode.getSuperNode(); // Need to also include suggestions against index normally! String superTypeName = IRubyConstants.OBJECT; if (superNode != null) { superTypeName = ASTUtils.getFullyQualifiedName(superNode); } allTypes.put(superTypeName, false); // Need to suggest methods up the hierarchy too... PerformanceStats stats = null; if (PerformanceStats.isEnabled(CALC_SUPER_TYPE_EVENT)) { stats = PerformanceStats.getStats(CALC_SUPER_TYPE_EVENT, this); stats.startRun(superTypeName); } allTypes.putAll(calculateSuperTypes(superTypeName)); if (stats != null) { stats.endRun(); stats = null; } } else { // Toplevel or Module // Need to suggest methods up the hierarchy too... PerformanceStats stats = null; if (PerformanceStats.isEnabled(CALC_SUPER_TYPE_EVENT)) { stats = PerformanceStats.getStats(CALC_SUPER_TYPE_EVENT, this); stats.startRun(enclosingTypeName); } allTypes.putAll(calculateSuperTypes(enclosingTypeName)); if (stats != null) { stats.endRun(); stats = null; } } // Now get method proposals up the hierarchy! PerformanceStats stats = null; if (PerformanceStats.isEnabled(METHOD_PROPOSALS_FOR_TYPE_EVENT)) { stats = PerformanceStats.getStats(METHOD_PROPOSALS_FOR_TYPE_EVENT, this); stats.startRun(allTypes.toString()); } // FIXME We want to include private methods for the enclosing type, but not the supertypes! Right now we just // include them all the time Set<String> receiverTypes = new HashSet<String>(); receiverTypes.add(enclosingTypeName); proposals .addAll(suggestMethodsForTypes(receiverTypes, allTypes, !includeInstance, includeInstance, true, true)); if (stats != null) { stats.endRun(); stats = null; } return proposals; } /** * Bulk query the index for all methods starting with prefix on the set of fully qualified type names passed in. * This generates a complex regexp pattern to use. * * @param receiverTypes * Possible receiver types inferred or statically determined. * @param typeNames * Map from type name to boolean indicating if type is a class (false) or module (true) * @param includeSingleton * @param includeInstance * @param includeProtected * @return */ @SuppressWarnings("nls") private Collection<? extends ICompletionProposal> suggestMethodsForTypes(Set<String> receiverTypes, Map<String, Boolean> typeNames, boolean includeSingleton, boolean includeInstance, boolean includeProtected, boolean includePrivate) { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Set<String> possibles = new HashSet<String>(); for (String typeName : typeNames.keySet()) { String simpleName = typeName; String namespace = StringUtil.EMPTY; int lastDelim = typeName.lastIndexOf(NAMESPACE_DELIMITER); if (lastDelim != -1) { namespace = typeName.substring(0, lastDelim); simpleName = typeName.substring(lastDelim + 2); } possibles.add(simpleName + IRubyIndexConstants.SEPARATOR + namespace); } StringBuilder keyBuilder = new StringBuilder(); // method prefix typed... keyBuilder.append('^'); keyBuilder.append(fContext.getPartialPrefix()); keyBuilder.append("[^/]*?"); keyBuilder.append(IRubyIndexConstants.SEPARATOR); // All the possible types keyBuilder.append('('); for (String possible : possibles) { keyBuilder.append(possible); keyBuilder.append('|'); } keyBuilder.deleteCharAt(keyBuilder.length() - 1); keyBuilder.append(')'); keyBuilder.append(IRubyIndexConstants.SEPARATOR); // visibility keyBuilder.append("(P"); if (includeProtected) { keyBuilder.append("|R"); } if (includePrivate) { keyBuilder.append("|V"); } keyBuilder.append(')'); keyBuilder.append(IRubyIndexConstants.SEPARATOR); // singleton/instance if (includeInstance && includeSingleton) { keyBuilder.append("(S|I)"); } else if (includeInstance) { keyBuilder.append('I'); } else if (includeSingleton) { keyBuilder.append('S'); } keyBuilder.append(IRubyIndexConstants.SEPARATOR); // Followed by whatever number of args keyBuilder.append("[^/]*$"); final String key = keyBuilder.toString(); for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.METHOD_DECL }, key, SearchPattern.REGEX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { String typeNameInKey = getTypeNameFromMethodDefKey(result.getWord()); if (includeSingleton) { // If type is a module and the method is a singleton, // don't add unless the receiver is that module boolean isModule = typeNames.get(typeNameInKey); boolean isSingletonMethod = isSingletonMethodInKey(result.getWord()); if (isSingletonMethod && isModule && !receiverTypes.contains(typeNameInKey)) { continue; } } String methodName = getMethodNameFromMethodDefKey(result.getWord()); proposals.add(createProposal(methodName, PROPERTY_IMAGE, typeNameInKey)); } } return proposals; } private boolean isSingletonMethodInKey(String word) { String[] parts = word.split(Character.toString(IRubyIndexConstants.SEPARATOR)); String singletonOrInstance = parts[4]; if (singletonOrInstance != null && singletonOrInstance.length() > 0) { char c = singletonOrInstance.charAt(0); switch (c) { case 'S': return true; case 'I': return false; } } return false; } protected Collection<Index> allIndicesForProject() { return RubyIndexUtil.allIndices(getProject()); } private String getTypeNameFromMethodDefKey(String word) { String[] parts = word.split(Character.toString(IRubyIndexConstants.SEPARATOR)); String simpleName = parts[1]; String namespace = parts[2]; if (namespace != null && namespace.length() > 0) { return namespace + IRubyConstants.NAMESPACE_DELIMETER + simpleName; } return simpleName; } private String getMethodNameFromMethodDefKey(String word) { return word.substring(0, word.indexOf(IRubyIndexConstants.SEPARATOR)); } private void trace(String format) { // System.out.println(format); } private Collection<? extends ICompletionProposal> suggestClassVariables() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Node enclosing = fContext.getEnclosingTypeNode(); List<Node> assignments = new ScopedNodeLocator().find(enclosing, new INodeAcceptor() { public boolean accepts(Node node) { return node instanceof ClassVarAsgnNode || node instanceof ClassVarDeclNode; } }); for (Node assign : assignments) { String varName = ASTUtils.getName(assign); CommonCompletionProposal proposal = createProposal(varName, PROPERTY_IMAGE); proposals.add(proposal); } return proposals; } private List<ICompletionProposal> suggestGlobals() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Set<String> globalNames = new TreeSet<String>(); for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.GLOBAL_DECL }, fContext.getPartialPrefix(), SearchPattern.PREFIX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { globalNames.add(result.getWord()); } } for (String globalName : globalNames) { CommonCompletionProposal proposal = createProposal(globalName, PROPERTY_IMAGE); proposals.add(proposal); } return proposals; } private Collection<? extends ICompletionProposal> suggestTypeNames() { List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); Set<String> typeKeys = new TreeSet<String>(); for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.TYPE_DECL }, fContext.getPartialPrefix(), SearchPattern.PREFIX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { typeKeys.add(result.getWord()); } } for (String typeKey : typeKeys) { String typeName = getTypeName(typeKey); CommonCompletionProposal proposal = createProposal(typeName, PROPERTY_IMAGE); proposals.add(proposal); } return proposals; } /** * Grab the simple type name out of the type declaration index key. * * @param typeKey * @return */ private String getTypeName(String typeKey) { return new String(typeKey.substring(0, typeKey.indexOf(IRubyIndexConstants.SEPARATOR))); } /** * Grab the namespace out of the type declaration index key. * * @param typeKey * @return */ private String getNamespace(String typeKey) { int firstSep = typeKey.indexOf(IRubyIndexConstants.SEPARATOR); return new String(typeKey.substring(firstSep + 1, typeKey.indexOf(IRubyIndexConstants.SEPARATOR, firstSep + 1))); } protected CommonCompletionProposal createProposal(String name, Image image) { return createProposal(name, image, null); } protected CommonCompletionProposal createProposal(String name, Image image, String location) { CommonCompletionProposal proposal = new CommonCompletionProposal(name, fContext.getReplaceStart(), fContext .getPartialPrefix().length(), name.length(), image, name, null, null); if (location != null) { proposal.setFileLocation(location); } proposal.setTriggerCharacters(getProposalTriggerCharacters()); return proposal; } /** * Returns a map containing all the super types mapped to a boolean indicating module (true) or class (false) * * @param typeName * @return */ @SuppressWarnings("nls") private Map<String, Boolean> calculateSuperTypes(String typeName) { Map<String, Boolean> typeNames = new HashMap<String, Boolean>(); if (typeName == null) { return typeNames; } if (IRubyConstants.OBJECT.equals(typeName)) { typeNames.put("Kernel", true); return typeNames; } // Break type_name up into type name and namespace... String simpleName = typeName; String namespace = StringUtil.EMPTY; int lastDelim = typeName.lastIndexOf(NAMESPACE_DELIMITER); if (lastDelim != -1) { namespace = typeName.substring(0, lastDelim); simpleName = typeName.substring(lastDelim + 2); } // For performance reasons we don't recurse on modules Map<String, Boolean> moduleNames = new HashMap<String, Boolean>(); final String key = "^[^/]*" + IRubyIndexConstants.SEPARATOR + "[^/]*" + IRubyIndexConstants.SEPARATOR + simpleName + IRubyIndexConstants.SEPARATOR + namespace + IRubyIndexConstants.SEPARATOR + ".*$"; // Take the type name and find all the super types and included modules for (Index index : allIndicesForProject()) { if (index == null) { continue; } List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.SUPER_REF }, key, SearchPattern.REGEX_MATCH | SearchPattern.CASE_SENSITIVE); if (results == null) { continue; } for (QueryResult result : results) { char classOrModule = result.getWord().charAt(result.getWord().length() - 2); if (IRubyIndexConstants.MODULE_SUFFIX == classOrModule) { moduleNames.put(getTypeNameFromSuperRefKey(result.getWord()), true); } else { typeNames.put(getTypeNameFromSuperRefKey(result.getWord()), false); } } } trace(MessageFormat.format("Supertypes of {0}: {1}", typeName, typeNames)); trace(MessageFormat.format("Included modules: {0}", moduleNames)); // Now grab all the supertypes of these super types! RECURSION!!!!1!1! Map<String, Boolean> typeNamesCopy = new HashMap<String, Boolean>(typeNames); for (String superType : typeNamesCopy.keySet()) { typeNames.putAll(calculateSuperTypes(superType)); } typeNames.putAll(moduleNames); return typeNames; } /** * Returns the fully qualified type name stored in the Super Reference index key. * * @param superRefKey * @return */ private String getTypeNameFromSuperRefKey(String superRefKey) { int firstSep = superRefKey.indexOf(IRubyIndexConstants.SEPARATOR); String simpleName = superRefKey.substring(0, firstSep); String namespace = superRefKey.substring(firstSep + 1, superRefKey.indexOf(IRubyIndexConstants.SEPARATOR, firstSep + 1)); if (namespace.length() == 0) { return simpleName; } return namespace + NAMESPACE_DELIMITER + simpleName; } /* * (non-Javadoc) * @see com.aptana.editor.common.CommonContentAssistProcessor#getPreferenceNodeQualifier() */ protected String getPreferenceNodeQualifier() { return RubyEditorPlugin.PLUGIN_ID; } /** * Sorts the completion proposals (by default, by display string). This inclusion is temporary as Ruby may wish to * pursue another mechanism. * * @param proposals */ protected void sortProposals(ICompletionProposal[] proposals) { // Sort by relevance first, descending, and then alphabetically, ascending Arrays.sort(proposals, CompletionProposalComparator.decending(CompletionProposalComparator.getComparator( CompletionProposalComparator.NameSort, CompletionProposalComparator.TemplateSort))); } }