/*******************************************************************************
* Copyright (c) 2015, 2016 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.yaml.completions;
import java.util.ArrayList;
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.jface.text.contentassist.ICompletionProposal;
import org.springframework.boot.configurationmetadata.ValueHint;
import org.springframework.ide.eclipse.boot.core.BootActivator;
import org.springframework.ide.eclipse.boot.properties.editor.FuzzyMap;
import org.springframework.ide.eclipse.boot.properties.editor.FuzzyMap.Match;
import org.springframework.ide.eclipse.boot.properties.editor.RelaxedNameConfig;
import org.springframework.ide.eclipse.boot.properties.editor.completions.JavaTypeNavigationHoverInfo;
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.PropertyInfo;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.StsValueHint;
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.properties.editor.yaml.reconcile.IndexNavigator;
import org.springframework.ide.eclipse.editor.support.completions.CompletionFactory.ScoreableProposal;
import org.springframework.ide.eclipse.editor.support.completions.DocumentEdits;
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.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.StringUtil;
import org.springframework.ide.eclipse.editor.support.util.YamlIndentUtil;
import org.springframework.ide.eclipse.editor.support.yaml.YamlDocument;
import org.springframework.ide.eclipse.editor.support.yaml.completions.AbstractYamlAssistContext;
import org.springframework.ide.eclipse.editor.support.yaml.completions.TopLevelAssistContext;
import org.springframework.ide.eclipse.editor.support.yaml.completions.YamlAssistContext;
import org.springframework.ide.eclipse.editor.support.yaml.completions.YamlPathEdits;
import org.springframework.ide.eclipse.editor.support.yaml.completions.YamlUtil;
import org.springframework.ide.eclipse.editor.support.yaml.path.YamlPath;
import org.springframework.ide.eclipse.editor.support.yaml.path.YamlPathSegment;
import org.springframework.ide.eclipse.editor.support.yaml.path.YamlPathSegment.YamlPathSegmentType;
import org.springframework.ide.eclipse.editor.support.yaml.structure.YamlStructureParser.SChildBearingNode;
import org.springframework.ide.eclipse.editor.support.yaml.structure.YamlStructureParser.SKeyNode;
import org.springframework.ide.eclipse.editor.support.yaml.structure.YamlStructureParser.SNode;
/**
* Represents a context insied a "application.yml" file relative to which we can provide
* content assistance.
*/
public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistContext {
protected final RelaxedNameConfig conf;
// This may prove useful later but we don't need it for now
// /**
// * AssistContextKind is an classification of the different kinds of
// * syntactic context that CA can be invoked from.
// */
// public static enum Kind {
// SKEY_KEY, /* CA called from a SKeyNode and node.isInKey(cursor)==true */
// SKEY_VALUE, /* CA called from a SKeyNode and node.isInKey(cursor)==false */
// SRAW /* CA called from a SRawNode */
// }
// protected final Kind contextKind;
public final TypeUtil typeUtil;
public ApplicationYamlAssistContext(int documentSelector, YamlPath contextPath, TypeUtil typeUtil, RelaxedNameConfig conf) {
super(documentSelector, contextPath);
this.typeUtil = typeUtil;
this.conf = conf;
}
/**
* Computes the text that should be appended at the end of a completion
* proposal depending on what type of value is expected.
*/
protected String appendTextFor(Type type) {
//Note that proper indentation after each \n" is added automatically
//so the strings created here do not need to contain indentation spaces
if (TypeUtil.isMap(type)) {
//ready to enter nested map key on next line
return "\n"+YamlIndentUtil.INDENT_STR;
} if (TypeUtil.isSequencable(type)) {
//ready to enter sequence element on next line
return "\n- ";
} else if (typeUtil.isAtomic(type)) {
//ready to enter whatever on the same line
return " ";
} else {
//Assume its some kind of pojo bean
return "\n"+YamlIndentUtil.INDENT_STR;
}
}
/**
* @return the type expected at this context, may return null if unknown.
*/
protected abstract Type getType();
public static ApplicationYamlAssistContext subdocument(int documentSelector, FuzzyMap<PropertyInfo> index, PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) {
return new IndexContext(documentSelector, YamlPath.EMPTY, IndexNavigator.with(index), completionFactory, typeUtil, conf);
}
public static YamlAssistContext forPath(YamlPath contextPath, FuzzyMap<PropertyInfo> index, PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) {
try {
YamlPathSegment documentSelector = contextPath.getSegment(0);
if (documentSelector!=null) {
contextPath = contextPath.dropFirst(1);
YamlAssistContext context = ApplicationYamlAssistContext.subdocument(documentSelector.toIndex(), index, completionFactory, typeUtil, conf);
for (YamlPathSegment s : contextPath.getSegments()) {
if (context==null) return null;
context = context.traverse(s);
}
return context;
}
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
@Override
abstract public YamlAssistContext traverse(YamlPathSegment s) throws Exception;
private static class TypeContext extends ApplicationYamlAssistContext {
private PropertyCompletionFactory completionFactory;
private Type type;
private ApplicationYamlAssistContext parent;
private HintProvider hints;
public TypeContext(ApplicationYamlAssistContext parent, YamlPath contextPath, Type type,
PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf, HintProvider hints) {
super(parent.documentSelector, contextPath, typeUtil, conf);
this.parent = parent;
this.completionFactory = completionFactory;
this.type = type;
this.hints = hints;
}
private HintProvider getHintProvider() {
return hints;
}
@Override
public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
String query = getPrefix(doc, node, offset);
EnumCaseMode enumCaseMode = enumCaseMode(query);
BeanPropertyNameMode beanMode = conf.getBeanMode();
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, offset, query, enumCaseMode);
if (!valueCompletions.isEmpty()) {
return valueCompletions;
}
return getKeyCompletions(doc, offset, query, enumCaseMode, beanMode);
}
private EnumCaseMode enumCaseMode(String query) {
if (query.isEmpty()) {
return conf.getEnumMode();
} else {
return EnumCaseMode.ALIASED; // will match candidates from both lower and original based on what user typed
}
}
public List<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query,
EnumCaseMode enumCaseMode, BeanPropertyNameMode beanMode) throws Exception {
int queryOffset = offset - query.length();
List<TypedProperty> properties = getProperties(query, enumCaseMode, beanMode);
if (CollectionUtil.hasElements(properties)) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(properties.size());
SNode contextNode = getContextNode(doc);
Set<String> definedProps = getDefinedProperties(contextNode);
for (TypedProperty p : properties) {
String name = p.getName();
double score = FuzzyMatcher.matchScore(query, name);
if (score!=0) {
YamlPath relativePath = YamlPath.fromSimpleProperty(name);
YamlPathEdits edits = new YamlPathEdits(doc);
if (!definedProps.contains(name)) {
//property not yet defined
Type type = p.getType();
edits.delete(queryOffset, query);
edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(type));
proposals.add(completionFactory.beanProperty(doc.getDocument(),
contextPath.toPropString(), getType(),
query, p, score, edits, typeUtil)
);
} else {
//property already defined
// instead of filtering, navigate to the place where its defined.
deleteQueryAndLine(doc, query, queryOffset, edits);
//Cast to SChildBearingNode cannot fail because otherwise definedProps would be the empty set.
edits.createPath((SChildBearingNode) contextNode, relativePath, "");
proposals.add(
completionFactory.beanProperty(doc.getDocument(),
contextPath.toPropString(), getType(),
query, p, score, edits, typeUtil)
.deemphasize() //deemphasize because it already exists
);
}
}
}
return proposals;
}
return Collections.emptyList();
}
protected List<TypedProperty> getProperties(String query, EnumCaseMode enumCaseMode, BeanPropertyNameMode beanMode) {
ArrayList<TypedProperty> props = new ArrayList<>();
List<TypedProperty> fromType = typeUtil.getProperties(type, enumCaseMode, beanMode);
if (CollectionUtil.hasElements(fromType)) {
props.addAll(fromType);
}
HintProvider hints = getHintProvider();
if (hints!=null) {
List<TypedProperty> fromHints = hints.getPropertyHints(query);
if (CollectionUtil.hasElements(fromHints)) {
props.addAll(fromHints);
}
}
return props;
}
private Set<String> getDefinedProperties(SNode contextNode) {
try {
if (contextNode instanceof SChildBearingNode) {
List<SNode> children = ((SChildBearingNode)contextNode).getChildren();
if (CollectionUtil.hasElements(children)) {
Set<String> keys = new HashSet<String>(children.size());
for (SNode c : children) {
if (c instanceof SKeyNode) {
keys.add(((SKeyNode) c).getKey());
}
}
return keys;
}
}
} catch (Exception e) {
BootActivator.log(e);
}
return Collections.emptySet();
}
private List<ICompletionProposal> getValueCompletions(YamlDocument doc, int offset, String query, EnumCaseMode enumCaseMode) {
Collection<StsValueHint> hints = getHintValues(query, doc, offset, enumCaseMode);
if (hints!=null) {
ArrayList<ICompletionProposal> completions = new ArrayList<ICompletionProposal>();
for (StsValueHint hint : hints) {
String value = hint.getValue();
double score = FuzzyMatcher.matchScore(query, value);
if (score!=0 && !value.equals(query)) {
DocumentEdits edits = new DocumentEdits(doc.getDocument());
int valueStart = offset-query.length();
edits.delete(valueStart, offset);
if (doc.getChar(valueStart-1)==':') {
edits.insert(offset, " ");
}
edits.insert(offset, YamlUtil.stringEscape(value));
completions.add(completionFactory.valueProposal(value, query, type, score, edits, new ValueHintHoverInfo(hint)));
}
}
return completions;
}
return Collections.emptyList();
}
@Override
public HoverInfo getValueHoverInfo(YamlDocument doc, DocumentRegion valueRegion) {
String value = valueRegion.toString();
if (TypeUtil.isClass(type)) {
//Special case. We want hovers/hyperlinks even if the class is not a valid hint (as long as it is a class)
StsValueHint hint = StsValueHint.className(value.toString(), typeUtil);
if (hint!=null) {
return new ValueHintHoverInfo(hint);
}
}
Collection<StsValueHint> hints = getHintValues(value, doc, valueRegion.getEnd(), EnumCaseMode.ALIASED);
//The hints where found by fuzzy match so they may not actually match exactly!
for (StsValueHint h : hints) {
if (value.equals(h.getValue())) {
return new ValueHintHoverInfo(h);
}
}
return super.getValueHoverInfo(doc, valueRegion);
}
protected Collection<StsValueHint> getHintValues(
String query,
YamlDocument doc, int offset,
EnumCaseMode enumCaseMode
) {
Collection<StsValueHint> allHints = new ArrayList<>();
{
Collection<StsValueHint> hints = typeUtil.getHintValues(type, query, enumCaseMode);
if (CollectionUtil.hasElements(hints)) {
allHints.addAll(hints);
}
}
{
HintProvider hintProvider = getHintProvider();
if (hintProvider!=null) {
allHints.addAll(hintProvider.getValueHints(query));
}
}
return allHints;
}
@Override
public YamlAssistContext traverse(YamlPathSegment s) {
if (s.getType()==YamlPathSegmentType.VAL_AT_KEY) {
if (TypeUtil.isSequencable(type) || TypeUtil.isMap(type)) {
return contextWith(s, TypeUtil.getDomainType(type));
}
String key = s.toPropString();
Map<String, TypedProperty> subproperties = typeUtil.getPropertiesMap(type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
if (subproperties!=null) {
return contextWith(s, TypedProperty.typeOf(subproperties.get(key)));
}
} else if (s.getType()==YamlPathSegmentType.VAL_AT_INDEX) {
if (TypeUtil.isSequencable(type)) {
return contextWith(s, TypeUtil.getDomainType(type));
}
}
return null;
}
private AbstractYamlAssistContext contextWith(YamlPathSegment s, Type nextType) {
if (nextType!=null) {
return new TypeContext(this, contextPath.append(s), nextType, completionFactory, typeUtil, conf,
new YamlPath(s).traverse(hints));
}
return null;
}
@Override
public String toString() {
return "TypeContext("+contextPath.toPropString()+"::"+type+")";
}
@Override
public HoverInfo getHoverInfo() {
if (parent instanceof IndexContext) {
//this context is in fact an 'alias' of its parent, representing the
// point in the context hierarchy where a we transition from navigating
// the index to navigating type/bean properties
return parent.getHoverInfo();
} else {
return new JavaTypeNavigationHoverInfo(contextPath.toPropString(), contextPath.getBeanPropertyName(), parent.getType(), getType(), typeUtil);
}
}
@Override
protected Type getType() {
return type;
}
}
private static class IndexContext extends ApplicationYamlAssistContext {
private IndexNavigator indexNav;
PropertyCompletionFactory completionFactory;
public IndexContext(int documentSelector, YamlPath contextPath, IndexNavigator indexNav,
PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) {
super(documentSelector, contextPath, typeUtil, conf);
this.indexNav = indexNav;
this.completionFactory = completionFactory;
}
@Override
public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
String query = getPrefix(doc, node, offset);
Collection<Match<PropertyInfo>> matchingProps = indexNav.findMatching(query);
if (!matchingProps.isEmpty()) {
ArrayList<ICompletionProposal> completions = new ArrayList<ICompletionProposal>();
for (Match<PropertyInfo> match : matchingProps) {
ProposalApplier edits = createEdits(doc, offset, query, match);
ScoreableProposal completion = completionFactory.property(
doc.getDocument(), edits, match, typeUtil
);
if (getContextRoot(doc).exists(YamlPath.fromProperty(match.data.getId()))) {
completion.deemphasize();
}
completions.add(completion);
}
return completions;
}
return Collections.emptyList();
}
protected ProposalApplier createEdits(final YamlDocument file,
final int offset, final String query, final Match<PropertyInfo> match)
throws Exception {
//Edits created lazyly as they are somwehat expensive to compute and mostly
// we need only the edits for the one proposal that user picks.
return new LazyProposalApplier() {
@Override
protected ProposalApplier create() throws Exception {
YamlPathEdits edits = new YamlPathEdits(file);
int queryOffset = offset-query.length();
edits.delete(queryOffset, query);
YamlPath propertyPath = YamlPath.fromProperty(match.data.getId());
YamlPath relativePath = propertyPath.dropFirst(contextPath.size());
YamlPathSegment nextSegment = relativePath.getSegment(0);
SNode contextNode = getContextNode(file);
//To determine if this completion is 'in place' or needs to be inserted
// elsewhere in the tree, we check whether a node already exists in our
// context. If it doesn't we can create it as any child of the context
// so that includes, right at place the user is typing now.
SNode existingNode = contextNode.traverse(nextSegment);
String appendText = appendTextFor(TypeParser.parse(match.data.getType()));
if (existingNode==null) {
edits.createPathInPlace(contextNode, relativePath, queryOffset, appendText);
} else {
String wholeLine = file.getLineTextAtOffset(queryOffset);
if (wholeLine.trim().equals(query.trim())) {
edits.deleteLineBackwardAtOffset(queryOffset);
}
edits.createPath(getContextRoot(file), YamlPath.fromProperty(match.data.getId()), appendText);
}
return edits;
}
};
}
@Override
public AbstractYamlAssistContext traverse(YamlPathSegment s) {
if (s.getType()==YamlPathSegmentType.VAL_AT_KEY) {
String key = s.toPropString();
IndexNavigator subIndex = indexNav.selectSubProperty(key);
if (subIndex.isEmpty()) {
//Nothing found for actual key... maybe its a 'camelCased' alias of real key?
String keyAlias = StringUtil.camelCaseToHyphens(key);
if (!keyAlias.equals(key)) { // no point checking alias is the same (likely key was not camelCased)
IndexNavigator aliasedSubIndex = indexNav.selectSubProperty(keyAlias);
if (!aliasedSubIndex.isEmpty()) {
subIndex = aliasedSubIndex;
}
}
}
if (subIndex.getExtensionCandidate()!=null) {
return new IndexContext(documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf);
} else if (subIndex.getExactMatch()!=null) {
IndexContext asIndexContext = new IndexContext(documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf);
PropertyInfo prop = subIndex.getExactMatch();
return new TypeContext(asIndexContext, contextPath.append(s), TypeParser.parse(prop.getType()), completionFactory, typeUtil, conf, prop.getHints(typeUtil, true));
}
}
//Unsuported navigation => no context for assist
return null;
}
@Override
public String toString() {
return "YamlAssistIndexContext("+indexNav+")";
}
@Override
protected Type getType() {
PropertyInfo match = indexNav.getExactMatch();
if (match!=null) {
return TypeParser.parse(match.getType());
}
return null;
}
@Override
public HoverInfo getHoverInfo() {
PropertyInfo prop = indexNav.getExactMatch();
if (prop!=null) {
return new SpringPropertyHoverInfo(typeUtil.getJavaProject(), prop);
}
return null;
}
}
public abstract HoverInfo getHoverInfo();
public HoverInfo getHoverInfo(YamlPathSegment s) {
//ApplicationYamlAssistContext implements getHoverInfo directly. so this is not needed.
return null;
}
public static YamlAssistContext global(final FuzzyMap<PropertyInfo> index, final PropertyCompletionFactory completionFactory, final TypeUtil typeUtil, final RelaxedNameConfig conf) {
return new TopLevelAssistContext() {
@Override
protected YamlAssistContext getDocumentContext(int documentSelector) {
return subdocument(documentSelector, index, completionFactory, typeUtil, conf);
}
};
}
}