/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.internal.text.dart;
import com.google.dart.tools.core.model.DartModelException;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.internal.util.StringMatcher;
import com.google.dart.tools.ui.text.dart.PositionBasedCompletionProposal;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.swt.graphics.Image;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* This class triggers a code-completion that will track all local and member variables for later
* use as a parameter guessing proposal.
*/
public class ParameterGuesser {
private static class MatchComparator implements Comparator<Variable> {
private String fParamName;
MatchComparator(String paramName) {
fParamName = paramName;
}
@Override
public int compare(Variable one, Variable two) {
return score(two) - score(one);
}
/**
* The four order criteria as described below - put already used into bit 10, all others into
* bits 0-9, 11-20, 21-30; 31 is sign - always 0
*
* @param v the variable
* @return the score for <code>v</code>
*/
private int score(Variable v) {
int variableScore = 100 - v.variableType; // since these are increasing with distance
int subStringScore = getLongestCommonSubstring(v.name, fParamName).length();
// substring scores under 60% are not considered
// this prevents marginal matches like a - ba and false - isBool that will
// destroy the sort order
int shorter = Math.min(v.name.length(), fParamName.length());
if (subStringScore < 0.6 * shorter) {
subStringScore = 0;
}
int positionScore = v.positionScore; // since ???
int matchedScore = v.alreadyMatched ? 0 : 1;
int autoboxingScore = v.isAutoboxingMatch ? 0 : 1;
int score = autoboxingScore << 30 | variableScore << 21 | subStringScore << 11
| matchedScore << 10 | positionScore;
return score;
}
}
private final static class Variable {
/**
* Variable type. Used to choose the best guess based on scope (Local beats instance beats
* inherited).
*/
public static final int LOCAL = 0;
public static final int FIELD = 1;
public static final int INHERITED_FIELD = 2;
public static final int METHOD = 3;
public static final int INHERITED_METHOD = 4;
public static final int LITERALS = 5;
public final String qualifiedTypeName;
public final String name;
public final int variableType;
public final int positionScore;
public final boolean isAutoboxingMatch;
public final char[] triggerChars;
public final ImageDescriptor descriptor;
public boolean alreadyMatched;
public Variable(String qualifiedTypeName, String name, int variableType,
boolean isAutoboxMatch, int positionScore, char[] triggerChars, ImageDescriptor descriptor) {
this.qualifiedTypeName = qualifiedTypeName;
this.name = name;
this.variableType = variableType;
this.positionScore = positionScore;
this.triggerChars = triggerChars;
this.descriptor = descriptor;
this.isAutoboxingMatch = isAutoboxMatch;
this.alreadyMatched = false;
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append(qualifiedTypeName);
buffer.append(' ');
buffer.append(name);
buffer.append(" ("); //$NON-NLS-1$
buffer.append(variableType);
buffer.append(')');
return buffer.toString();
}
}
private static final char[] NO_TRIGGERS = new char[0];
/**
* Returns the longest common substring of two strings.
*
* @param first the first string
* @param second the second string
* @return the longest common substring
*/
private static String getLongestCommonSubstring(String first, String second) {
String shorter = (first.length() <= second.length()) ? first : second;
String longer = shorter == first ? second : first;
int minLength = shorter.length();
StringBuffer pattern = new StringBuffer(shorter.length() + 2);
String longestCommonSubstring = ""; //$NON-NLS-1$
for (int i = 0; i < minLength; i++) {
for (int j = i + 1; j <= minLength; j++) {
if (j - i < longestCommonSubstring.length()) {
continue;
}
String substring = shorter.substring(i, j);
pattern.setLength(0);
pattern.append('*');
pattern.append(substring);
pattern.append('*');
StringMatcher matcher = new StringMatcher(pattern.toString(), true, false);
if (matcher.match(longer)) {
longestCommonSubstring = substring;
}
}
}
return longestCommonSubstring;
}
/**
* Determine the best match of all possible type matches. The input into this method is all
* possible completions that match the type of the argument. The purpose of this method is to
* choose among them based on the following simple rules: 1) Local Variables > Instance/Class
* Variables > Inherited Instance/Class Variables 2) A longer case insensitive substring match
* will prevail 3) Variables that have not been used already during this completion will prevail
* over those that have already been used (this avoids the same String/int/char from being passed
* in for multiple arguments) 4) A better source position score will prevail (the declaration
* point of the variable, or "how close to the point of completion?"
*
* @param typeMatches the list of type matches
* @param paramName the parameter name
*/
private static void orderMatches(List<Variable> typeMatches, String paramName) {
if (typeMatches != null) {
Collections.sort(typeMatches, new MatchComparator(paramName));
}
}
private final Set<String> fAlreadyMatchedNames;
/**
* Creates a parameter guesser
*/
public ParameterGuesser() {
fAlreadyMatchedNames = new HashSet<String>();
}
/**
* Returns the matches for the type and name argument, ordered by match quality.
*
* @param expectedType - the qualified type of the parameter we are trying to match
* @param paramName - the name of the parameter (used to find similarly named matches)
* @param pos the position
* @param fillBestGuess <code>true</code> if the best guess should be filled in
* @return returns the name of the best match, or <code>null</code> if no match found
* @throws DartModelException if it fails
*/
public ICompletionProposal[] parameterProposals(String expectedType, String paramName,
Position pos, boolean fillBestGuess) throws DartModelException {
List<Variable> typeMatches = evaluateVisibleMatches(expectedType);
orderMatches(typeMatches, paramName);
boolean hasVarWithParamName = false;
ICompletionProposal[] ret = new ICompletionProposal[typeMatches.size()];
int i = 0;
int replacementLength = 0;
for (Iterator<Variable> it = typeMatches.iterator(); it.hasNext();) {
Variable v = it.next();
if (i == 0) {
fAlreadyMatchedNames.add(v.name);
replacementLength = v.name.length();
}
String displayString = v.name;
hasVarWithParamName |= displayString.equals(paramName);
final char[] triggers = new char[v.triggerChars.length + 1];
System.arraycopy(v.triggerChars, 0, triggers, 0, v.triggerChars.length);
triggers[triggers.length - 1] = ',';
ret[i++] = new PositionBasedCompletionProposal(
v.name,
pos,
replacementLength,
getImage(v.descriptor),
displayString,
null,
null,
triggers);
}
if (!fillBestGuess && !hasVarWithParamName) {
// insert a proposal with the argument name
ICompletionProposal[] extended = new ICompletionProposal[ret.length + 1];
System.arraycopy(ret, 0, extended, 1, ret.length);
extended[0] = new PositionBasedCompletionProposal(
paramName,
pos,
replacementLength,
null,
paramName,
null,
null,
NO_TRIGGERS);
return extended;
}
return ret;
}
private List<Variable> evaluateVisibleMatches(String expectedType) throws DartModelException {
ArrayList<Variable> res = new ArrayList<Variable>();
String literalTypeCode = getLiteralTypeCode(expectedType);
if (literalTypeCode == null) {
// add 'null'
res.add(new Variable(
expectedType,
"null", Variable.LITERALS, false, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
} else {
String typeName = literalTypeCode;
boolean isAutoboxing = false;
if (literalTypeCode.equals("bool")) {
// add 'true', 'false'
res.add(new Variable(
typeName,
"true", Variable.LITERALS, isAutoboxing, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
res.add(new Variable(
typeName,
"false", Variable.LITERALS, isAutoboxing, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
} else if (literalTypeCode.equals("int")) {
// add 0
res.add(new Variable(
typeName,
"0", Variable.LITERALS, isAutoboxing, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
} else if (literalTypeCode.equals("double")) {
// add 0.0
res.add(new Variable(
typeName,
"0.0", Variable.LITERALS, isAutoboxing, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
} else if (literalTypeCode.equals("num")) {
// add 0
res.add(new Variable(
typeName,
"0", Variable.LITERALS, isAutoboxing, res.size(), NO_TRIGGERS, null)); //$NON-NLS-1$
}
}
return res;
}
private Image getImage(ImageDescriptor descriptor) {
return (descriptor == null) ? null : DartToolsPlugin.getImageDescriptorRegistry().get(
descriptor);
}
private String getLiteralTypeCode(String type) {
if (type.equals("bool")) {
return type;
} else if (type.equals("int")) {
return type;
} else if (type.equals("double")) {
return type;
} else if (type.equals("num")) {
return type;
} else {
return null;
}
}
}