package com.aptana.rdt.internal.ui.text.correction; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.jruby.ast.ClassNode; import org.jruby.ast.DefnNode; import org.jruby.ast.ModuleNode; import org.jruby.ast.Node; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.core.formatter.EditableFormatHelper; import org.rubypeople.rdt.core.formatter.FormatHelper; import org.rubypeople.rdt.core.formatter.Indents; import org.rubypeople.rdt.core.formatter.ReWriteVisitor; import org.rubypeople.rdt.core.util.Util; import org.rubypeople.rdt.internal.ti.util.ClosestSpanningNodeLocator; import org.rubypeople.rdt.internal.ti.util.INodeAcceptor; import org.rubypeople.rdt.internal.ui.rubyeditor.ASTProvider; import org.rubypeople.rdt.refactoring.core.NodeFactory; import org.rubypeople.rdt.refactoring.core.renamelocal.RenameLocalRefactoring; import org.rubypeople.rdt.ui.text.ruby.IInvocationContext; import org.rubypeople.rdt.ui.text.ruby.IProblemLocation; import org.rubypeople.rdt.ui.text.ruby.IQuickFixProcessor; import org.rubypeople.rdt.ui.text.ruby.IRubyCompletionProposal; import com.aptana.rdt.IProblem; import com.aptana.rdt.ui.AptanaRDTUIPlugin; public class QuickFixProcessor implements IQuickFixProcessor { private static final String NEWLINE = "\n"; public IRubyCompletionProposal[] getCorrections(IInvocationContext context, IProblemLocation[] locations) throws CoreException { if (locations == null || locations.length == 0) { return null; } HashSet<Integer> handledProblems = new HashSet<Integer>(locations.length); ArrayList<IRubyCompletionProposal> resultingCollections = new ArrayList<IRubyCompletionProposal>(); for (int i = 0; i < locations.length; i++) { IProblemLocation curr = locations[i]; Integer id = Integer.valueOf(curr.getProblemId()); if (handledProblems.add(id)) { process(context, curr, resultingCollections); } } return (IRubyCompletionProposal[]) resultingCollections.toArray(new IRubyCompletionProposal[resultingCollections.size()]); } public boolean hasCorrections(IRubyScript unit, int problemId) { switch (problemId) { case IProblem.MisspelledConstructor: case IProblem.ConstantNamingConvention: case IProblem.MethodMissingWithoutRespondTo: case IProblem.LocalAndMethodNamingConvention: case IProblem.ComparableInclusionMissingCompareMethod: case IProblem.EnumerableInclusionMissingEachMethod: case IProblem.PossibleAccidentalBooleanAssignment: case IProblem.DeprecatedRequireGem: case IProblem.DynamicVariableAliasesLocal: return true; default: return false; } } private void process(IInvocationContext context, final IProblemLocation problem, Collection<IRubyCompletionProposal> proposals) throws CoreException { int id = problem.getProblemId(); if (id == 0) { // no proposals for none-problem locations return; } switch (id) { case IProblem.DeprecatedRequireGem: LocalCorrectionsSubProcessor.addReplacementProposal(problem.getOffset(), "require_gem".length(), "gem", "Replace with call to 'gem'", proposals); break; case IProblem.MisspelledConstructor: LocalCorrectionsSubProcessor.addReplacementProposal("initialize\n", "Rename to 'initialize'", problem, proposals); break; case IProblem.ConstantNamingConvention: String constName = getProblemSource(context, problem); String fixed = Util.camelCaseToUnderscores(constName).toUpperCase(); // FIXME Use the rename refactoring! LocalCorrectionsSubProcessor.addReplacementProposal(fixed, "Convert to UPPERCASE_WITH_UNDERSCORES convention", problem, proposals); break; case IProblem.LocalVariablePossibleAttributeAccess: String local = getProblemSource(context, problem); fixed = "self." + local; LocalCorrectionsSubProcessor.addReplacementProposal(fixed, "change assignment to " + fixed + " to use attribute", problem, proposals); // Add a proposal to invoke rename local refactoring RefactoringCorrectionProposal proposal = new RefactoringCorrectionProposal("Rename local variable", RenameLocalRefactoring.class, problem); proposals.add(proposal); break; case IProblem.LocalAndMethodNamingConvention: String name = getProblemSource(context, problem); fixed = Util.camelCaseToUnderscores(name).toLowerCase(); // FIXME Use the rename refactoring! LocalCorrectionsSubProcessor.addReplacementProposal(fixed, "Convert to lowercase_with_undercores convention", problem, proposals); break; case IProblem.MethodMissingWithoutRespondTo: // FIXME Only do this stuff when we apply the proposal! Don't do all this work just to create the proposal... int offset = getOffsetOfFirstLineInsideType(context, problem); String text = insertedMethodText(context, offset, "respond_to?", new String[] {"symbol", "include_private = false"}); LocalCorrectionsSubProcessor.addReplacementProposal(offset, 0, text, "Add respond_to? method stub", proposals); break; case IProblem.ComparableInclusionMissingCompareMethod: offset = getOffsetOfFirstLineInsideType(context, problem); text = insertedMethodText(context, offset, "<=>", new String[] {"other"}); LocalCorrectionsSubProcessor.addReplacementProposal(offset, 0, text, "Add <=> method stub", proposals); break; case IProblem.EnumerableInclusionMissingEachMethod: offset = getOffsetOfFirstLineInsideType(context, problem); text = insertedMethodText(context, offset, "each", new String[] {}); LocalCorrectionsSubProcessor.addReplacementProposal(offset, 0, text, "Add each method stub", proposals); break; case IProblem.PossibleAccidentalBooleanAssignment: name = getProblemSource(context, problem); fixed = name.replace("=", "=="); LocalCorrectionsSubProcessor.addReplacementProposal(fixed, "Convert '=' to '=='", problem, proposals); break; case IProblem.DynamicVariableAliasesLocal: RefactoringCorrectionProposal prop = new RefactoringCorrectionProposal("Rename dynamic variable", RenameLocalRefactoring.class, problem); proposals.add(prop); default: } } private String insertedMethodText(IInvocationContext context, int offset, String methodName, String[] args) { IRubyScript script = context.getRubyScript(); String src = ""; try { src = script.getSource(); } catch (RubyModelException e) { AptanaRDTUIPlugin.log(e); } DefnNode methodNode = NodeFactory.createMethodNode(methodName, args, null); Node insert = NodeFactory.createBlockNode(true, NodeFactory.createNewLineNode(methodNode)); String text = ReWriteVisitor.createCodeFromNode(insert, src, getFormatHelper()); StringBuffer buffer = new StringBuffer(text); int index = text.indexOf(NEWLINE, 1); buffer.insert(index + 1, " # TODO Auto-generated method stub\n"); // Figure out indent at offset and apply that to each line of text and at end of text String indent = findIndent(offset, script, src); buffer.insert(0, indent); buffer.append(NEWLINE); text = buffer.toString(); text = text.replaceAll("\\n", NEWLINE + indent); return text; } private String findIndent(int offset, IRubyScript script, String src) { if (src == null || src.length() == 0) return ""; int index = src.indexOf(NEWLINE, offset); if (index < 1 || index > src.length()) return ""; String line = src.substring(0, index); index = line.lastIndexOf(NEWLINE); Map options = script.getRubyProject().getOptions(true); if (index == -1 || ((index + 1) >= line.length()) ) return Indents.extractIndentString(line, options); line = line.substring(index + 1); return Indents.extractIndentString(line, options); } private int getOffsetOfFirstLineInsideType(IInvocationContext context, IProblemLocation problem) { IRubyScript script = context.getRubyScript(); int offset = -1; Node rootNode = ASTProvider.getASTProvider().getAST(script, ASTProvider.WAIT_YES, null); Node typeNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode, problem.getOffset(), new INodeAcceptor() { public boolean doesAccept(Node node) { return node instanceof ClassNode || node instanceof ModuleNode; } }); if (typeNode instanceof ClassNode) { ClassNode classNode = (ClassNode) typeNode; offset = classNode.getBodyNode().getPosition().getStartOffset(); } else if (typeNode instanceof ModuleNode) { ModuleNode classNode = (ModuleNode) typeNode; offset = classNode.getBodyNode().getPosition().getStartOffset(); } return offset; } private String getProblemSource(IInvocationContext context, IProblemLocation problem) throws RubyModelException { IRubyScript script = context.getRubyScript(); String src = script.getSource(); return src.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); } protected FormatHelper getFormatHelper() { // FIXME Hooks these settings up to format prefs EditableFormatHelper helper = new EditableFormatHelper(); helper.setAlwaysParanthesizeMethodCalls(true); helper.setAlwaysParanthesizeMethodDefs(true); return helper; } }