package org.rubypeople.rdt.ui.text.ruby;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.graphics.Image;
import org.rubypeople.rdt.core.CompletionProposal;
import org.rubypeople.rdt.core.CompletionRequestor;
import org.rubypeople.rdt.core.IMember;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.IRubyScript;
import org.rubypeople.rdt.core.compiler.IProblem;
import org.rubypeople.rdt.internal.ui.RubyPlugin;
import org.rubypeople.rdt.internal.ui.text.ruby.AbstractRubyCompletionProposal;
import org.rubypeople.rdt.internal.ui.text.ruby.CompletionProposalLabelProvider;
import org.rubypeople.rdt.internal.ui.text.ruby.FillArgsAndBlockProposal;
import org.rubypeople.rdt.internal.ui.text.ruby.FillMethodArgumentsProposal;
import org.rubypeople.rdt.internal.ui.text.ruby.ProposalInfo;
import org.rubypeople.rdt.internal.ui.text.ruby.RubyCompletionProposal;
import org.rubypeople.rdt.internal.ui.text.ruby.RubyContentAssistInvocationContext;
import org.rubypeople.rdt.ui.PreferenceConstants;
import org.rubypeople.rdt.ui.viewsupport.ImageDescriptorRegistry;
public class CompletionProposalCollector extends CompletionRequestor {
/** Tells whether this class is in debug mode. */
private static final boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.rubypeople.rdt.ui/debug/ResultCollector")); //$NON-NLS-1$//$NON-NLS-2$
private final CompletionProposalLabelProvider fLabelProvider= new CompletionProposalLabelProvider();
private final ImageDescriptorRegistry fRegistry= RubyPlugin.getImageDescriptorRegistry();
/** Triggers for variables. Do not modify. */
protected final static char[] VAR_TRIGGER= new char[] { '\t', ' ', '=', ';', '.' };
private final List fRubyProposals= new ArrayList();
private final List fKeywords= new ArrayList();
private final Set fSuggestedMethodNames= new HashSet();
private final IRubyScript fRubyScript;
private final IRubyProject fRubyProject;
private int fUserReplacementLength;
private IProblem fLastProblem;
/* performance instrumentation */
private long fStartTime;
private long fUITime;
private RubyContentAssistInvocationContext context;
/**
* Creates a new instance ready to collect proposals. If the passed
* <code>IRubyScript</code> is not contained in an
* {@link IRubyProject}, no javadoc will be available as
* {@link org.eclipse.jface.text.contentassist.ICompletionProposal#getAdditionalProposalInfo() additional info}
* on the created proposals.
*
* @param cu the compilation unit that the result collector will operate on
*/
public CompletionProposalCollector(RubyContentAssistInvocationContext context) {
this(context.getProject(), context.getRubyScript());
this.context = context;
}
private CompletionProposalCollector(IRubyProject project, IRubyScript cu) {
fRubyProject= project;
fRubyScript= cu;
fUserReplacementLength= -1;
}
/**
* Returns the unsorted list of received proposals.
*
* @return the unsorted list of received proposals
*/
public final IRubyCompletionProposal[] getRubyCompletionProposals() {
return (IRubyCompletionProposal[]) fRubyProposals.toArray(new IRubyCompletionProposal[fRubyProposals.size()]);
}
/**
* Returns the unsorted list of received keyword proposals.
*
* @return the unsorted list of received keyword proposals
*/
public final IRubyCompletionProposal[] getKeywordCompletionProposals() {
return (IRubyCompletionProposal[]) fKeywords.toArray(new RubyCompletionProposal[fKeywords.size()]);
}
@Override
public void accept(CompletionProposal proposal) {
if (proposal == null) return;
long start= DEBUG ? System.currentTimeMillis() : 0;
try {
if (isFiltered(proposal))
return;
if (proposal.getKind() == CompletionProposal.POTENTIAL_METHOD_DECLARATION) {
// TODO Handle potential method declarations
// acceptPotentialMethodDeclaration(proposal);
} else {
IRubyCompletionProposal rubyProposal= createRubyCompletionProposal(proposal);
if (rubyProposal != null) {
fRubyProposals.add(rubyProposal);
if (proposal.getKind() == CompletionProposal.KEYWORD)
fKeywords.add(rubyProposal);
}
}
} catch (IllegalArgumentException e) {
// all signature processing method may throw IAEs
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=84657
// don't abort, but log and show all the valid proposals
RubyPlugin.log(new Status(IStatus.ERROR, RubyPlugin.getPluginId(), IStatus.OK, "Exception when processing proposal for: " + String.valueOf(proposal.getCompletion()), e)); //$NON-NLS-1$
}
if (DEBUG) fUITime += System.currentTimeMillis() - start;
}
/**
* Computes the relevance for a given <code>CompletionProposal</code>.
* <p>
* Subclasses may replace, but usually should not need to.
* </p>
* @param proposal the proposal to compute the relevance for
* @return the relevance for <code>proposal</code>
*/
protected int computeRelevance(CompletionProposal proposal) {
final int baseRelevance= proposal.getRelevance() * 16;
switch (proposal.getKind()) {
case CompletionProposal.KEYWORD:
return baseRelevance + 2;
case CompletionProposal.TYPE_REF:
return baseRelevance + 3;
case CompletionProposal.METHOD_REF:
case CompletionProposal.METHOD_NAME_REFERENCE:
case CompletionProposal.METHOD_DECLARATION:
return baseRelevance + 4;
case CompletionProposal.POTENTIAL_METHOD_DECLARATION:
return baseRelevance + 4 /* + 99 */;
case CompletionProposal.CONSTANT_REF:
case CompletionProposal.CLASS_VARIABLE_REF:
case CompletionProposal.INSTANCE_VARIABLE_REF:
return baseRelevance + 5;
case CompletionProposal.LOCAL_VARIABLE_REF:
case CompletionProposal.VARIABLE_DECLARATION:
return baseRelevance + 6;
default:
return baseRelevance;
}
}
/**
* Returns <code>true</code> if <code>proposal</code> is filtered, e.g.
* should not be proposed to the user, <code>false</code> if it is valid.
* <p>
* Subclasses may extends this method. The default implementation filters
* proposals set to be ignored via
* {@linkplain CompletionRequestor#setIgnored(int, boolean) setIgnored} and
* types set to be ignored in the preferences.
* </p>
*
* @param proposal the proposal to filter
* @return <code>true</code> to filter <code>proposal</code>,
* <code>false</code> to let it pass
*/
protected boolean isFiltered(CompletionProposal proposal) {
if (isIgnored(proposal.getKind()))
return true;
// TODO Handle Type filtering?
// char[] declaringType= getDeclaringType(proposal);
// return declaringType!= null && TypeFilter.isFiltered(declaringType);
return false;
}
/**
* Creates a new ruby completion proposal from a core proposal. This may
* involve computing the display label and setting up some context.
* <p>
* This method is called for every proposal that will be displayed to the
* user, which may be hundreds. Implementations should therefore defer as
* much work as possible: Labels should be computed lazily to leverage
* virtual table usage, and any information only needed when
* <em>applying</em> a proposal should not be computed yet.
* </p>
* <p>
* Implementations may return <code>null</code> if a proposal should not
* be included in the list presented to the user.
* </p>
* <p>
* Subclasses may extend or replace this method.
* </p>
*
* @param proposal the core completion proposal to create a UI proposal for
* @return the created ruby completion proposal, or <code>null</code> if
* no proposal should be displayed
*/
protected IRubyCompletionProposal createRubyCompletionProposal(CompletionProposal proposal) {
switch (proposal.getKind()) {
// case CompletionProposal.KEYWORD:
// return createKeywordProposal(proposal);
// case CompletionProposal.TYPE_REF:
// return createTypeProposal(proposal);
// case CompletionProposal.FIELD_REF:
// return createFieldProposal(proposal);
case CompletionProposal.METHOD_REF:
// case CompletionProposal.METHOD_NAME_REFERENCE:
IRubyCompletionProposal proposal2 = createMethodReferenceProposal(proposal);
if (fSuggestedMethodNames.contains(proposal2.getDisplayString())) return null;
fSuggestedMethodNames.add(proposal2.getDisplayString());
return proposal2;
// case CompletionProposal.METHOD_DECLARATION:
// return createMethodDeclarationProposal(proposal);
// case CompletionProposal.LOCAL_VARIABLE_REF:
// case CompletionProposal.VARIABLE_DECLARATION:
// return createLocalVariableProposal(proposal);
// case CompletionProposal.POTENTIAL_METHOD_DECLARATION:
// default:
// return null;
}
return createProposal(proposal);
}
private IRubyCompletionProposal createMethodReferenceProposal(CompletionProposal methodProposal) {
boolean fillArgs = RubyPlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.CODEASSIST_FILL_ARGUMENT_NAMES);
if (fillArgs) {
boolean fillBlockArgs = RubyPlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.CODEASSIST_FILL_METHOD_BLOCK_ARGUMENTS);
String completion= String.valueOf(methodProposal.getCompletion());
// super class' behavior if this is not a normal completion or has no parameters
if ((completion.length() == 0) || ((completion.length() == 1) && completion.charAt(0) == ')') || methodProposal.getParameterNames() == null || methodProposal.getParameterNames().length == 0)
return createProposal(methodProposal);
if (fillBlockArgs)
return new FillArgsAndBlockProposal(methodProposal, context);
else
return new FillMethodArgumentsProposal(methodProposal, context);
} else {
return createProposal(methodProposal);
}
}
/**
* Returns the ruby script that the receiver operates on, or
* <code>null</code> if the <code>IRubyProject</code> constructor was
* used to create the receiver.
*
* @return the ruby script that the receiver operates on, or
* <code>null</code>
*/
protected final IRubyScript getRubyScript() {
return fRubyScript;
}
/**
* Returns a cached image for the given descriptor.
*
* @param descriptor the image descriptor to get an image for, may be
* <code>null</code>
* @return the image corresponding to <code>descriptor</code>
*/
protected final Image getImage(ImageDescriptor descriptor) {
return (descriptor == null) ? null : fRegistry.get(descriptor);
}
private IRubyCompletionProposal createProposal(CompletionProposal proposal) {
String completion= proposal.getCompletion();
int start= proposal.getReplaceStart();
int length= getLength(proposal);
String label= fLabelProvider.createLabel(proposal);
int relevance= computeRelevance(proposal);
Image image = getImage(fLabelProvider.createImageDescriptor(proposal));
AbstractRubyCompletionProposal rubyProposal = new RubyCompletionProposal(completion, start, length, image, label, relevance);
if(proposal.getElement() != null && proposal.getElement() instanceof IMember) {
ProposalInfo info = new ProposalInfo((IMember) proposal.getElement());
rubyProposal.setProposalInfo(info);
}
return rubyProposal;
}
/**
* Returns the replacement length of a given completion proposal. The
* replacement length is usually the difference between the return values of
* <code>proposal.getReplaceEnd</code> and
* <code>proposal.getReplaceStart</code>, but this behavior may be
* overridden by calling {@link #setReplacementLength(int)}.
*
* @param proposal the completion proposal to get the replacement length for
* @return the replacement length for <code>proposal</code>
*/
protected final int getLength(CompletionProposal proposal) {
int start= proposal.getReplaceStart();
int end= proposal.getReplaceEnd();
int length;
if (fUserReplacementLength == -1) {
length= end - start;
} else {
length= fUserReplacementLength;
// extend length to begin at start
int behindCompletion= proposal.getCompletionLocation() + 1;
if (start < behindCompletion) {
length+= behindCompletion - start;
}
}
return length;
}
/**
* {@inheritDoc}
*
* Subclasses may extend, but must call the super implementation.
*/
public void beginReporting() {
if (DEBUG) {
fStartTime= System.currentTimeMillis();
fUITime= 0;
}
fLastProblem= null;
fRubyProposals.clear();
fKeywords.clear();
fSuggestedMethodNames.clear();
}
/**
* {@inheritDoc}
*
* Subclasses may extend, but must call the super implementation.
*/
public void completionFailure(IProblem problem) {
fLastProblem= problem;
}
/**
* {@inheritDoc}
*
* Subclasses may extend, but must call the super implementation.
*/
public void endReporting() {
if (DEBUG) {
long total= System.currentTimeMillis() - fStartTime;
System.err.println("Core Collector (core):\t" + (total - fUITime)); //$NON-NLS-1$
System.err.println("Core Collector (ui):\t" + fUITime); //$NON-NLS-1$
}
fSuggestedMethodNames.clear();
}
/**
* Returns an error message about any error that may have occurred during
* code completion, or the empty string if none.
* <p>
* Subclasses may replace or extend.
* </p>
* @return an error message or the empty string
*/
public String getErrorMessage() {
if (fLastProblem != null)
return fLastProblem.getMessage();
return ""; //$NON-NLS-1$
}
}