Refactor: generalzing 'relaxed indentation'

This paves the way to doing relaxed content assist that are indented less
than the current CA query.
This commit is contained in:
Kris De Volder
2017-05-02 15:01:10 -07:00
parent cfe489d1c2
commit 4047f767ce
5 changed files with 209 additions and 132 deletions

View File

@@ -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

View File

@@ -324,8 +324,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
return null;
}
@Override
public Collection<ICompletionProposal> getDashedCompletions(YamlDocument doc, SNode current, int offset) {
protected Collection<ICompletionProposal> getDashedCompletions(YamlDocument doc, SNode current, int offset) {
try {
YamlAssistContext relaxed = relaxForDashes();
if (relaxed!=null) {

View File

@@ -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<YamlAssistContext> {
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 '- '.
* <p>
* Implementors that don't support '- ' completions can just return an empty list.
*/
default Collection<ICompletionProposal> getDashedCompletions(YamlDocument doc, SNode current, int offset) {
return ImmutableList.of();
}
}

View File

@@ -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<ICompletionProposal> 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<SNode> contextNodes = getContextNodes(doc, current, offset);
if (current.getNodeType()==SNodeType.RAW) {
//relaxed indentation
List<ICompletionProposal> 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<ICompletionProposal> addIndentations(
Collection<ICompletionProposal> completions) {
protected Collection<? extends ICompletionProposal> 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<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> completions, SNode currentNode, SNode contextNode, int baseIndent) {
if (!completions.isEmpty()) {
int dashyIndent = getTargetIndent(contextNode, currentNode, true);
int plainIndent = getTargetIndent(contextNode, currentNode, false);
List<ICompletionProposal> 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<SNode> 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<? extends ICompletionProposal> 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<ICompletionProposal> 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<? extends ICompletionProposal> getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, boolean onlyDashes) throws Exception {
private Collection<ICompletionProposal> 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<ICompletionProposal> 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<SNode> 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<SNode> 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) {

View File

@@ -327,7 +327,7 @@ public class YamlStructureParser {
public YamlPath getPath() throws Exception {
List<YamlPathSegment> 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;
}