/******************************************************************************* * Copyright (c) 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.editor.support.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.ide.eclipse.editor.support.EditorSupportActivator; import org.springframework.ide.eclipse.editor.support.completions.DocumentEdits; import org.springframework.ide.eclipse.editor.support.hover.HoverInfo; import org.springframework.ide.eclipse.editor.support.hover.YPropertyHoverInfo; import org.springframework.ide.eclipse.editor.support.util.CollectionUtil; import org.springframework.ide.eclipse.editor.support.util.FuzzyMatcher; 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.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.schema.YType; import org.springframework.ide.eclipse.editor.support.yaml.schema.YTypeUtil; import org.springframework.ide.eclipse.editor.support.yaml.schema.YTypedProperty; import org.springframework.ide.eclipse.editor.support.yaml.schema.YValueHint; 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; public class YTypeAssistContext extends AbstractYamlAssistContext { final private YTypeUtil typeUtil; final private YType type; final private YamlAssistContext parent; public YTypeAssistContext(YTypeAssistContext parent, YamlPath contextPath, YType YType, YTypeUtil typeUtil) { super(parent.documentSelector, contextPath); this.parent = parent; this.type = YType; this.typeUtil = typeUtil; } public YTypeAssistContext(TopLevelAssistContext parent, int documentSelector, YType type, YTypeUtil typeUtil) { super(documentSelector, YamlPath.EMPTY); this.type = type; this.typeUtil = typeUtil; this.parent = parent; } @Override public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { String query = getPrefix(doc, node, offset); List<ICompletionProposal> valueCompletions = getValueCompletions(doc, offset, query); if (!valueCompletions.isEmpty()) { return valueCompletions; } return getKeyCompletions(doc, offset, query); } public List<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception { int queryOffset = offset - query.length(); List<YTypedProperty> properties = typeUtil.getProperties(type); if (CollectionUtil.hasElements(properties)) { ArrayList<ICompletionProposal> proposals = new ArrayList<>(properties.size()); SNode contextNode = getContextNode(doc); Set<String> definedProps = getDefinedProperties(contextNode); for (YTypedProperty 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 YType YType = p.getType(); edits.delete(queryOffset, query); edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType)); 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(); } /** * 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(YType type) { //Note that proper indentation after each \n" is added automatically //to align with the parent. The strings created here only need to contain //indentation spaces to indent *more* than the parent node. if (type==null) { //Assume its some kind of pojo bean return "\n"+YamlIndentUtil.INDENT_STR; } else 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; } } 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<>(children.size()); for (SNode c : children) { if (c instanceof SKeyNode) { keys.add(((SKeyNode) c).getKey()); } } return keys; } } } catch (Exception e) { EditorSupportActivator.log(e); } return Collections.emptySet(); } private List<ICompletionProposal> getValueCompletions(YamlDocument doc, int offset, String query) { YValueHint[] values = typeUtil.getHintValues(type); if (values!=null) { ArrayList<ICompletionProposal> completions = new ArrayList<>(); for (YValueHint value : values) { double score = FuzzyMatcher.matchScore(query, value.getValue()); if (score!=0 && !value.equals(query)) { DocumentEdits edits = new DocumentEdits(doc.getDocument()); edits.delete(offset-query.length(), offset); edits.insert(offset, value.getValue()); completions.add(completionFactory().valueProposal(value.getValue(), query, value.getLabel(), type, score, edits, null)); } } return completions; } return Collections.emptyList(); } @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, YTypedProperty> subproperties = typeUtil.getPropertiesMap(type); if (subproperties!=null) { return contextWith(s, getType(subproperties.get(key))); } } else if (s.getType()==YamlPathSegmentType.VAL_AT_INDEX) { if (typeUtil.isSequencable(type)) { return contextWith(s, typeUtil.getDomainType(type)); } } return null; } private YType getType(YTypedProperty prop) { if (prop!=null) { return prop.getType(); } return null; } private YamlAssistContext contextWith(YamlPathSegment s, YType nextType) { if (nextType!=null) { return new YTypeAssistContext(this, contextPath.append(s), nextType, typeUtil); } return null; } @Override public String toString() { return "TypeContext("+contextPath.toPropString()+"::"+type+")"; } @Override public HoverInfo getHoverInfo() { if (parent!=null) { return parent.getHoverInfo(contextPath.getLastSegment()); } return null; } public YType getType() { return type; } @Override public HoverInfo getHoverInfo(YamlPathSegment lastSegment) { //Hoverinfo is only attached to YTypedProperties so... switch (lastSegment.getType()) { case VAL_AT_KEY: case KEY_AT_KEY: YTypedProperty prop = getProperty(lastSegment.toPropString()); if (prop!=null) { return new YPropertyHoverInfo(contextPath.toPropString(), getType(), prop); } break; default: } return null; } private YTypedProperty getProperty(String name) { return typeUtil.getPropertiesMap(getType()).get(name); } }