/*******************************************************************************
* Copyright (c) 2014 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.properties.editor;
import static org.springframework.ide.eclipse.editor.support.util.StringUtil.camelCaseToHyphens;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Provider;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.internal.ui.propertiesfileeditor.IPropertiesFilePartitions;
import org.eclipse.jface.fieldassist.ContentProposal;
import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.springframework.boot.configurationmetadata.ValueHint;
import org.springframework.ide.eclipse.boot.properties.editor.FuzzyMap.Match;
import org.springframework.ide.eclipse.boot.properties.editor.completions.LazyProposalApplier;
import org.springframework.ide.eclipse.boot.properties.editor.completions.PropertyCompletionFactory;
import org.springframework.ide.eclipse.boot.properties.editor.completions.SpringPropertyHoverInfo;
import org.springframework.ide.eclipse.boot.properties.editor.completions.ValueHintHoverInfo;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.HintProvider;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.HintProviders;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.PropertyInfo;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.StsValueHint;
import org.springframework.ide.eclipse.boot.properties.editor.reconciling.PropertyNavigator;
import org.springframework.ide.eclipse.boot.properties.editor.util.Type;
import org.springframework.ide.eclipse.boot.properties.editor.util.TypeParser;
import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil;
import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil.BeanPropertyNameMode;
import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil.EnumCaseMode;
import org.springframework.ide.eclipse.boot.properties.editor.util.TypedProperty;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.editor.support.completions.DocumentEdits;
import org.springframework.ide.eclipse.editor.support.completions.ICompletionEngine;
import org.springframework.ide.eclipse.editor.support.completions.ProposalApplier;
import org.springframework.ide.eclipse.editor.support.hover.HoverInfo;
import org.springframework.ide.eclipse.editor.support.hover.HoverInfoProvider;
import org.springframework.ide.eclipse.editor.support.util.CollectionUtil;
import org.springframework.ide.eclipse.editor.support.util.DocumentRegion;
import org.springframework.ide.eclipse.editor.support.util.FuzzyMatcher;
import org.springframework.ide.eclipse.editor.support.util.PrefixFinder;
import org.springframework.ide.eclipse.editor.support.util.StringUtil;
import com.google.common.collect.ImmutableList;
/**
* @author Kris De Volder
*/
@SuppressWarnings("restriction")
public class SpringPropertiesCompletionEngine implements HoverInfoProvider, ICompletionEngine {
private boolean preferLowerCaseEnums = true; //might make sense to make this user configurable
/**
* Pattern we look for at the start of the Document partition in 'value' part of a 'key-value'
* assignment. The stuff matching this pattern isn't to be treated as part of the actual value.
*/
public static final Pattern ASSIGN = Pattern.compile("^(\\h)*(=|:)(\\h|\\\\\\s)*");
private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder");
public static void debug(String msg) {
if (DEBUG) {
System.out.println("SpringPropertiesCompletionEngine: "+msg);
}
}
public static final boolean DEFAULT_VALUE_INCLUDED = false; //might make sense to make this user configurable
private static boolean isValuePrefixChar(char c) {
return !Character.isWhitespace(c) && c!=',';
}
private static final PrefixFinder valuePrefixFinder = new PrefixFinder() {
protected boolean isPrefixChar(char c) {
return isValuePrefixChar(c);
}
};
private static final PrefixFinder fuzzySearchPrefix = new PrefixFinder() {
protected boolean isPrefixChar(char c) {
return !Character.isWhitespace(c);
}
};
private static final PrefixFinder navigationPrefixFinder = new PrefixFinder() {
public String getPrefix(IDocument doc, int offset) {
String prefix = super.getPrefix(doc, offset);
//Check if character before looks like 'navigation'.. otherwise don't
// return a navigationPrefix.
char charBefore = getCharBefore(doc, prefix, offset);
if (charBefore=='.' || charBefore==']') {
return prefix;
}
return null;
}
private char getCharBefore(IDocument doc, String prefix, int offset) {
try {
if (prefix!=null) {
int offsetBefore = offset-prefix.length()-1;
if (offsetBefore>=0) {
return doc.getChar(offsetBefore);
}
}
} catch (BadLocationException e) {
//ignore
}
return 0;
}
protected boolean isPrefixChar(char c) {
return !Character.isWhitespace(c) && c!=']' && c!=']' && c!='.';
}
};
private static final IContentProposal[] NO_CONTENT_PROPOSALS = new IContentProposal[0];
private DocumentContextFinder documentContextFinder = null;
private Provider<FuzzyMap<PropertyInfo>> indexProvider = null;
private TypeUtil typeUtil = null;
private PropertyCompletionFactory completionFactory = null;
/**
* Create an empty completion engine. Meant for unit testing. Real clients should use the
* constructor that accepts an {@link IJavaProject}.
* <p>
* In a test context the test harness is responsible for injecting proper documentContextFinder
* and indexProvider.
*/
public SpringPropertiesCompletionEngine() {
}
/**
* Constructor used in 'production'. Wires up stuff properly for running inside a normal
* Eclipse runtime.
*/
public SpringPropertiesCompletionEngine(final IJavaProject jp) throws Exception {
this.indexProvider = new Provider<FuzzyMap<PropertyInfo>>() {
public FuzzyMap<PropertyInfo> get() {
return SpringPropertiesEditorPlugin.getIndexManager().get(jp);
}
};
setDocumentContextFinder(DocumentContextFinders.PROPS_DEFAULT);
this.typeUtil = new TypeUtil(jp);
// System.out.println(">>> spring properties metadata loaded "+index.size()+" items===");
// dumpAsTestData();
// System.out.println(">>> spring properties metadata loaded "+index.size()+" items===");
}
/**
* Create completions proposals in the context of a properties text editor.
*/
public Collection<ICompletionProposal> getCompletions(IDocument doc, int offset) throws BadLocationException {
ITypedRegion partition = getPartition(doc, offset);
String type = partition.getType();
if (type.equals(IDocument.DEFAULT_CONTENT_TYPE)) {
//inside a property 'key'
return getPropertyCompletions(doc, offset);
} else if (type.equals(IPropertiesFilePartitions.PROPERTY_VALUE)) {
return getValueCompletions(doc, offset, partition);
}
return Collections.emptyList();
}
private Collection<ICompletionProposal> getNavigationProposals(IDocument doc, int offset) {
String navPrefix = navigationPrefixFinder.getPrefix(doc, offset);
try {
if (navPrefix!=null) {
int navOffset = offset-navPrefix.length()-1; //offset of 'nav' operator char (i.e. '.' or ']').
navPrefix = fuzzySearchPrefix.getPrefix(doc, navOffset);
if (navPrefix!=null && !navPrefix.isEmpty()) {
PropertyInfo prop = findLongestValidProperty(getIndex(), navPrefix);
if (prop!=null) {
int regionStart = navOffset-navPrefix.length();
Collection<ICompletionProposal> hintProposals = getKeyHintProposals(doc, prop, navOffset, offset);
if (CollectionUtil.hasElements(hintProposals)) {
return hintProposals;
}
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, region(regionStart, navOffset));
Type type = navigator.navigate(regionStart+prop.getId().length(), TypeParser.parse(prop.getType()));
if (type!=null) {
return getNavigationProposals(doc, type, navOffset, offset);
}
}
}
}
} catch (Exception e) {
Log.log(e);
}
return Collections.emptyList();
}
private Collection<ICompletionProposal> getKeyHintProposals(IDocument doc, PropertyInfo prop, int navOffset, int offset) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
String query = textBetween(doc, navOffset+1, offset);
List<TypedProperty> hintProperties = hintProvider.getPropertyHints(query);
if (CollectionUtil.hasElements(hintProperties)) {
return createPropertyProposals(doc, TypeParser.parse(prop.getType()), navOffset, offset, query, hintProperties);
}
}
return ImmutableList.of();
}
private String textBetween(IDocument doc, int start, int end) {
if (end > doc.getLength()) {
end = doc.getLength();
}
if (start>doc.getLength()) {
start = doc.getLength();
}
if (start<0) {
start = 0;
}
if (end < 0) {
end = 0;
}
if (start<end) {
try {
return doc.get(start, end-start);
} catch (BadLocationException e) {
}
}
return "";
}
private IRegion region(int start, int end) {
return new Region(start, end-start);
}
/**
* @param type Type of the expression leading upto the 'nav' operator
* @param navOffset Offset of the nav operator (either ']' or '.'
* @param offset Offset of the cursor where CA was requested.
*/
private Collection<ICompletionProposal> getNavigationProposals(IDocument doc, Type type, int navOffset, int offset) {
try {
char navOp = doc.getChar(navOffset);
if (navOp=='.') {
String prefix = doc.get(navOffset+1, offset-(navOffset+1));
EnumCaseMode caseMode = caseMode(prefix);
List<TypedProperty> objectProperties = typeUtil.getProperties(type, caseMode, BeanPropertyNameMode.HYPHENATED);
//Note: properties editor itself deals with relaxed names. So it expects the properties here to be returned in hyphenated form only.
if (objectProperties!=null && !objectProperties.isEmpty()) {
return createPropertyProposals(doc, type, navOffset, offset, prefix, objectProperties);
}
} else {
//TODO: other cases ']' or '[' ?
}
} catch (Exception e) {
Log.log(e);
}
return Collections.emptyList();
}
protected Collection<ICompletionProposal> createPropertyProposals(IDocument doc, Type type, int navOffset,
int offset, String prefix, List<TypedProperty> objectProperties) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (TypedProperty prop : objectProperties) {
double score = FuzzyMatcher.matchScore(prefix, prop.getName());
if (score!=0) {
Type valueType = prop.getType();
String postFix = propertyCompletionPostfix(valueType);
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(navOffset+1, offset);
edits.insert(offset, prop.getName()+postFix);
proposals.add(
completionFactory.beanProperty(doc, null, type, prefix, prop, score, edits, typeUtil)
);
}
}
return proposals;
}
/**
* Determines the EnumCaseMode used to generate completion candidates based on prefix.
*/
protected EnumCaseMode caseMode(String prefix) {
EnumCaseMode caseMode;
if ("".equals(prefix)) {
caseMode = preferLowerCaseEnums?EnumCaseMode.LOWER_CASE:EnumCaseMode.ORIGNAL;
} else {
caseMode = Character.isLowerCase(prefix.charAt(0))?EnumCaseMode.LOWER_CASE:EnumCaseMode.ORIGNAL;
}
return caseMode;
}
protected String propertyCompletionPostfix(Type type) {
String postfix = "";
if (type!=null) {
if (typeUtil.isAssignableType(type)) {
postfix = "=";
} else if (TypeUtil.isBracketable(type)) {
postfix = "[";
} else if (typeUtil.isDotable(type)) {
postfix = ".";
}
}
return postfix;
}
public static boolean isAssign(char assign) {
return assign==':'||assign=='=';
}
private ITypedRegion getPartition(IDocument doc, int offset) throws BadLocationException {
ITypedRegion part = TextUtilities.getPartition(doc, IPropertiesFilePartitions.PROPERTIES_FILE_PARTITIONING, offset, true);
if (part.getType()==IDocument.DEFAULT_CONTENT_TYPE && part.getLength()==0 && offset==doc.getLength() && offset>0 && !newlineBefore(doc, offset)) {
//A special case because when cursor at end of document and just after a space-padded '=' sign, then we get a DEFAULT content type
// with a empty region. We rather would get the non-empty 'Value' partition just before that (which has the assignment in it.
ITypedRegion previousPart = TextUtilities.getPartition(doc, IPropertiesFilePartitions.PROPERTIES_FILE_PARTITIONING, offset-1, false);
int previousEnd = previousPart.getOffset()+previousPart.getLength();
if (previousEnd==offset) {
//prefer this over a 0 length partition ending at the same location
return previousPart;
}
}
return part;
}
private boolean newlineBefore(IDocument doc, int offset) {
try {
if (offset>0) {
char c = doc.getChar(offset-1);
return c == '\n' || c=='\r';
}
} catch (BadLocationException e) {
Log.log(e);
}
return false;
}
private HoverInfo getValueHoverInfo(DocumentRegion value) {
try {
String valueString = value.toString();
IDocument doc = value.getDocument();
ITypedRegion valuePartition = getPartition(value.getDocument(), value.getStart());
int valuePartitionStart = valuePartition.getOffset();
String propertyName = fuzzySearchPrefix.getPrefix(doc, valuePartitionStart); //note: no need to skip whitespace backwards.
//because value partition includes whitespace around the assignment
Type type = getValueType(propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
if (TypeUtil.isClass(type)) {
//Special case. We want to provide hoverinfos more liberally than what's suggested for completions (i.e. even class names
//that are not suggested by the hints because they do not meet subtyping constraints should be hoverable and linkable!
StsValueHint hint = StsValueHint.className(valueString, typeUtil);
if (hint!=null) {
return new ValueHintHoverInfo(hint);
}
}
//Hack: pretend to invoke content-assist at the end of the value text. This should provide hints applicable to that value
// then show hoverinfo based on that. That way we can avoid duplication a lot of similar logic to compute hoverinfos and hyperlinks.
Collection<StsValueHint> hints = getValueHints(valueString, propertyName, EnumCaseMode.ALIASED);
if (hints!=null) {
for (StsValueHint h : hints) {
if (valueString.equals(h.getValue())) {
return new ValueHintHoverInfo(h);
}
}
}
} catch (BadLocationException e) {
Log.log(e);
}
return null;
}
private Collection<ICompletionProposal> getValueCompletions(IDocument doc, int offset, IRegion valuePartition) {
int regionStart = valuePartition.getOffset();
try {
int startOfValue = skipAssign(doc, offset, valuePartition);
String query = valuePrefixFinder.getPrefix(doc, offset, startOfValue);
startOfValue = offset - query.length();
EnumCaseMode caseMode = caseMode(query);
String propertyName = fuzzySearchPrefix.getPrefix(doc, regionStart); //note: no need to skip whitespace backwards.
//because value partition includes whitespace around the assignment
if (propertyName!=null) {
Collection<StsValueHint> valueCompletions = getValueHints(query, propertyName, caseMode);
if (valueCompletions!=null && !valueCompletions.isEmpty()) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (StsValueHint hint : valueCompletions) {
String valueCandidate = hint.getValue();
double score = FuzzyMatcher.matchScore(query, valueCandidate);
if (score!=0) {
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(startOfValue, offset);
edits.insert(offset, valueCandidate);
proposals.add(
completionFactory.valueProposal(valueCandidate, query, getValueType(propertyName), score, edits, new ValueHintHoverInfo(hint))
//new ValueProposal(startOfValue, valuePrefix, valueCandidate, i)
);
}
}
return proposals;
}
}
} catch (Exception e) {
SpringPropertiesEditorPlugin.log(e);
}
return Collections.emptyList();
}
/**
* Determine the end of the 'ASSIGN' pattern which is expected at the beginning of a 'value' partition of
* props file. This is used to determine the start of the *real* value region (i.e. the assignment isn't
* really part of the value text even though Eclipse document partitioner inlcudes it as part of the 'valuePartition'
* region).
*/
private int skipAssign(IDocument doc, int offset, IRegion valuePartition) {
try {
String text = doc.get(valuePartition.getOffset(), valuePartition.getLength());
Matcher matcher = ASSIGN.matcher(text);
if (matcher.find() && matcher.start()==0) {
int len = matcher.end();
return valuePartition.getOffset()+len;
}
return valuePartition.getOffset();
} catch (BadLocationException e) {
//This shouldn't really happen, but...
return valuePartition.getOffset();
}
}
private Collection<StsValueHint> getValueHints(String query, String propertyName, EnumCaseMode caseMode) {
Type type = getValueType(propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
List<StsValueHint> allHints = new ArrayList<>();
{
Collection<StsValueHint> hints = typeUtil.getHintValues(type, query, caseMode);
if (CollectionUtil.hasElements(hints)) {
allHints.addAll(hints);
}
}
{
PropertyInfo prop = getIndex().findLongestCommonPrefixEntry(propertyName);
if (prop!=null) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
allHints.addAll(hintProvider.getValueHints(query));
}
}
}
return allHints;
}
/**
* Determine the value type for a give propertyName.
*/
protected Type getValueType(String propertyName) {
try {
PropertyInfo prop = getIndex().get(propertyName);
if (prop!=null) {
return TypeParser.parse(prop.getType());
} else {
prop = findLongestValidProperty(getIndex(), propertyName);
if (prop!=null) {
Document doc = new Document(propertyName);
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, new Region(0, doc.getLength()));
return navigator.navigate(prop.getId().length(), TypeParser.parse(prop.getType()));
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
private List<Match<PropertyInfo>> findMatches(String prefix) {
List<Match<PropertyInfo>> matches = getIndex().find(camelCaseToHyphens(prefix));
return matches;
}
private Collection<ICompletionProposal> getPropertyCompletions(IDocument doc, int offset) throws BadLocationException {
Collection<ICompletionProposal> navProposals = getNavigationProposals(doc, offset);
if (!navProposals.isEmpty()) {
return navProposals;
}
return getFuzzyCompletions(doc, offset);
}
protected Collection<ICompletionProposal> getFuzzyCompletions(
final IDocument doc, final int offset) {
final String prefix = fuzzySearchPrefix.getPrefix(doc, offset);
if (prefix != null) {
Collection<Match<PropertyInfo>> matches = findMatches(prefix);
if (matches!=null && !matches.isEmpty()) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(matches.size());
for (final Match<PropertyInfo> match : matches) {
ProposalApplier edits = new LazyProposalApplier() {
@Override
protected ProposalApplier create() throws Exception {
Type type = TypeParser.parse(match.data.getType());
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(offset-prefix.length(), offset);
edits.insert(offset, match.data.getId() + propertyCompletionPostfix(type));
return edits;
}
};
proposals.add(
completionFactory.property(doc, edits, match, typeUtil)
);
}
return proposals;
}
}
return Collections.emptyList();
}
/**
* Create completions proposals for a field editor where property names can be entered.
*/
public IContentProposal[] getPropertyFieldProposals(String contents, int position) {
String prefix = contents.substring(0,position);
if (StringUtil.hasText(prefix)) {
List<Match<PropertyInfo>> matches = findMatches(prefix);
if (matches!=null && !matches.isEmpty()) {
IContentProposal[] proposals = new IContentProposal[matches.size()];
Collections.sort(matches, new Comparator<Match<PropertyInfo>>() {
@Override
public int compare(Match<PropertyInfo> o1, Match<PropertyInfo> o2) {
int scoreCompare = Double.compare(o2.score, o1.score);
if (scoreCompare!=0) {
return scoreCompare;
} else {
return o1.data.getId().compareTo(o2.data.getId());
}
}
});
int i = 0;
for (Match<PropertyInfo> m : matches) {
proposals[i++] = new ContentProposal(m.data.getId(), m.data.getDescription());
}
return proposals;
}
}
return NO_CONTENT_PROPOSALS;
}
public HoverInfo getHoverInfo(IDocument doc, IRegion _region) {
debug("getHoverInfo("+_region+")");
//The delegate 'getHoverRegion' for spring propery editor will return smaller word regions.
// we must ensure to use our own region finder to identify correct property name.
ITypedRegion region = getHoverRegion(doc, _region.getOffset());
if (region!=null) {
String contentType = region.getType();
try {
if (contentType.equals(IDocument.DEFAULT_CONTENT_TYPE)) {
debug("hoverRegion = "+region);
PropertyInfo best = findBestHoverMatch(doc.get(region.getOffset(), region.getLength()).trim());
if (best!=null) {
return new SpringPropertyHoverInfo(documentContextFinder.getJavaProject(doc), best);
}
} else if (contentType.equals(IPropertiesFilePartitions.PROPERTY_VALUE)) {
return getValueHoverInfo(new DocumentRegion(doc, region));
}
} catch (Exception e) {
SpringPropertiesEditorPlugin.log(e);
}
}
return null;
}
public ITypedRegion getHoverRegion(IDocument document, int offset) {
try {
ITypedRegion candidate = getPartition(document, offset);
if (candidate!=null) {
String type = candidate.getType();
if (IDocument.DEFAULT_CONTENT_TYPE.equals(type)) {
return candidate;
} else if (IPropertiesFilePartitions.PROPERTY_VALUE.equals(type)) {
DocumentRegion valueRegion = new DocumentRegion(document, candidate).trimStart(ASSIGN);
return getValueHoverRegion(valueRegion, valueRegion.toRelative(offset));
}
}
} catch (Exception e) {
SpringPropertiesEditorPlugin.log(e);
}
return null;
}
private ITypedRegion getValueHoverRegion(DocumentRegion r, int offset) {
int len = r.length();
if (offset>=0 && offset<=len) {
int start = offset;
while (start>0 && isValuePrefixChar(r.charAt(start-1))) {
start--;
}
int end = offset;
while (end<len && isValuePrefixChar(r.charAt(end))) {
end++;
}
r = r.subSequence(start, end);
if (!r.isEmpty()) {
return r.asTypedRegion(IPropertiesFilePartitions.PROPERTY_VALUE);
}
}
return null;
}
/**
* Search known properties for the best 'match' to show as hover data.
*/
private PropertyInfo findBestHoverMatch(String propName) {
debug(">> findBestHoverMatch("+propName+")");
debug("index size: "+getIndex().size());
//TODO: optimize, should be able to use index's treemap to find this without iterating all entries.
PropertyInfo best = null;
int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix
int bestExtraLen = Integer.MAX_VALUE;
for (PropertyInfo candidate : getIndex()) {
int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId());
int extraLen = candidate.getId().length()-commonPrefixLen;
if (commonPrefixLen==propName.length() && extraLen==0) {
//exact match found, can stop searching for better matches
return candidate;
}
//candidate is better if...
if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix
|| commonPrefixLen==bestCommonPrefixLen && extraLen<bestExtraLen //or same common prefix but fewer extra chars
) {
bestCommonPrefixLen = commonPrefixLen;
bestExtraLen = extraLen;
best = candidate;
}
}
debug("<< findBestHoverMatch("+propName+"): "+best);
return best;
}
public FuzzyMap<PropertyInfo> getIndex() {
return indexProvider.get();
}
public Provider<FuzzyMap<PropertyInfo>> getIndexProvider() {
return indexProvider;
}
public void setDocumentContextFinder(DocumentContextFinder it) {
this.documentContextFinder = it;
this.completionFactory = new PropertyCompletionFactory(it);
}
public void setIndexProvider(Provider<FuzzyMap<PropertyInfo>> it) {
this.indexProvider = it;
}
public void setTypeUtil(TypeUtil it) {
this.typeUtil = it;
}
public TypeUtil getTypeUtil() {
return typeUtil;
}
public boolean getPreferLowerCaseEnums() {
return preferLowerCaseEnums;
}
public void setPreferLowerCaseEnums(boolean preferLowerCaseEnums) {
this.preferLowerCaseEnums = preferLowerCaseEnums;
}
/**
* Find the longest known property that is a prefix of the given name. Here prefix does not mean
* 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So
* 'prefix' is not allowed to end in the middle of a 'segment'.
*/
public static PropertyInfo findLongestValidProperty(FuzzyMap<PropertyInfo> index, String name) {
int bracketPos = name.indexOf('[');
int endPos = bracketPos>=0?bracketPos:name.length();
PropertyInfo prop = null;
String prefix = null;
while (endPos>0 && prop==null) {
prefix = name.substring(0, endPos);
String canonicalPrefix = camelCaseToHyphens(prefix);
prop = index.get(canonicalPrefix);
if (prop==null) {
endPos = name.lastIndexOf('.', endPos-1);
}
}
if (prop!=null) {
//We should meet caller's expectation that matched properties returned by this method
// match the names exactly even if we found them using relaxed name matching.
return prop.withId(prefix);
}
return null;
}
}