package org.radrails.rails.internal.ui.text; import java.io.File; import java.io.FilenameFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.jruby.ast.RootNode; import org.jruby.lexer.yacc.SyntaxException; import org.radrails.rails.core.Inflector; import org.radrails.rails.internal.core.RailsPlugin; import org.radrails.rails.ui.RailsUILog; import org.radrails.rails.ui.text.RailsHeuristicCompletionComputer; import org.rubypeople.rdt.core.CompletionProposal; import org.rubypeople.rdt.core.Flags; import org.rubypeople.rdt.core.IMethod; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.IType; 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.SearchEngine; 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.codeassist.CompletionContext; import org.rubypeople.rdt.internal.codeassist.RubyElementRequestor; import org.rubypeople.rdt.internal.core.LogicalType; import org.rubypeople.rdt.internal.core.RubyScript; import org.rubypeople.rdt.internal.core.parser.RubyParser; import org.rubypeople.rdt.internal.ti.ITypeGuess; import org.rubypeople.rdt.internal.ti.ITypeInferrer; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.rubyeditor.ASTProvider; import org.rubypeople.rdt.internal.ui.text.ruby.RubyCompletionProposal; import org.rubypeople.rdt.internal.ui.text.ruby.RubyContentAssistInvocationContext; import org.rubypeople.rdt.ui.text.ruby.CompletionProposalCollector; import org.rubypeople.rdt.ui.text.ruby.ContentAssistInvocationContext; import org.rubypeople.rdt.ui.text.ruby.IRubyCompletionProposal; import org.rubypeople.rdt.ui.text.ruby.IRubyCompletionProposalComputer; public class RailsCompletionProposalComputer implements IRubyCompletionProposalComputer { private ContentAssistInvocationContext fContext; private static final String[] TableNameFirstArgs = new String[] { "rename_column", "drop_table", "remove_column", "remove_index", "add_column", "add_index", "change_column", "create_table" }; private static final String[] ColumnNameSecondArgs = new String[] { "rename_column", "remove_column", "add_index", "change_column" }; public List computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) { this.fContext = context; List<ICompletionProposal> list = new ArrayList<ICompletionProposal>(); // Completions for :controller, or :action Map<String, File> completions = RailsHeuristicCompletionComputer.getControllerCompletions( getControllersFolder(context), context.getDocument(), context.getInvocationOffset()); completions.putAll(RailsHeuristicCompletionComputer.getActionCompletions(getControllersFolder(context), context .getDocument(), context.getInvocationOffset())); for (String replacement : completions.keySet()) { ICompletionProposal prop = new RubyCompletionProposal(replacement, context.getInvocationOffset(), 0, null, replacement, 10000); list.add(prop); } if (context instanceof RubyContentAssistInvocationContext) { RubyContentAssistInvocationContext rubyContext = (RubyContentAssistInvocationContext) context; IType type = getInferredActiveRecord(rubyContext); if (type != null) { list.addAll(addActiveRecordFieldMethods(type, rubyContext)); // Completions // for // db // fields // / // finders // on // ActiveRecord // models list.addAll(addActiveRecordAssociations(type, rubyContext)); // Completions // for // associations // of // model } list.addAll(addMigrationMethods(rubyContext)); // Completions for // methods available // in migrations list.addAll(addMigrationMethodArgumentSuggestions(rubyContext)); } fContext = null; return list; } private String getArgumentsToMethodCall() { String prefix = getStatementPrefix(); String methodCall = getMethodName(); String args = prefix.trim().substring(methodCall.length()); if (args.startsWith("(")) args = args.substring(1); return args; } private String getMethodName() { String prefix = getStatementPrefix(); String methodCall = prefix.trim(); int space = methodCall.indexOf(" "); if (space != -1) { methodCall = methodCall.substring(0, space); } space = methodCall.indexOf("("); if (space != -1) { methodCall = methodCall.substring(0, space); } return methodCall; } private Collection<? extends ICompletionProposal> addMigrationMethodArgumentSuggestions( RubyContentAssistInvocationContext context) { IRubyScript script = getScript(context); if (!isDBMigration(script)) return Collections.emptyList(); CompletionProposalCollector completion = createCollector(context); String methodName = getMethodName(); String args = getArgumentsToMethodCall(); int argumentIndex = calculateArgIndex(args); if (contains(methodName, TableNameFirstArgs)) { // offer up the table names for first arg if (argumentIndex == 0) { Collection<String> tableNames = getDBTableNames(script); for (String tableName : tableNames) { if (!tableName.startsWith(":")) tableName = ":" + tableName; if (tableName.equals(":table_name")) continue; CompletionProposal proposal = new CompletionProposal(CompletionProposal.KEYWORD, tableName, 201); proposal.setName(tableName); int start = context.getInvocationOffset(); proposal.setReplaceRange(start, start + tableName.length()); completion.accept(proposal); } } } if (contains(methodName, ColumnNameSecondArgs)) { if (argumentIndex == 1) { // suggest field names! String tableName = getArgAt(0, args).trim().substring(1); // drop // the // ":" Collection<String> fieldNames = getDBFieldNames(script, Inflector.singularize(tableName)); for (String fieldName : fieldNames) { fieldName = "'" + fieldName + "'"; CompletionProposal proposal = new CompletionProposal(CompletionProposal.KEYWORD, fieldName, 201); proposal.setName(fieldName); int start = context.getInvocationOffset(); proposal.setReplaceRange(start, start + fieldName.length()); completion.accept(proposal); } } } return Arrays.asList(completion.getRubyCompletionProposals()); } private boolean contains(String methodName, String[] array) { for (int i = 0; i < array.length; i++) { if (array[i].equals(methodName)) return true; } return false; } private String getArgAt(int i, String argsRaw) { String[] args = argsRaw.split(","); return args[i]; } private int calculateArgIndex(String prefix) { // TODO Auto-generated method stub String[] args = prefix.split(","); if (args.length == 1) { if (prefix.indexOf(",") == -1) return 0; return 1; } return args.length; } private Collection<? extends ICompletionProposal> addActiveRecordAssociations(IType type, RubyContentAssistInvocationContext context) { if (type == null) return Collections.emptyList(); IRubyScript script = type.getRubyScript(); RootNode ast = ASTProvider.getASTProvider().getAST(script, ASTProvider.WAIT_YES, new NullProgressMonitor()); if (ast == null) return Collections.emptyList(); ActiveRecordAssociationsVisitor visitor = new ActiveRecordAssociationsVisitor(); ast.accept(visitor); List<IMethod> fields = visitor.getMethods(); CompletionProposalCollector collector = createCollector(context); for (IMethod method : fields) { collector.accept(createProposal(context, type.getElementName(), method)); } return Arrays.asList(collector.getRubyCompletionProposals()); } protected CompletionProposalCollector createCollector(RubyContentAssistInvocationContext context) { return new CompletionProposalCollector(context); } private Collection<? extends ICompletionProposal> addMigrationMethods(RubyContentAssistInvocationContext context) { if (getStatementPrefix().trim().length() > 0) return Collections.emptyList(); IRubyScript script = getScript(context); if (!isDBMigration(script)) return Collections.emptyList(); CompletionProposalCollector completion = createCollector(context); String typeName = "ActiveRecord::ConnectionAdapters::SchemaStatements"; List<IType> types = findTypeDeclarations(typeName, script); try { for (IType type : types) { IMethod[] methods = type.getMethods(); for (int i = 0; i < methods.length; i++) { IMethod method = methods[i]; if (method == null || !method.isPublic()) continue; completion.accept(createProposal(context, typeName, method)); } } } catch (CoreException e) { RailsUILog.log(e); } return Arrays.asList(completion.getRubyCompletionProposals()); } private boolean isDBMigration(IRubyScript script) { if (script == null) return false; IPath path = script.getPath(); return getMigrationPath(script).isPrefixOf(path); } private IPath getMigrationPath(IRubyScript script) { if (script == null) return null; IPath railsRoot = RailsPlugin.findRailsRoot(script.getRubyProject().getProject()); return script.getRubyProject().getPath().append(railsRoot).append("db").append("migrate"); } private List<IType> findTypeDeclarations(String typeName, IRubyScript script) { List<IType> types = new ArrayList<IType>(); try { SearchEngine engine = new SearchEngine(); SearchPattern pattern = SearchPattern.createPattern(IRubyElement.TYPE, typeName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }; IRubySearchScope scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script.getRubyProject() }); CollectingSearchRequestor requestor = new CollectingSearchRequestor(); engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); List<SearchMatch> matches = requestor.getResults(); for (SearchMatch match : matches) { IType type = (IType) match.getElement(); types.add(type); } } catch (CoreException e) { RailsUILog.log(e); } return types; } /** * Create a method completion proposal. * * @param context * @param typeName * @param method * @return */ private CompletionProposal createProposal(ContentAssistInvocationContext context, String typeName, IMethod method) { String methodName = method.getElementName(); CharSequence prefix = ""; try { prefix = context.computeIdentifierPrefix(); } catch (BadLocationException e) { RailsUILog.log(e); } if (!methodName.startsWith(prefix.toString())) return null; CompletionProposal proposal = new CompletionProposal(CompletionProposal.METHOD_REF, methodName, 101); // 101 // relevance // to push // it over // the // standard // completions proposal.setDeclaringType(typeName); proposal.setElement(method); proposal.setName(methodName); int flags = Flags.AccPublic; if (method.isSingleton()) flags = flags | Flags.AccStatic; proposal.setFlags(flags); int start = context.getInvocationOffset() - prefix.length(); proposal.setReplaceRange(start, start + methodName.length()); return proposal; } /** * Grab all ITypes inferred to be our receiver * * @param context * @return */ private List<IType> inferType(RubyContentAssistInvocationContext context) { List<IType> inferred = new ArrayList<IType>(); IRubyScript script = getScript(context); if (script == null) return inferred; try { CompletionContext myContext = new CompletionContext(script, context.getInvocationOffset() - 1); Collection<ITypeGuess> guesses = getTypeInferrer().infer(myContext.getCorrectedSource(), myContext.getOffset()); if (guesses == null) return inferred; RubyElementRequestor requestor = new RubyElementRequestor(script); for (ITypeGuess guess : guesses) { IType[] types = requestor.findType(guess.getType()); if (types != null && types.length > 0) inferred.add(new LogicalType(types)); } } catch (RubyModelException e) { RailsUILog.log(e); } return inferred; } protected ITypeInferrer getTypeInferrer() { return RubyCore.getTypeInferrer(); } /** * Infer the receiver, and if it could possibly be an ActiveRecord, return that type guess. * * @param context * @return an IType of an ActiveRecord model that we have inferred may be our receiver */ private IType getInferredActiveRecord(RubyContentAssistInvocationContext context) { List<IType> types = inferType(context); for (IType type : types) { try { // FIXME Check up the entire hierarchy! if ("ActiveRecord::Base".equals(type.getSuperclassName())) return type; } catch (RubyModelException e) { RailsUILog.log(e); } } return null; } /** * Infer the type we're being invoked on. If it's an ActiveRecord model, then suggest each DB field (as defined in * migrations) accessor, writer and find_by finder method. * * @param context * @return */ private List<IRubyCompletionProposal> addActiveRecordFieldMethods(IType type, RubyContentAssistInvocationContext context) { if (type == null) return Collections.EMPTY_LIST; CompletionProposalCollector collector = createCollector(context); Set<String> fieldNames = getDBFieldNames(getScript(context), type.getElementName()); for (String fieldName : fieldNames) { // add accessor collector.accept(createProposal(context, type.getElementName(), new PsuedoMethod(fieldName, null, Flags.AccPublic))); // add writer collector.accept(createProposal(context, type.getElementName(), new PsuedoMethod(fieldName + "=", new String[] { fieldName }, Flags.AccPublic))); // add dynamic finders collector.accept(createProposal(context, type.getElementName(), new PsuedoMethod("find_by_" + fieldName, new String[] { fieldName }, Flags.AccPublic | Flags.AccStatic))); collector.accept(createProposal(context, type.getElementName(), new PsuedoMethod( "find_all_by_" + fieldName, new String[] { fieldName }, Flags.AccPublic | Flags.AccStatic))); } // TODO Add more complicated finder methods that combine multiple // fields? return Arrays.asList(collector.getRubyCompletionProposals()); } private IRubyScript getScript(RubyContentAssistInvocationContext context) { return context.getRubyScript(); } private Set<String> getDBFieldNames(IRubyScript script, String modelName) { Set<String> fieldNames = new HashSet<String>(); File[] scripts = getMigrationScripts(script); for (int j = 0; j < scripts.length; j++) { IFile iFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( new Path(scripts[j].getAbsolutePath())); IRubyScript migrateScript = RubyCore.create(iFile); RootNode ast = null; if (migrateScript.isWorkingCopy()) { try { String prefix = getStatementPrefix(); int fudgeFactor = 1; while (prefix.endsWith(" ")) { fudgeFactor++; prefix = prefix.substring(0, prefix.length() - 1); } CompletionContext correctingContext = new CompletionContext(migrateScript, fContext .getInvocationOffset() - fudgeFactor); if (correctingContext.isBroken()) { try { RubyParser parser = new RubyParser(); ast = (RootNode) parser.parse(correctingContext.getCorrectedSource()).getAST(); } catch (SyntaxException e) { // ignore } } } catch (RubyModelException e) { RailsUILog.log(e); } } if (ast == null) { ast = RubyPlugin.getDefault().getASTProvider().getAST(migrateScript, ASTProvider.WAIT_YES, new NullProgressMonitor()); if (ast == null) { ast = (RootNode) ((RubyScript) migrateScript).lastGoodAST; } } if (ast == null) continue; MigrationVisitor visitor = new MigrationVisitor(); ast.accept(visitor); fieldNames.addAll(visitor.getFieldNames(Inflector.pluralize(modelName))); } return fieldNames; } private String getStatementPrefix() { try { return fContext.computeStatementPrefix().toString(); } catch (BadLocationException e) { RailsUILog.log(e); return ""; } } private Set<String> getDBTableNames(IRubyScript script) { Set<String> fieldNames = new HashSet<String>(); File[] scripts = getMigrationScripts(script); for (int j = 0; j < scripts.length; j++) { IFile iFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( new Path(scripts[j].getAbsolutePath())); IRubyScript migrateScript = RubyCore.create(iFile); RootNode ast = RubyPlugin.getDefault().getASTProvider().getAST(migrateScript, ASTProvider.WAIT_YES, new NullProgressMonitor()); if (ast == null) { ast = (RootNode) ((RubyScript) migrateScript).lastGoodAST; } if (ast == null) continue; MigrationVisitor visitor = new MigrationVisitor(); ast.accept(visitor); fieldNames.addAll(visitor.getTableNames()); } return fieldNames; } private File[] getMigrationScripts(IRubyScript script) { IPath migrationFolder = getMigrationPath(script); if (migrationFolder == null) return new File[0]; migrationFolder = ResourcesPlugin.getWorkspace().getRoot().getFolder(migrationFolder).getLocation(); File[] scripts = migrationFolder.toFile().listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".rb"); } }); if (scripts == null) return new File[0]; return scripts; } private File getControllersFolder(ContentAssistInvocationContext context) { if (!(context instanceof RubyContentAssistInvocationContext)) return null; RubyContentAssistInvocationContext rContext = (RubyContentAssistInvocationContext) context; IRubyScript script = rContext.getRubyScript(); if (script == null) return null; IProject project = script.getRubyProject().getProject(); IPath path = RailsPlugin.findRailsRoot(project); IFolder folder = project.getFolder(path.append("app").append("controllers")); if (folder == null) return null; return folder.getLocation().toFile(); } public List computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) { return Collections.EMPTY_LIST; } public String getErrorMessage() { return null; } public void sessionEnded() { } public void sessionStarted() { } }