diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java index eeff8901c..193c7d182 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java @@ -44,13 +44,6 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { private final YamlDocument doc; - private static PrefixFinder prefixfinder = new PrefixFinder() { - @Override - protected boolean isPrefixChar(char c) { - return !Character.isWhitespace(c); - } - }; - @Override public YamlDocument getDocument() { return doc; @@ -70,7 +63,14 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { // as the need for it arises in real use-cases. } - protected final String getPrefix(YamlDocument doc, SNode node, int offset) { + private static PrefixFinder prefixfinder = new PrefixFinder() { + @Override + protected boolean isPrefixChar(char c) { + return !Character.isWhitespace(c); + } + }; + + protected String getPrefix(YamlDocument doc, SNode node, int offset) { //For value completions... in general we would like to determine the whole text // corresponding to the value, so a simplistic backwards scan isn't good enough. // instead we should use offset in current node / structure to determine the diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index 743016fdc..bf80a09fb 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -324,8 +324,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { return null; } - @Override - public Collection getDashedCompletions(YamlDocument doc, SNode current, int offset) { + protected Collection getDashedCompletions(YamlDocument doc, SNode current, int offset) { try { YamlAssistContext relaxed = relaxForDashes(); if (relaxed!=null) { diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java index b571b191d..68e306059 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java @@ -20,8 +20,6 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode; -import com.google.common.collect.ImmutableList; - /** * @author Kris De Volder */ @@ -36,14 +34,4 @@ public interface YamlAssistContext extends YamlNavigable { Renderable getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion); YamlDocument getDocument(); - - /** - * The completion engine calls this instead of the more general `getCompletions` when it wants only completions - * that are suitable as list item. I.e. they must start with a '- '. - *

- * Implementors that don't support '- ' completions can just return an empty list. - */ - default Collection getDashedCompletions(YamlDocument doc, SNode current, int offset) { - return ImmutableList.of(); - } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index dfc9c9791..c6fbdbbbe 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016 Pivotal, Inc. + * Copyright (c) 2016-2017 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 @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,7 @@ import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SChildBearingNode; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SKeyNode; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNodeType; @@ -37,9 +39,12 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + /** * Implements {@link ICompletionEngine} for .yml file, based on a YamlAssistContextProvider - * which has to to be injected into engine via its contructor. + * which has to to be injected into engine via its constructor. * * @author Kris De Volder */ @@ -72,65 +77,104 @@ public class YamlCompletionEngine implements ICompletionEngine { if (!doc.isCommented(offset)) { SRootNode root = doc.getStructure(); SNode current = root.find(offset); - SNode contextNode = getContextNode(doc, current, offset); - List all = new ArrayList<>(getBaseCompletions(offset, doc, current, contextNode, false)); - all.addAll(getMoreIndentedCompletions(offset, doc, contextNode, current)); - all.addAll(getDashedCompletions(offset, doc, contextNode, current)); - return all; + List contextNodes = getContextNodes(doc, current, offset); + if (current.getNodeType()==SNodeType.RAW) { + //relaxed indentation + List completions = new ArrayList<>(); + int cursorIndent = doc.getColumn(offset); + int nodeIndent = current.getIndent(); + int baseIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); + for (SNode contextNode : contextNodes) { + completions.addAll(getRelaxedCompletions(offset, doc, current, contextNode, baseIndent)); + } + return completions; + } else { + //precise indentation only + Assert.isLegal(contextNodes.size()<=1); + for (SNode contextNode : contextNodes) { + return getBaseCompletions(offset, doc, current, contextNode); + } + } } return Collections.emptyList(); } - private Collection addIndentations( - Collection completions) { + protected Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent) { + try { + return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), + current, contextNode, baseIndent); + } catch (Exception e) { + Log.log(e); + } + return ImmutableList.of(); + } + + protected Collection fixIndentations(Collection completions, SNode currentNode, SNode contextNode, int baseIndent) { if (!completions.isEmpty()) { + int dashyIndent = getTargetIndent(contextNode, currentNode, true); + int plainIndent = getTargetIndent(contextNode, currentNode, false); List transformed = new ArrayList<>(); for (ICompletionProposal p : completions) { - transformed.add(indented(p)); + ICompletionProposal p_fixed = null; + if (p.getLabel().startsWith("- ")) { + p_fixed = indentFix(p, dashyIndent - baseIndent, contextNode); + } else { + p_fixed = indentFix(p, plainIndent - baseIndent, contextNode); + } + if (p_fixed!=null) { + transformed.add(p_fixed); + } } return transformed; } return Collections.emptyList(); } - public ICompletionProposal indented(ICompletionProposal proposal) { + protected ICompletionProposal indentFix(ICompletionProposal p, int fixIndentBy, SNode contextNode) { + if (fixIndentBy==0) { + return p; + } else if (fixIndentBy>0 && isExtraIndentRelaxable(contextNode)) { + return indented(p, Strings.repeat(" ", fixIndentBy)); + } else { // fixIndentBy < 0 + return null; + } + } + + /** + * Determine the indentation level needed to line up with other contextNode children. + * If the contextNode has no children, then compute a proper default indentation where + * a new child could be added. + */ + private int getTargetIndent(SNode contextNode, SNode currentNode, boolean dashy) { + Optional child = Optional.empty(); + if (contextNode instanceof SChildBearingNode) { + child = ((SChildBearingNode)contextNode).getChildren().stream() + .filter(c -> c!=currentNode && c.getIndent()>=0) + .max((c1, c2) -> Integer.compare(c1.getIndent(), c2.getIndent())); + } + if (child.isPresent()) { + return child.get().getIndent(); + } + return (dashy || contextNode.getNodeType()==SNodeType.DOC) + ? contextNode.getIndent() + : contextNode.getIndent() + YamlIndentUtil.INDENT_BY; + } + + public ICompletionProposal indented(ICompletionProposal proposal, String indentStr) { ScoreableProposal transformed = new TransformedCompletion(proposal) { @Override public String tranformLabel(String originalLabel) { return Unicodes.RIGHT_ARROW+" " + originalLabel; } @Override public DocumentEdits transformEdit(DocumentEdits originalEdit) { - originalEdit.indentFirstEdit(YamlIndentUtil.INDENT_STR); + originalEdit.indentFirstEdit(indentStr); return originalEdit; } }; - transformed.deemphasize(DEEMP_INDENTED_PROPOSAL); + transformed.deemphasize(DEEMP_INDENTED_PROPOSAL*indentStr.length()/2); return transformed; } - private Collection getDashedCompletions( - int offset, YamlDocument doc, - SNode preciseContextNode, SNode currentNode - ) throws Exception { - SNode contextNode = getContextNode(doc, currentNode, offset, "- "); - if (preciseContextNode!=contextNode) { - return getBaseCompletions(offset, doc, currentNode, contextNode, true); - } - return Collections.emptyList(); - } - - - private Collection getMoreIndentedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception { - SNode contextNode = getContextNode(doc, currentNode, offset, YamlIndentUtil.INDENT_STR); - if (preciseContextNode!=contextNode && isIndentRelaxable(contextNode)) { - YamlAssistContext context = getContext(doc, contextNode); - if (context!=null) { - return addIndentations(context.getCompletions(doc, currentNode, offset)); - } - } - return Collections.emptyList(); - } - - private boolean isIndentRelaxable(SNode contextNode) throws Exception { + private boolean isExtraIndentRelaxable(SNode contextNode) { return contextNode!=null && ( isBarrenKey(contextNode) || isBarrenSeq(contextNode) @@ -150,16 +194,20 @@ public class YamlCompletionEngine implements ICompletionEngine { return false; } - private boolean isBarrenKey(SNode node) throws Exception { - if (node.getNodeType()==SNodeType.KEY) { - SKeyNode keyNode = (SKeyNode) node; - String value = keyNode.getSimpleValue(); - return value.trim().isEmpty(); + private boolean isBarrenKey(SNode node) { + try { + if (node.getNodeType()==SNodeType.KEY) { + SKeyNode keyNode = (SKeyNode) node; + String value = keyNode.getSimpleValue(); + return value.trim().isEmpty(); + } + } catch (Exception e) { + Log.log(e); } return false; } - private Collection getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, boolean onlyDashes) throws Exception { + private Collection getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode) throws Exception { if (contextNode!=null) { YamlAssistContext context = getContext(doc, contextNode); if (context==null && isDubiousKey(contextNode, offset)) { @@ -168,13 +216,7 @@ public class YamlCompletionEngine implements ICompletionEngine { context = getContext(doc, contextNode); } if (context!=null) { - Collection all = new ArrayList<>(); - if (onlyDashes) { - all.addAll(context.getDashedCompletions(doc, current, offset)); - } else { - all.addAll(context.getCompletions(doc, current, offset)); - } - return all; + return context.getCompletions(doc, current, offset); } } return Collections.emptyList(); @@ -204,8 +246,15 @@ public class YamlCompletionEngine implements ICompletionEngine { } return null; } - - protected SNode getContextNode(YamlDocument doc, SNode node, int offset, String adjustIndentStr) throws Exception { + + /** + * Get context node candidates taking into account that we want to have a 'relaxed' interpretation + * of the context node with respect to the current indentation where we ask for a completion. + * To allow for the ambiguity in indentation a list of context nodes is returned instead of a + * single node. (Note we may still return a singleton list for cases where relaxed indentation + * doesn't seem desirable). + */ + protected List getContextNodes(YamlDocument doc, SNode node, int offset) { if (node==null) { return null; } else if (node.getNodeType()==SNodeType.KEY) { @@ -213,62 +262,98 @@ public class YamlCompletionEngine implements ICompletionEngine { // contexts for content assistance SKeyNode keyNode = (SKeyNode)node; if (keyNode.isInValue(offset)) { - return keyNode; + return ImmutableList.of(keyNode); } else { - return keyNode.getParent(); - } - } else if (node.getNodeType()==SNodeType.RAW) { - if (adjustIndentStr.startsWith("- ")) { - // We are trying to determine context node for a completion that starts with a '- '. - // Yaml indentation rules means we have to treat this differently because '-' doesn't - // have to be indented to be considered as nested under a key node! - int cursorIndent = doc.getColumn(offset); - int nodeIndent = node.getIndent(); - int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); - while (node.getNodeType()!=SNodeType.DOC && ( - nodeIndent==-1 || - nodeIndent>currentIndent || - nodeIndent==currentIndent && (node.getNodeType()==SNodeType.SEQ || node.getNodeType() == SNodeType.RAW) - )) { - node = node.getParent(); - nodeIndent = node.getIndent(); - } - return node; - } else { - //Treat raw node as a 'key node'. This is basically assuming that is misclasified - // by structure parser because the ':' was not yet typed into the document. - - //Complication: if line with cursor is empty or the cursor is inside the indentation - // area then the structure may not reflect correctly the context. This is because - // the correct context depends on text the user has not typed yet.(which will change the - // indentation level of the current line. So we must use the cursorIndentation - // rather than the structure-tree to determine the 'context' node. - int adjustIndent = adjustIndentStr==null? 0 : adjustIndentStr.length(); - int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent); - int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent); - int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); - while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) { - node = node.getParent(); - nodeIndent = node.getIndent(); - } - return node; + return ImmutableList.of(keyNode.getParent()); } } else if (node.getNodeType()==SNodeType.SEQ) { SSeqNode seqNode = (SSeqNode)node; if (seqNode.isInValue(offset)) { - return seqNode; + return ImmutableList.of(seqNode); } else { - return seqNode.getParent(); + return ImmutableList.of(seqNode.getParent()); } } else if (node.getNodeType()==SNodeType.DOC) { - return node; + return ImmutableList.of(node); + } else if (node.getNodeType()==SNodeType.RAW) { + //This node has flexibility around indentation. So this is where me need to build a list of candidates! + ImmutableList.Builder contextNodes = ImmutableList.builder(); + while (node!=null ) { + //Any node that represents a 'step' between contexts must be kept. + if (node.getSegment()!=null) { + contextNodes.add(node); + } + node = node.getParent(); + } + return contextNodes.build(); } - return null; + return ImmutableList.of(); } - protected SNode getContextNode(YamlDocument doc, SNode node, int offset) throws Exception { - return getContextNode(doc, node, offset, ""); - } +// protected SNode getContextNode(YamlDocument doc, SNode node, int offset, String adjustIndentStr) throws Exception { +// if (node==null) { +// return null; +// } else if (node.getNodeType()==SNodeType.KEY) { +// //slight complication. The area in the key and value of a key node represent different +// // contexts for content assistance +// SKeyNode keyNode = (SKeyNode)node; +// if (keyNode.isInValue(offset)) { +// return keyNode; +// } else { +// return keyNode.getParent(); +// } +// } else if (node.getNodeType()==SNodeType.RAW) { +// if (adjustIndentStr.startsWith("- ")) { +// // We are trying to determine context node for a completion that starts with a '- '. +// // Yaml indentation rules means we have to treat this differently because '-' doesn't +// // have to be indented to be considered as nested under a key node! +// int cursorIndent = doc.getColumn(offset); +// int nodeIndent = node.getIndent(); +// int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); +// while (node.getNodeType()!=SNodeType.DOC && ( +// nodeIndent==-1 || +// nodeIndent>currentIndent || +// nodeIndent==currentIndent && (node.getNodeType()==SNodeType.SEQ || node.getNodeType() == SNodeType.RAW) +// )) { +// node = node.getParent(); +// nodeIndent = node.getIndent(); +// } +// return node; +// } else { +// //Treat raw node as a 'key node'. This is basically assuming that is misclasified +// // by structure parser because the ':' was not yet typed into the document. +// +// //Complication: if line with cursor is empty or the cursor is inside the indentation +// // area then the structure may not reflect correctly the context. This is because +// // the correct context depends on text the user has not typed yet.(which will change the +// // indentation level of the current line. So we must use the cursorIndentation +// // rather than the structure-tree to determine the 'context' node. +// int adjustIndent = adjustIndentStr==null? 0 : adjustIndentStr.length(); +// int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent); +// int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent); +// int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); +// while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) { +// node = node.getParent(); +// nodeIndent = node.getIndent(); +// } +// return node; +// } +// } else if (node.getNodeType()==SNodeType.SEQ) { +// SSeqNode seqNode = (SSeqNode)node; +// if (seqNode.isInValue(offset)) { +// return seqNode; +// } else { +// return seqNode.getParent(); +// } +// } else if (node.getNodeType()==SNodeType.DOC) { +// return node; +// } +// return null; +// } +// +// protected SNode getContextNode(YamlDocument doc, SNode node, int offset) throws Exception { +// return getContextNode(doc, node, offset, ""); +// } protected YamlPath getContextPath(YamlDocument doc, SNode node, int offset) throws Exception { if (node==null) { diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java index d9847d2e5..86e638b87 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java @@ -327,7 +327,7 @@ public class YamlStructureParser { public YamlPath getPath() throws Exception { List path = new ArrayList<>(); for (SNode node : getPathNodes()) { - YamlPathSegment segment = getSegment(node); + YamlPathSegment segment = node.getSegment(); if (segment!=null) { path.add(segment); } @@ -340,19 +340,24 @@ public class YamlStructureParser { * null because not all SNodes can be interpreted as 'step' in the yml * structure (e.g. raw nodes will return null, as will the 'root' node). */ - private YamlPathSegment getSegment(SNode node) throws Exception { - if (node!=null) { - SNodeType nodeType = node.getNodeType(); - if (nodeType==SNodeType.KEY) { - String key = ((SKeyNode)node).getKey(); - return YamlPathSegment.valueAt(key); - } else if (nodeType==SNodeType.SEQ) { - int index = ((SSeqNode)node).getIndex(); - return YamlPathSegment.valueAt(index); - } else if (nodeType==SNodeType.DOC) { - int index = ((SDocNode)node).getIndex(); - return YamlPathSegment.valueAt(index); + public YamlPathSegment getSegment() { + try { + SNode node = this; + if (node!=null) { + SNodeType nodeType = node.getNodeType(); + if (nodeType==SNodeType.KEY) { + String key = ((SKeyNode)node).getKey(); + return YamlPathSegment.valueAt(key); + } else if (nodeType==SNodeType.SEQ) { + int index = ((SSeqNode)node).getIndex(); + return YamlPathSegment.valueAt(index); + } else if (nodeType==SNodeType.DOC) { + int index = ((SDocNode)node).getIndex(); + return YamlPathSegment.valueAt(index); + } } + } catch (Exception e) { + Log.log(e); } return null; }