Port YamlStructureParser and its tests to yaml-commons

This commit is contained in:
Kris De Volder
2016-10-03 14:42:13 -07:00
parent e804ea03f9
commit 741d110bad
24 changed files with 2794 additions and 14 deletions

View File

@@ -1,7 +0,0 @@
package org.springframework.ide.vscode.commons.reconcile;
public interface IDocument {
String get();
}

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.reconcile;
import org.springframework.ide.vscode.util.IDocument;
public interface IReconcileEngine {
public void reconcile(IDocument doc, IProblemCollector problemCollector);
}

View File

@@ -0,0 +1,12 @@
package org.springframework.ide.vscode.util;
/**
* Replacement for Eclipse's BadLocationException (so as ot make porting code easier)
*/
public class BadLocationException extends Exception {
public BadLocationException(Throwable e) {
super(e);
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.ide.vscode.util;
import java.util.Collection;
/**
* @author Kris De Volder
*/
public class CollectionUtil {
public static <E> boolean hasElements(Collection<E> c) {
return c!=null && !c.isEmpty();
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.ide.vscode.util;
public class DocumentUtil {
/**
* Fetch text between two offsets. Doesn't throw BadLocationException.
* If either one or both of the offsets points outside the
* document then they will be adjusted to point the appropriate boundary to
* retrieve the text just upto the end or beginning of the document instead.
*/
public static String textBetween(IDocument doc, int start, int end) {
Assert.isLegal(start<=end);
if (start>=doc.getLength()) {
return "";
}
if (start<0) {
start = 0;
}
if (end>doc.getLength()) {
end = doc.getLength();
}
if (end<start) {
end = start;
}
try {
return doc.get(start, end-start);
} catch (BadLocationException e) {
//unless the code above is wrong... this is supposed to be impossible!
throw new IllegalStateException("Bug!", e);
}
}
}

View File

@@ -0,0 +1,16 @@
package org.springframework.ide.vscode.util;
public interface IDocument {
String get();
IRegion getLineInformationOfOffset(int offset);
int getLength();
String get(int start, int len) throws BadLocationException;
int getNumberOfLines();
String getDefaultLineDelimiter();
char getChar(int offset) throws BadLocationException;
int getLineOfOffset(int offset);
IRegion getLineInformation(int line);
int getLineOffset(int line);
}

View File

@@ -0,0 +1,11 @@
package org.springframework.ide.vscode.util;
/**
* Mimicks eclipse IRegion (i.e. a region is a offset + length).
*/
public interface IRegion {
int getOffset();
int getLength();
}

View File

@@ -0,0 +1,59 @@
package org.springframework.ide.vscode.util;
/**
* Trivial implementation of {@link IRegion}
* @author kdvolder
*
*/
public class Region implements IRegion {
private int ofs;
private int len;
public Region(int ofs, int len) {
super();
this.ofs = ofs;
this.len = len;
}
@Override
public int getOffset() {
return ofs;
}
@Override
public int getLength() {
return len;
}
@Override
public String toString() {
return "Region [ofs=" + ofs + ", len=" + len + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + len;
result = prime * result + ofs;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Region other = (Region) obj;
if (len != other.len)
return false;
if (ofs != other.ofs)
return false;
return true;
}
}

View File

@@ -5,8 +5,6 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.commons.reconcile.IDocument;
import io.typefox.lsapi.Range;
import io.typefox.lsapi.TextDocumentContentChangeEvent;
import io.typefox.lsapi.impl.PositionImpl;
@@ -118,6 +116,100 @@ public class TextDocument implements IDocument {
return array;
}
@Override
public IRegion getLineInformationOfOffset(int offset) {
if (offset<=getLength()) {
int line = lineNumber(offset);
return getLineInformation(line);
}
return null;
}
@Override
public int getLength() {
return text.length();
}
@Override
public String get(int start, int len) throws BadLocationException {
try {
return text.substring(start, start+len);
} catch (Exception e) {
throw new BadLocationException(e);
}
}
@Override
public int getNumberOfLines() {
return lineStarts().length;
}
@Override
public String getDefaultLineDelimiter() {
Matcher newlineFinder = NEWLINE.matcher(text);
if (newlineFinder.find()) {
return text.substring(newlineFinder.start(), newlineFinder.end());
}
return System.getProperty(System.getProperty("line.separator"));
}
@Override
public char getChar(int offset) throws BadLocationException {
try {
return text.charAt(offset);
} catch (Exception e) {
throw new BadLocationException(e);
}
}
@Override
public int getLineOfOffset(int offset) {
return lineNumber(offset);
}
@Override
public IRegion getLineInformation(int line) {
int[] starts = lineStarts();
if (line<starts.length) {
int start = starts[line];
int nextLine = line+1;
int end;
if (nextLine>=starts.length) {
//no next line. Last line in the document
end = getLength();
} else {
end = starts[line+1];
//To behave like Eclipse IDocument we must strip off line delimiter from the end.
char c1 = getSafeChar(end-1);
if (c1=='\r' || c1=='\n') {
end--;
char c2 = getSafeChar(end-1);
if (c1!=c2 && (c2=='\r' || c2=='\n')) {
end--;
}
}
}
int len = end - start;
if (len<0) {
len = 0;
}
return new Region(start, end-start);
}
return null;
}
private char getSafeChar(int ofs) {
try {
return getChar(ofs);
} catch (BadLocationException e) {
return 0;
}
}
@Override
public int getLineOffset(int line) {
// TODO Auto-generated method stub
return 0;
}
}

View File

@@ -13,5 +13,11 @@ public class Assert {
throw new IllegalStateException();
}
}
public static void isLegal(boolean b, String msg) {
if (!b) {
throw new IllegalStateException(msg);
}
}
}

View File

@@ -17,4 +17,12 @@ public class StringUtil {
}
return b.toString();
}
public static String trimEnd(String s) {
if (s!=null) {
return s.replaceAll("\\s+\\z", "");
}
return null;
}
}

View File

@@ -1,6 +1,6 @@
package org.springframework.ide.vscode.yaml.ast;
import org.springframework.ide.vscode.commons.reconcile.IDocument;
import org.springframework.ide.vscode.util.IDocument;
@FunctionalInterface
public interface YamlASTProvider {

View File

@@ -0,0 +1,37 @@
/*******************************************************************************
* 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.vscode.yaml.path;
import java.util.Collections;
/**
* Provides a way to compute all the 'aliases' that are considered 'equivalent' to a
* given key. This is used by structure traversals to try alternatives if the exact
* name itself doesn't correspond to a node in the tree.
* <p>
* A default implementation is provided where a key has no 'aliases'.
*
* @author Kris De Volder
*/
public interface KeyAliases {
public static final KeyAliases NONE = new KeyAliases() {
@Override
public Iterable<String> getKeyAliases(String base) {
return Collections.emptyList();
}
public String toString() { return "KeyAliasses.NONE"; };
};
Iterable<String> getKeyAliases(String base);
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2015 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.vscode.yaml.path;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode;
/**
* Different types of things (e.g. {@link ApplicationYamlAssistContext}, {@link SNode} ...) can
* be traversed interpeting {@link YamlPath} as 'navigation operations'. To facilitate
* 'reusable' traversal code, they can implement this interface.
*/
public interface YamlNavigable<T> {
T traverse(YamlPathSegment s) throws Exception;
}

View File

@@ -0,0 +1,268 @@
/*******************************************************************************
* 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.vscode.yaml.path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.ide.vscode.yaml.ast.NodeRef;
import org.springframework.ide.vscode.yaml.ast.NodeRef.RootRef;
import org.springframework.ide.vscode.yaml.ast.NodeRef.SeqRef;
import org.springframework.ide.vscode.yaml.ast.NodeRef.TupleValueRef;
import org.springframework.ide.vscode.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.yaml.path.YamlPathSegment.YamlPathSegmentType;
/**
* @author Kris De Volder
*/
public class YamlPath {
public static final YamlPath EMPTY = new YamlPath();
private final YamlPathSegment[] segments;
public YamlPath(List<YamlPathSegment> segments) {
this.segments = segments.toArray(new YamlPathSegment[segments.size()]);
}
public YamlPath() {
this.segments = new YamlPathSegment[0];
}
public YamlPath(YamlPathSegment... segments) {
this.segments = segments;
}
public String toPropString() {
StringBuilder buf = new StringBuilder();
boolean first = true;
for (YamlPathSegment s : segments) {
if (first) {
buf.append(s.toPropString());
} else {
buf.append(s.toNavString());
}
first = false;
}
return buf.toString();
}
public String toNavString() {
StringBuilder buf = new StringBuilder();
for (YamlPathSegment s : segments) {
buf.append(s.toNavString());
}
return buf.toString();
}
public YamlPathSegment[] getSegments() {
return segments;
}
/**
* Parse a YamlPath from a dotted property name. The segments are obtained
* by spliting the name at each dot.
*/
public static YamlPath fromProperty(String propName) {
ArrayList<YamlPathSegment> segments = new ArrayList<YamlPathSegment>();
for (String s : propName.split("\\.")) {
segments.add(YamlPathSegment.valueAt(s));
}
return new YamlPath(segments);
}
/**
* Create a YamlPath with a single segment (i.e. like 'fromProperty', but does
* not parse '.' as segment separators.
*/
public static YamlPath fromSimpleProperty(String name) {
return new YamlPath(YamlPathSegment.valueAt(name));
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("YamlPath(");
boolean first = true;
for (YamlPathSegment s : segments) {
if (!first) {
buf.append(", ");
}
buf.append(s);
first = false;
}
buf.append(")");
return buf.toString();
}
public int size() {
return segments.length;
}
public YamlPathSegment getSegment(int segment) {
if (segment>=0 && segment<segments.length) {
return segments[segment];
}
return null;
}
public YamlPath append(YamlPathSegment s) {
YamlPathSegment[] newPath = Arrays.copyOf(segments, segments.length+1);
newPath[segments.length] = s;
return new YamlPath(newPath);
}
public <T extends YamlNavigable<T>> T traverse(T startNode) {
try {
T node = startNode;
for (YamlPathSegment s : segments) {
if (node==null) {
return null;
}
node = node.traverse(s);
}
return node;
} catch (Exception e) {
return null;
}
}
public YamlPath dropFirst(int dropCount) {
if (dropCount>=size()) {
return EMPTY;
}
if (dropCount==0) {
return this;
}
YamlPathSegment[] newPath = new YamlPathSegment[segments.length-dropCount];
for (int i = 0; i < newPath.length; i++) {
newPath[i] = segments[i+dropCount];
}
return new YamlPath(newPath);
}
public YamlPath dropLast() {
return dropLast(1);
}
public YamlPath dropLast(int dropCount) {
if (dropCount>=size()) {
return EMPTY;
}
if (dropCount==0) {
return this;
}
YamlPathSegment[] newPath = new YamlPathSegment[segments.length-dropCount];
for (int i = 0; i < newPath.length; i++) {
newPath[i] = segments[i];
}
return new YamlPath(newPath);
}
public boolean isEmpty() {
return segments.length==0;
}
public YamlPath tail() {
return dropFirst(1);
}
/**
* Attempt to convert a path represented as a list of {@link NodeRef} into YamlPath.
* <p>
* Note that not all AST path can be converted into a YamlPath. Some paths in AST
* do not have a corresponding YamlPath. For such cases this method may return null.
*/
public static YamlPath fromASTPath(List<NodeRef<?>> path) {
List<YamlPathSegment> segments = new ArrayList<YamlPathSegment>(path.size());
for (NodeRef<?> nodeRef : path) {
switch (nodeRef.getKind()) {
case ROOT:
RootRef rref = (RootRef) nodeRef;
segments.add(YamlPathSegment.valueAt(rref.getIndex()));
break;
case KEY: {
String key = NodeUtil.asScalar(nodeRef.get());
if (key==null) {
return null;
} else {
segments.add(YamlPathSegment.keyAt(key));
} }
break;
case VAL: {
TupleValueRef vref = (TupleValueRef) nodeRef;
String key = NodeUtil.asScalar(vref.getTuple().getKeyNode());
if (key==null) {
return null;
} else {
segments.add(YamlPathSegment.valueAt(key));
} }
break;
case SEQ:
SeqRef sref = ((SeqRef)nodeRef);
segments.add(YamlPathSegment.valueAt(sref.getIndex()));
break;
default:
return null;
}
}
return new YamlPath(segments);
}
public YamlPathSegment getLastSegment() {
if (!isEmpty()) {
return segments[segments.length-1];
}
return null;
}
/**
* Attempt to interpret last segment of path as a bean property name.
* @return The name of the property or null if not applicable.
*/
public String getBeanPropertyName() {
if (!isEmpty()) {
YamlPathSegment lastSegment = getLastSegment();
YamlPathSegmentType kind = lastSegment.getType();
if (kind==YamlPathSegmentType.KEY_AT_KEY || kind==YamlPathSegmentType.VAL_AT_KEY) {
return lastSegment.toPropString();
}
}
return null;
}
public boolean pointsAtKey() {
YamlPathSegment s = getLastSegment();
return s!=null && s.getType()==YamlPathSegmentType.KEY_AT_KEY;
}
public boolean pointsAtValue() {
YamlPathSegment s = getLastSegment();
if (s!=null) {
YamlPathSegmentType type = s.getType();
return type==YamlPathSegmentType.VAL_AT_KEY || type==YamlPathSegmentType.VAL_AT_INDEX;
}
return false;
}
public YamlPath commonPrefix(YamlPath other) {
ArrayList<YamlPathSegment> common = new ArrayList<>(this.size());
for (int i = 0; i < this.size(); i++) {
YamlPathSegment s = this.getSegment(i);
if (s.equals(other.getSegment(i))) {
common.add(s);
}
}
return new YamlPath(common);
}
}

View File

@@ -0,0 +1,169 @@
/*******************************************************************************
* Copyright (c) 2015 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.vscode.yaml.path;
/**
* A YamlPathSegment is a 'primitive' NodeNavigator operation.
* More complex operations (i.e {@link YamlPath}) are composed as seqences
* of 0 or more {@link YamlPathSegment}s.
*
* @author Kris De Volder
*/
public abstract class YamlPathSegment {
public static enum YamlPathSegmentType {
VAL_AT_KEY, //Go to value associate with given key in a map.
KEY_AT_KEY, //Go to the key node associated with a given key in a map.
VAL_AT_INDEX //Go to value associate with given index in a sequence
}
public static class AtIndex extends YamlPathSegment {
private int index;
public AtIndex(int index) {
this.index = index;
}
public String toNavString() {
return "["+index+"]";
}
public String toPropString() {
return "["+index+"]";
}
@Override
public YamlPathSegmentType getType() {
return YamlPathSegmentType.VAL_AT_INDEX;
}
@Override
public Integer toIndex() {
return index;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + index;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AtIndex other = (AtIndex) obj;
if (index != other.index)
return false;
return true;
}
}
public static class ValAtKey extends YamlPathSegment {
private String key;
public ValAtKey(String key) {
this.key = key;
}
@Override
public String toNavString() {
if (key.indexOf('.')>=0) {
//TODO: what if key contains '[' or ']'??
return "["+key+"]";
}
return "."+key;
}
@Override
public String toPropString() {
//Don't start with a '.' if trying to build a 'self contained' expression.
return key;
}
@Override
public YamlPathSegmentType getType() {
return YamlPathSegmentType.VAL_AT_KEY;
}
@Override
public Integer toIndex() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ValAtKey other = (ValAtKey) obj;
if (key == null) {
if (other.key != null)
return false;
} else if (!key.equals(other.key))
return false;
return true;
}
}
private static class KeyAtKey extends ValAtKey {
public KeyAtKey(String key) {
super(key);
}
@Override
public YamlPathSegmentType getType() {
return YamlPathSegmentType.KEY_AT_KEY;
}
}
public String toString() {
return toNavString();
}
public abstract String toNavString();
public abstract String toPropString();
public abstract Integer toIndex();
public abstract YamlPathSegmentType getType();
public static YamlPathSegment valueAt(String key) {
return new ValAtKey(key);
}
public static YamlPathSegment valueAt(int index) {
return new AtIndex(index);
}
public static YamlPathSegment keyAt(String key) {
return new KeyAtKey(key);
}
}

View File

@@ -2,10 +2,10 @@ package org.springframework.ide.vscode.yaml.reconcile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.reconcile.IDocument;
import org.springframework.ide.vscode.commons.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.yaml.ast.YamlASTProvider;
import org.springframework.ide.vscode.yaml.ast.YamlFileAST;
import org.yaml.snakeyaml.error.Mark;

View File

@@ -1,8 +1,8 @@
package org.springframework.ide.vscode.yaml.reconcile;
import org.springframework.ide.vscode.commons.reconcile.IDocument;
import org.springframework.ide.vscode.commons.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.yaml.ast.YamlASTProvider;
import org.springframework.ide.vscode.yaml.schema.YamlSchema;

View File

@@ -0,0 +1,160 @@
/*******************************************************************************
* Copyright (c) 2015 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.vscode.yaml.structure;
import org.springframework.ide.vscode.util.BadLocationException;
import org.springframework.ide.vscode.util.DocumentUtil;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.util.IRegion;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode;
/**
* Wraps around a IDocument which is presumed to contain YML content and provides
* some utility methods for working with the contents of the document.
*
* @author Kris De Volder
*/
public class YamlDocument {
private IDocument doc;
private YamlStructureProvider structureProvider;
private SRootNode structure;
public YamlDocument(IDocument _doc, YamlStructureProvider structureProvider) {
this.doc = _doc;
this.structureProvider = structureProvider;
}
public IDocument getDocument() {
return doc;
}
public SRootNode getStructure() throws Exception {
if (this.structure==null) {
this.structure = structureProvider.getStructure(this);
}
return structure;
}
public int getLineOfOffset(int offset) throws BadLocationException {
return doc.getLineOfOffset(offset);
}
public IRegion getLineInformation(int line) throws BadLocationException {
return doc.getLineInformation(line);
}
public int getLineOffset(int line) throws BadLocationException {
return doc.getLineOffset(line);
}
/**
* Returns the number of leading spaces in front of a line. If the line is effectively empty (only contains
* comments and/or spaces then this returns -1 (meaning undefined, as indentation level only really means
* something for lines which have 'real' content.
*/
public int getLineIndentation(int line) {
IRegion r;
try {
r = getLineInformation(line);
} catch (BadLocationException e) {
//not a line in the document so it has no indentation
return -1;
}
int len = r.getLength();
int startOfLine = r.getOffset();
int leadingSpaces = 0;
while (leadingSpaces<len) {
char c = getChar(startOfLine+leadingSpaces);
if (c==' ') {
leadingSpaces++;
} else if (c=='#') {
return -1;
} else if (c!=' ') {
return leadingSpaces;
}
leadingSpaces++;
}
//Whole line scanned and nothing but spaces found
return -1;
}
public char getChar(int offset) {
try {
return doc.getChar(offset);
} catch (BadLocationException e) {
return 0;
}
}
/**
* Determine whether given offset is inside a comment.
*/
public boolean isCommented(int offset) throws Exception {
//Yaml only has end of line comments marked with a '#'.
//So comments never span multiple lines of text and we only have scan back
//from offset upto the start of the current line.
IRegion lineInfo = doc.getLineInformationOfOffset(offset);
int startOfLine = lineInfo.getOffset();
while (offset>=startOfLine) {
char c = getChar(offset);
if (c=='#') {
return true;
}
offset--;
}
return false;
}
/**
* Fetch text between two offsets. Doesn't throw BadLocationException.
* If either one or both of the offsets points outside the
* document then they will be adjusted to point the appropriate boundary to
* retrieve the text just upto the end or beginning of the document instead.
*/
public String textBetween(int start, int end) {
return DocumentUtil.textBetween(doc, start, end);
}
public int getColumn(int offset) throws Exception {
IRegion r = doc.getLineInformationOfOffset(offset);
return offset - r.getOffset();
}
/**
* Fetct text between a given offset and the start of the line that
* offset belongs to.
*/
public String getLineTextBefore(int offset) throws Exception {
IRegion l = doc.getLineInformationOfOffset(offset);
return textBetween(l.getOffset(), offset);
}
/**
* Fetch the text of the line at a given offset (i.e. all text extending from
* offset to the beginning and end of line)
*/
public String getLineTextAtOffset(int offset) throws Exception {
IRegion l = doc.getLineInformationOfOffset(offset);
return textBetween(l.getOffset(), l.getOffset()+l.getLength());
}
public int getStartOfLineAtOffset(int offset) throws Exception {
return doc.getLineInformationOfOffset(offset).getOffset();
}
@Override
public String toString() {
return "YamlDocument(>>>>\n"+getDocument().get()+"\n<<<<)";
}
}

View File

@@ -0,0 +1,753 @@
package org.springframework.ide.vscode.yaml.structure;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.util.Assert;
import org.springframework.ide.vscode.util.CollectionUtil;
import org.springframework.ide.vscode.util.IRegion;
import org.springframework.ide.vscode.util.StringUtil;
import org.springframework.ide.vscode.yaml.path.KeyAliases;
import org.springframework.ide.vscode.yaml.path.YamlNavigable;
import org.springframework.ide.vscode.yaml.path.YamlPath;
import org.springframework.ide.vscode.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.yaml.util.YamlIndentUtil;
/**
* A robust, coarse-grained parser that guesses the structure of a
* yml document based on indentation levels.
* <p>
* This is not a full parser but is desgned to succeed computing some kind of 'structure' tree
* for anything you might throw at it. The goal is to be accurate only for 'typical' yml files
* used to define spring-boot properties. Essentially a the file contains a bunch of nested
* mapping nodes in 'block' style using 'simple' keys.
* <p>
* I.e something like this:
* <pre>
* foo:
* bar:
* zor: Hello
* this is
* tex
* more-keys:
* - foo
* - bar
* </pre>
* <p>
* When the parser encounters something it can not identify as a 'simple-key: <value>'
* binding then it treats that just as 'raw' text data and associates it as nested
* information with the closest preceding recognized key node which is indented
* at the same or lower level than this node.
*
* @author Kris De Volder
*/
public class YamlStructureParser {
/**
* Pattern that matches a line starting with a 'simple key'
*/
public static final Pattern SIMPLE_KEY_LINE = Pattern.compile(
"^(\\w(\\.|\\w|-)*):( .*|$)");
//TODO: the parrern above is too selective (e.g. in real yaml one can have
//spaces in simple keys and lots of other characters that this pattern does not
//allow. For now it is good enough because we are only interested in spring property
//names which typically do not contain spaces and other funky characters.
/**
* Pattern that matches a line starting with a sequence header '- '
*/
public static final Pattern SEQ_LINE = Pattern.compile(
"^(\\-( |$)).*");
public static final Pattern DOCUMENT_SEPERATOR = Pattern.compile("^(---|\\.\\.\\.)(\\s)*(\\#.*)?");
//This expression matches:
// either "..." or "---" at the start of a line
// followed by arbitrary amount of whitepsace
// optionally followed by a "#" end of line comment.
//Note: "..." isn't a document separator but document terminator. Treating it as a separator is
// technically not correct. As the structure parser is meant to be 'robust' and do something
// sensible with incorrect input this makes sense here. The effect it will have is that user
// can type after a document terminator and get content assist as if they are in a new document.
// (They will also receive a syntax error message from the more formal and precise SnakeYaml parser)
// public static final Pattern SEQ_LINE = Pattern.compile(
// "^( *)- .*");
public static enum SNodeType {
ROOT, DOC, KEY, SEQ, RAW
}
private YamlLineReader input;
private final KeyAliases keyAliases;
public static class YamlLine {
// line = " hello"
// ^ ^ ^
// | | end
// | indent
// start
public static YamlLine atLineNumber(YamlDocument doc, int line) throws Exception {
if (line<doc.getDocument().getNumberOfLines()) {
IRegion l = doc.getLineInformation(line);
int start = l.getOffset();
int end = start + l.getLength();
return new YamlLine(doc, start, doc.getLineIndentation(line), end);
}
return null;
}
private YamlDocument doc;
private int start;
private int indent;
private int end;
private YamlLine(YamlDocument doc, int start, int indent, int end) {
this.doc = doc;
this.start = start;
this.indent = indent;
this.end = end;
}
public int getIndent() {
return indent;
}
public int getEnd() {
return end;
}
public int getStart() {
return start;
}
public boolean matches(Pattern pat) throws Exception {
return pat.matcher(getTextWithoutIndent()).matches();
}
public String getTextWithoutIndent() throws Exception {
return doc.textBetween(getStart()+getIndent(), getEnd());
}
public String getText() throws Exception {
return doc.textBetween(getStart(), getEnd());
}
@Override
public String toString() {
try {
return "YamlLine("+getLineNumber()+": "+getText()+")";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private int getLineNumber() throws Exception {
return doc.getLineOfOffset(start);
}
public YamlLine moveIndentMark(int moveBy) throws Exception {
return new YamlLine(doc, start, Math.min(indent+moveBy, getLineLength()), end);
}
private int getLineLength() throws Exception {
return getEnd()-getStart();
}
public YamlDocument getDocument() {
return doc;
}
}
public class YamlLineReader {
private final YamlDocument doc;
private int nextLine = 0; //next line to read
public YamlLineReader(YamlDocument doc) {
this.doc = doc;
}
public YamlLine read() throws Exception {
if (nextLine < doc.getDocument().getNumberOfLines()) {
return YamlLine.atLineNumber(doc, nextLine++);
}
return null; //means EOF
}
public YamlDocument getDocument() {
return doc;
}
}
public YamlStructureParser(YamlDocument doc, KeyAliases keyAliases) {
this.input = new YamlLineReader(doc);
this.keyAliases = keyAliases;
}
private static void indent(Writer out, int indent) throws Exception {
for (int i = 0; i < indent; i++) {
out.write(" ");
}
}
public static abstract class SNode implements YamlNavigable<SNode> {
private SChildBearingNode parent;
private int indent;
private int start;
private int end;
protected final YamlDocument doc;
public SNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) {
Assert.isLegal(this instanceof SRootNode || parent!=null);
this.parent = parent;
this.doc = doc;
this.indent = indent;
this.start = start;
this.end = end;
if (parent!=null) {
parent.addChild(this);
}
}
public SChildBearingNode getParent() {
return parent;
}
public int getStart() {
return start;
}
public int getNodeEnd() {
return end;
}
public abstract int getTreeEnd();
public final int getIndent() {
return indent;
}
public final String toString() {
StringWriter out = new StringWriter();
try {
dump(out, 0);
} catch (Exception e) {
throw new RuntimeException(e);
}
return out.toString();
}
public abstract SNodeType getNodeType();
public abstract SNode find(int offset);
public boolean nodeContains(int offset) {
return getStart()+Math.max(0, getIndent())<=offset && offset<=getNodeEnd();
}
public boolean treeContains(int offset) {
return getStart()<=offset && offset<=getTreeEnd();
}
public String getText() throws Exception {
return doc.textBetween(start, end);
}
/**
* Default implementation, doesn't support any type of traversal operation.
* Subclasses must override and implement where appropriate.
*/
@Override
public SNode traverse(YamlPathSegment s) throws Exception {
return null;
}
protected abstract void dump(Writer out, int indent) throws Exception;
public YamlPath getPath() throws Exception {
ArrayList<YamlPathSegment> segments = new ArrayList<YamlPathSegment>();
buildPath(this, segments);
return new YamlPath(segments);
}
private static void buildPath(SNode node, ArrayList<YamlPathSegment> segments) throws Exception {
if (node!=null) {
buildPath(node.getParent(), segments);
SNodeType nodeType = node.getNodeType();
if (nodeType==SNodeType.KEY) {
String key = ((SKeyNode)node).getKey();
segments.add(YamlPathSegment.valueAt(key));
} else if (nodeType==SNodeType.SEQ) {
int index = ((SSeqNode)node).getIndex();
segments.add(YamlPathSegment.valueAt(index));
} else if (nodeType==SNodeType.DOC) {
int index = ((SDocNode)node).getIndex();
segments.add(YamlPathSegment.valueAt(index));
}
}
}
public SRootNode getRoot() {
if (parent==null) {
return (SRootNode) this;
}
return parent.getRoot();
}
public SDocNode getDocNode() {
SNode it = this;
while (it!=null && !(it instanceof SDocNode)) {
it = it.getParent();
}
return (SDocNode) it;
}
}
public class SRootNode extends SChildBearingNode {
public SRootNode(YamlDocument doc) {
super(null, doc, 0,0,0);
}
@Override
public SNodeType getNodeType() {
return SNodeType.ROOT;
}
@Override
public void addChild(SNode c) {
Assert.isLegal(c.getNodeType()==SNodeType.DOC, ""+c.getNodeType());
super.addChild(c);
}
@Override
public SNode traverse(YamlPathSegment s) throws Exception {
Integer index = s.toIndex();
if (index!=null) {
List<SNode> cs = getChildren();
if (index>=0 && index<cs.size()) {
return cs.get(index);
}
}
return null;
}
}
public class SDocNode extends SChildBearingNode {
private int index;
/**
* If this SDocNode is started explicitly by '---' document separator
* then the start and end will be set according to its position.
* <p>
* If a document is started implicitly (at the start of the file/editor)
* then start and end are set to 0.
*/
public SDocNode(SRootNode parent, int start, int end) {
super(parent, parent.doc, 0, start, end);
this.index = parent.getChildren().size()-1;
}
public int getIndex() {
return index;
}
@Override
public SNodeType getNodeType() {
return SNodeType.DOC;
}
public boolean exists(YamlPath path) throws Exception {
return path.traverse((SNode)this) != null;
}
}
public abstract class SChildBearingNode extends SNode {
private List<SNode> children = null;
private Map<String, SKeyNode> keyMap = null; //lazily constructed index of children children.
public SChildBearingNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) {
super(parent, doc, indent, start, end);
}
public List<SNode> getChildren() {
if (children!=null) {
return Collections.unmodifiableList(children);
}
return Collections.emptyList();
}
public void addChild(SNode c) {
if (children==null) {
children = new ArrayList<SNode>();
}
children.add(c);
}
public SNode getLastChild() {
List<SNode> cs = getChildren();
if (!cs.isEmpty()) {
return cs.get(cs.size()-1);
}
return null;
}
@Override
public int getTreeEnd() {
if (getChildren().isEmpty()) {
return getNodeEnd();
}
return getLastChild().getTreeEnd();
}
@Override
protected final void dump(Writer out, int indent) throws Exception {
indent(out, indent);
out.write(getNodeType().toString());
out.write('(');
int nodeIndent = getIndent();
out.write(""+nodeIndent);
out.write("): ");
out.write(getText());
out.write('\n');
for (SNode child : getChildren()) {
child.dump(out, indent+1);
}
}
@Override
public SNode find(int offset) {
if (!treeContains(offset)) {
return null;
}
for (SNode c : getChildren()) {
SNode fromChild = c.find(offset);
if (fromChild!=null) {
return fromChild;
}
}
return this;
}
@Override
public SNode traverse(YamlPathSegment s) throws Exception {
switch (s.getType()) {
case VAL_AT_KEY:
return this.getChildWithKey(s.toPropString());
case VAL_AT_INDEX:
return this.getSeqChildWithIndex(s.toIndex());
default:
return null;
}
}
private SSeqNode getSeqChildWithIndex(int index) {
if (index>=0) {
List<SNode> children = getChildren();
if (index<children.size()) {
SNode child = children.get(index);
if (child instanceof SSeqNode) {
return (SSeqNode) child;
}
}
}
return null;
}
public SKeyNode getChildWithKey(String key) throws Exception {
if (CollectionUtil.hasElements(children)) {
SKeyNode child = keyMap().get(key);
if (child==null) {
Iterable<String> keyAliases = getKeyAliases(key);
if (keyAliases!=null) {
for (String keyAlias : keyAliases) {
child = keyMap().get(keyAlias);
if (child!=null) {
return child;
}
}
}
}
return child;
}
return null;
}
private Map<String, SKeyNode> keyMap() throws Exception {
if (keyMap==null) {
HashMap<String, SKeyNode> index = new HashMap<String, SKeyNode>();
for (SNode node: getChildren()) {
if (node.getNodeType()==SNodeType.KEY) {
SKeyNode keyNode = (SKeyNode)node;
String key = ((SKeyNode)node).getKey();
SKeyNode existing = index.get(key);
if (existing==null) {
index.put(key, keyNode);
}
}
}
keyMap = index;
}
return keyMap;
}
public SNode getFirstRealChild() {
for (SNode c : getChildren()) {
if (c.getIndent()>=0) {
return c;
}
}
return null;
}
}
public abstract class SLeafNode extends SNode {
public SLeafNode(SChildBearingNode parent, YamlDocument doc,
int indent, int start, int end) {
super(parent, doc, indent, start, end);
}
public int getTreeEnd() {
return getNodeEnd();
}
@Override
protected final void dump(Writer out, int indent) throws Exception {
indent(out, indent);
out.write(getNodeType().toString());
out.write('(');
int nodeIndent = getIndent();
out.write(""+nodeIndent);
out.write("): ");
out.write(getText());
out.write('\n');
}
@Override
public SNode find(int offset) {
if (treeContains(offset)) {
return this;
}
return null;
}
}
public class SRawNode extends SLeafNode {
public SRawNode(SChildBearingNode parent, YamlDocument doc, int indent,
int start, int end) {
super(parent, doc, indent, start, end);
}
@Override
public SNodeType getNodeType() {
return SNodeType.RAW;
}
}
public SRootNode parse() throws Exception {
SRootNode root = new SRootNode(input.getDocument());
SDocNode doc = new SDocNode(root,0,0);
SChildBearingNode parent = doc;
YamlLine line;
while (null!=(line=input.read())) {
int indent = line.getIndent();
if (indent==-1) {
createRawNode(parent, line);
} else {
parent = dropTo(parent, indent);
parent = parseLine(parent, line, true);
}
}
return root;
}
protected SChildBearingNode parseLine(SChildBearingNode parent, YamlLine line, boolean createRawNode) throws Exception {
if (line.matches(DOCUMENT_SEPERATOR)) {
parent = createDocNode(parent.getRoot(), line);
} else if (line.matches(SIMPLE_KEY_LINE)) {
int currentIndent = line.getIndent();
while (currentIndent==parent.getIndent() && parent.getNodeType()!=SNodeType.DOC) {
parent = parent.getParent();
}
parent = createKeyNode(parent, line);
} else if (line.matches(SEQ_LINE)) {
int currentIndent = line.getIndent();
while (currentIndent==parent.getIndent() && parent.getNodeType()==SNodeType.SEQ) {
parent = parent.getParent();
}
parent = createSeqNode(parent, line);
parent = parseLine(parent, line.moveIndentMark(2), false); //parse from just after "- " for nested seq and key nodes
} else if (createRawNode) {
createRawNode(parent, line);
}
return parent;
}
private SChildBearingNode createDocNode(SRootNode parent, YamlLine line) {
int start = line.getStart();
int end = line.getEnd();
return new SDocNode(parent, start, end);
}
private SChildBearingNode createSeqNode(SChildBearingNode parent, YamlLine line) throws Exception {
int indent = line.getIndent();
int start = line.getStart() + line.getIndent(); //use + is okay because seq node never have 'indefined' indent
int end = line.getEnd();
return new SSeqNode(parent, line.getDocument(), indent, start, end);
}
private SChildBearingNode createKeyNode(SChildBearingNode parent, YamlLine line) throws Exception {
int indent = line.getIndent();
int start = line.getStart() + line.getIndent(); //use + is okay because key node never have 'indefined' indent
int end = line.getEnd();
return new SKeyNode(parent, line.getDocument(), indent, start, end);
}
private SRawNode createRawNode(SChildBearingNode parent, YamlLine line) {
int indent = line.getIndent();
int start = YamlIndentUtil.addToOffset(line.getStart(), indent);
int end = line.getEnd();
return new SRawNode(parent, line.getDocument(), indent, start, end);
}
private SChildBearingNode dropTo(SChildBearingNode node, int indent) {
while (indent<node.getIndent()) {
node = node.getParent();
}
return node;
}
public class SSeqNode extends SChildBearingNode {
/**
* position of this in its parent. I.e. index is chosen such that
* parent.getChildren()[index] == this
*/
private int index;
public SSeqNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) throws Exception {
super(parent, doc, indent, start, end);
this.index = parent.getChildren().size()-1;
}
public int getIndex() {
return index;
}
@Override
public SNodeType getNodeType() {
return SNodeType.SEQ;
}
public boolean isInValue(int offset) {
return offset>=getStart()+2 //"- ".length()
&& offset <= getTreeEnd();
}
}
public class SKeyNode extends SChildBearingNode {
private int colonOffset;
public SKeyNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) throws Exception {
super(parent, doc, indent, start, end);
int relativeColonOffset = doc.textBetween(start, end).indexOf(':');
Assert.isLegal(relativeColonOffset>=0);
this.colonOffset = relativeColonOffset + start;
}
@Override
public SNodeType getNodeType() {
return SNodeType.KEY;
}
public String getKey() throws Exception {
return doc.textBetween(getStart(), getColonOffset());
}
/**
* Get the offset of the ':' character that separates the 'key' from the 'value' area.
* @return Absolute offset (from beginning of document).
*/
public int getColonOffset() {
return colonOffset;
}
public boolean isInKey(int offset) throws Exception {
return getStart()<=offset && offset <= getColonOffset();
}
public boolean isInValue(int offset) {
return offset> getColonOffset() && offset<=getTreeEnd();
}
/**
* Gets the raw text of the 'stuff' assigned to the key in this node.
* This includes all the text starting from the ':' upto the very end of this node,
* including the text for this node's children (if any).
*/
public String getValue() {
int start = getColonOffset()+1;
int end = getTreeEnd();
String indentedText = StringUtil.trimEnd(doc.textBetween(start, end));
List<SNode> children = getChildren();
int indent = determineIndentation(children);
if (indent>0) {
return stripIndentation(indent, indentedText);
}
return indentedText;
}
private String stripIndentation(int indent, String indentedText) {
StringBuilder out = new StringBuilder();
Pattern NEWLINE = Pattern.compile("(\\n|\\r)+");
boolean first = true;
Matcher matcher = NEWLINE.matcher(indentedText);
int pos = 0;
while (matcher.find()) {
int newline = matcher.start();
int newline_end = matcher.end();
String line = indentedText.substring(pos, newline);
if (first) {
first = false;
} else {
line = stripIndentationFromLine(indent, line);
}
out.append(line);
out.append(indentedText.substring(newline, newline_end));
pos = newline_end;
}
out.append(stripIndentationFromLine(indent, indentedText.substring(pos)));
return out.toString();
}
private String stripIndentationFromLine(int indent, String line) {
int start = 0;
while (start<line.length() && start < indent && line.charAt(start)==' ') {
start++;
}
return line.substring(start);
}
/**
* Determine the indentation of a block of children.
*/
private int determineIndentation(List<SNode> children) {
//The tricky bit is that the block may start with comment nodes which provide no hints about the indentation
//indicated by indentation level = -1
//So... we must take indentation from the first node that actually has one
if (children!=null) {
for (SNode c : children) {
int indent = c.getIndent();
if (indent>=0) {
return indent;
}
}
}
return -1; //Couldn't determine it.
}
}
private Iterable<String> getKeyAliases(String key) {
return keyAliases.getKeyAliases(key);
}
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.vscode.yaml.structure;
import org.springframework.ide.vscode.yaml.path.KeyAliases;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode;
/**
* @author Kris De Volder
*/
public abstract class YamlStructureProvider {
public abstract SRootNode getStructure(YamlDocument doc) throws Exception;
public static final YamlStructureProvider withAliases(final KeyAliases keyAliases) {
//TODO: its kind of fishy that we need this method. This is injecting some behavior
// related to 'alias aware' traversing of the parse tree. But this behavior probably
// doesn't belong in the parse tree but in the 'traverser'.
//
// So we should find a way to get rid of this method and move 'alias awareness'
// elsewhere.
//
// For now, however it was the easiest way to make the parser reusable without
// breaking Application.yml support.
return new YamlStructureProvider() {
public SRootNode getStructure(YamlDocument doc) throws Exception {
return new YamlStructureParser(doc, keyAliases).parse();
}
};
}
public static final YamlStructureProvider DEFAULT = new YamlStructureProvider() {
public SRootNode getStructure(YamlDocument doc) throws Exception {
return new YamlStructureParser(doc, KeyAliases.NONE).parse();
}
};
}

View File

@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (c) 2015 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.vscode.yaml.util;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.yaml.structure.YamlDocument;
/**
* Helper methods to mainpulate indentation levels.
*
* @author Kris De Volder
*/
public class YamlIndentUtil {
/**
* Number of indentation levels (spaces) added between a child and parent.
* TODO: replace this constant by (existing!) yedit preference value
*/
public static final int INDENT_BY = 2;
/**
* Some functions introduce line separators and this may depend on the context (i.e. default line separator
* for the current document).
*/
public final String NEWLINE;
public YamlIndentUtil(String newline) {
this.NEWLINE = newline;
}
public YamlIndentUtil(YamlDocument doc) {
IDocument d = doc.getDocument();
this.NEWLINE = d.getDefaultLineDelimiter();
}
/**
* Determine the 'known minimum' of two indentation levels. Correctly handle
* when either one or both indent levels are '-1' (unknown).
*/
public static int minIndent(int a, int b) {
if (a==-1) {
return b;
} else if (b==-1) {
return a;
} else {
return Math.min(a, b);
}
}
public static void addIndent(int indent, StringBuilder buf) {
for (int i = 0; i < indent; i++) {
buf.append(' ');
}
}
public void addNewlineWithIndent(int indent, StringBuilder buf) {
buf.append(NEWLINE);
addIndent(indent, buf);
}
public String newlineWithIndent(int indent) {
StringBuilder buf = new StringBuilder();
addNewlineWithIndent(indent, buf);
return buf.toString();
}
/**
* Applies a certain level of indentation to all new lines in the given text. Newlines
* are expressed by '\n' characters in the text will be replaced by the appropriate
* newline + indent.
* <p>
* Notes:
* - '\n' are replaced by the default line delimeter for the current document.
* - indentation is not applied to the first line of text.
*/
public String applyIndentation(String text, int indentBy) {
return text.replaceAll("\\n", newlineWithIndent(indentBy));
}
/**
* Increase offset by indentation. Take care when 'indent' is -1 (unkownn) to
* just return offset unmodified.
*/
public static int addToOffset(int offset, int indent) {
if (indent==-1) {
return offset;
}
return offset + indent;
}
}

View File

@@ -0,0 +1,980 @@
/*******************************************************************************
* Copyright (c) 2015 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.vscode.yaml.structure;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.junit.Test;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.util.TextDocument;
import org.springframework.ide.vscode.yaml.path.YamlPath;
import org.springframework.ide.vscode.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.yaml.structure.YamlDocument;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SChildBearingNode;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SDocNode;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode;
import org.springframework.ide.vscode.yaml.structure.YamlStructureProvider;
public class YamlStructureParserTest {
static class YamlEditor {
private String text;
public YamlEditor(String string) throws Exception {
this.text = string;
}
@Override
public String toString() {
return "YamlEditor("+text+")";
}
public SRootNode parseStructure() throws Exception {
YamlStructureProvider sp = YamlStructureProvider.DEFAULT;
TextDocument _doc = new TextDocument(null);
_doc.setText(text);
YamlDocument doc = new YamlDocument(_doc, sp);
return sp.getStructure(doc);
}
public String getRawText() {
return text;
}
public String getText() {
//No cursor support, not needed for these tests.
return getRawText();
}
public int startOf(String snippet) {
int start = text.indexOf(snippet);
assertTrue("Snippet not found in editor '"+snippet+"'", start>=0);
return start;
}
public String textBetween(int start, int end) {
return text.substring(start, end);
}
public String textUnder(SNode node) throws Exception {
int start = node.getStart();
int end = node.getTreeEnd();
return textBetween(start, end);
}
}
@Test public void testSimple() throws Exception {
YamlEditor editor = new YamlEditor(
"hello:\n"+
" world:\n" +
" message\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): hello:",
" KEY(2): world:",
" RAW(4): message",
" RAW(-1): "
);
}
public void assertParse(YamlEditor editor, String... expectDumpLines) throws Exception {
StringBuilder expected = new StringBuilder();
for (String line : expectDumpLines) {
expected.append(line);
expected.append("\n");
}
assertEquals(expected.toString().trim(), editor.parseStructure().toString().trim());
}
public void assertParseOneDoc(YamlEditor editor, String... expectDumpLines) throws Exception {
StringBuilder expected = new StringBuilder();
for (String line : expectDumpLines) {
expected.append(line);
expected.append("\n");
}
assertEquals(expected.toString().trim(), getOnlyDocument(editor.parseStructure()).toString().trim());
}
@Test public void testComments() throws Exception {
YamlEditor editor = new YamlEditor(
"#A comment\n" +
"hello:\n"+
" #Another comment\n" +
" world:\n" +
" message\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" RAW(-1): #A comment",
" KEY(0): hello:",
" RAW(-1): #Another comment",
" KEY(2): world:",
" RAW(4): message",
" RAW(-1): "
);
}
@Test public void testSiblings() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): world:",
" KEY(2): europe:",
" KEY(4): france:",
" RAW(6): cheese",
" KEY(4): belgium:",
" RAW(4): beer",
" KEY(2): canada:",
" KEY(4): montreal: poutine",
" KEY(4): vancouver:",
" RAW(6): salmon",
" KEY(0): moon:",
" KEY(2): moonbase-alfa:",
" RAW(4): moonstone",
" RAW(-1): "
);
}
@Test public void testMultiDocs() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
"---\n"+
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"---\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n" +
"...\n"
);
assertParse(editor,
"ROOT(0): ",
" DOC(0): ",
" KEY(0): world:",
" KEY(2): europe:",
" KEY(4): france:",
" RAW(6): cheese",
" KEY(4): belgium:",
" RAW(4): beer",
" DOC(0): ---",
" KEY(2): canada:",
" KEY(4): montreal: poutine",
" KEY(4): vancouver:",
" RAW(6): salmon",
" DOC(0): ---",
" KEY(0): moon:",
" KEY(2): moonbase-alfa:",
" RAW(4): moonstone",
" DOC(0): ...",
" RAW(-1): "
);
}
@Test public void testSequenceBasic() throws Exception {
YamlEditor editor;
//Sequence at root level
editor = new YamlEditor(
"- foo\n" +
"- bar\n" +
"- zor"
);
assertParseOneDoc(editor,
"DOC(0): ",
" SEQ(0): - foo",
" SEQ(0): - bar",
" SEQ(0): - zor"
);
//Sequences nested in map without indent
editor = new YamlEditor(
"something:\n" +
"- foo\n" +
"- bar\n" +
"- zor\n" +
"else:\n" +
"- a\n" +
"- def"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): something:" ,
" SEQ(0): - foo",
" SEQ(0): - bar",
" SEQ(0): - zor",
" KEY(0): else:",
" SEQ(0): - a",
" SEQ(0): - def"
);
//Sequences nested in map without indent
editor = new YamlEditor(
"higher:\n" +
" something:\n" +
" - foo\n" +
" - bar\n" +
" - zor\n" +
" else:\n" +
" - a\n" +
" - def"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): higher:",
" KEY(2): something:" ,
" SEQ(2): - foo",
" SEQ(2): - bar",
" SEQ(2): - zor",
" KEY(2): else:",
" SEQ(2): - a",
" SEQ(2): - def"
);
//Sequences nested in map with indent
editor = new YamlEditor(
"something:\n" +
" - foo\n" +
" - bar\n" +
" - zor\n" +
"else:\n" +
" - a\n" +
" - def"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): something:" ,
" SEQ(2): - foo",
" SEQ(2): - bar",
" SEQ(2): - zor",
" KEY(0): else:",
" SEQ(2): - a",
" SEQ(2): - def"
);
}
@Test public void testKeyWithADot() throws Exception {
YamlEditor editor;
//First try without a '.'
editor = new YamlEditor(
"logging:\n" +
" level:\n" +
" somepackage: "
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): logging:",
" KEY(2): level:",
" KEY(4): somepackage:"
);
editor = new YamlEditor(
"logging:\n" +
" level:\n" +
" some.package: "
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): logging:",
" KEY(2): level:",
" KEY(4): some.package:"
);
}
@Test public void testSequenceWithNestedSequence() throws Exception {
YamlEditor editor;
editor = new YamlEditor(
"- - a\n" +
" - b\n" +
"- - c\n" +
" - d\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" SEQ(0): - - a",
" SEQ(2): - a",
" SEQ(2): - b",
" SEQ(0): - - c",
" SEQ(2): - c",
" SEQ(2): - d",
" RAW(-1): "
);
editor = new YamlEditor(
"foo:\n" +
"- - a\n" +
" - b\n" +
"- - c\n" +
" - d"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): - - a",
" SEQ(2): - a",
" SEQ(2): - b",
" SEQ(0): - - c",
" SEQ(2): - c",
" SEQ(2): - d"
);
editor = new YamlEditor(
"foo:\n" +
"- - a\n" +
" - b\n" +
"bar:\n" +
"- - c\n" +
" - d\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): - - a",
" SEQ(2): - a",
" SEQ(2): - b",
" KEY(0): bar:",
" SEQ(0): - - c",
" SEQ(2): - c",
" SEQ(2): - d",
" RAW(-1): "
);
editor = new YamlEditor(
"foo:\n" +
"- \n" +
" - a\n" +
" - b\n" +
"-\n" +
" - c\n" +
" - d"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): - ",
" SEQ(2): - a",
" SEQ(2): - b",
" SEQ(0): -",
" SEQ(2): - c",
" SEQ(2): - d"
);
editor = new YamlEditor(
"foo:\n" +
"- - - - a\n" +
" - b\n" +
" - c\n" +
" - d\n" +
"- e\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): - - - - a",
" SEQ(2): - - - a",
" SEQ(4): - - a",
" SEQ(6): - a",
" SEQ(6): - b",
" SEQ(4): - c",
" SEQ(2): - d",
" SEQ(0): - e",
" RAW(-1): "
);
editor = new YamlEditor(
"foo:\n" +
"- - - - a\n" +
" - c\n" +
"- e\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): - - - - a",
" SEQ(2): - - - a",
" SEQ(4): - - a",
" SEQ(6): - a",
" SEQ(4): - c",
" SEQ(0): - e",
" RAW(-1): "
);
}
@Test public void testSequenceWithNestedMap() throws Exception {
YamlEditor editor;
// A map nested in a sequence may start on the same line
editor = new YamlEditor(
"- foo: is foo\n" +
" bar: is bar\n" +
" junk\n" +
"- a: aaa\n" +
" b: bbb\n"
);
assertParseOneDoc(editor,
"DOC(0): ",
" SEQ(0): - foo: is foo",
" KEY(2): foo: is foo",
" KEY(2): bar: is bar",
" RAW(4): junk",
" SEQ(0): - a: aaa",
" KEY(2): a: aaa",
" KEY(2): b: bbb",
" RAW(-1): "
);
//A map nested in a sequence may start on a new line
editor = new YamlEditor(
"-\n"+ //without space
" foo: is foo\n" +
" bar: is bar\n" +
" junk\n" +
"- \n" + //with space
" a: aaa\n" +
" b: bbb"
);
assertParseOneDoc(editor,
"DOC(0): ",
" SEQ(0): -",
" KEY(2): foo: is foo",
" KEY(2): bar: is bar",
" RAW(4): junk",
" SEQ(0): - ", //with space
" KEY(2): a: aaa",
" KEY(2): b: bbb"
);
editor = new YamlEditor(
"foo:\n" +
"-\n"+ //without space
" foo: is foo\n" +
" bar: is bar\n" +
" junk\n" +
"- \n" + //with space
" a: aaa\n" +
" b: bbb"
);
assertParseOneDoc(editor,
"DOC(0): ",
" KEY(0): foo:",
" SEQ(0): -",
" KEY(2): foo: is foo",
" KEY(2): bar: is bar",
" RAW(4): junk",
" SEQ(0): - ", //with space
" KEY(2): a: aaa",
" KEY(2): b: bbb"
);
}
@Test public void testTraverseSeq() throws Exception {
YamlEditor editor = new YamlEditor(
"foo:\n" +
"- - - - a\n" +
" - c\n" +
"- e"
);
SRootNode root = editor.parseStructure();
YamlPath path;
path = pathWith(0, "foo", 0, 0, 0, 0);
assertEquals(
"SEQ(6): - a\n",
path.traverse((SNode)root).toString());
path = pathWith(0, "foo", -1);
assertNull(path.traverse((SNode)root));
path = pathWith(0, "foo", 1);
assertEquals(
"SEQ(0): - e\n",
path.traverse((SNode)root).toString());
path = pathWith(0, "foo", 2);
assertNull(path.traverse((SNode)root));
}
@Test public void testFindAndTraverseSeqNode() throws Exception {
YamlEditor editor;
editor = new YamlEditor(
"foo:\n"+
"- abc\n" +
"- def\n" +
"- ghi\n"
);
findAndTraversPathPath(editor, "abc");
findAndTraversPathPath(editor, "def");
findAndTraversPathPath(editor, "ghi");
// nodes are position sensitive make sure that generated positions agree
// with traverse interpretation, even in case where it is not so well-defined
// how the indices should be interpreted:
editor = new YamlEditor(
"foo:\n"+
" garbage\n" +
" - abc\n" +
" junk\n" +
" - def\n" +
" crap\n" +
" - ghi\n"
);
findAndTraversPathPath(editor, "abc");
findAndTraversPathPath(editor, "def");
findAndTraversPathPath(editor, "ghi");
}
private void findAndTraversPathPath(YamlEditor editor, String snippet) throws Exception {
SRootNode root = editor.parseStructure();
SNode node = root.find(editor.startOf(snippet));
assertNotNull(node);
YamlPath path = node.getPath();
SNode actualNode = path.traverse((SNode)root);
assertEquals(node, actualNode);
}
@Test public void testTraverseSeqKey() throws Exception {
YamlEditor editor = new YamlEditor(
"foo:\n" +
"- bar:\n" +
" - a\n" +
" - key: lol\n" +
"- e\n"
);
SRootNode root = editor.parseStructure();
YamlPath path;
path = pathWith(0, "foo", 0, "bar", 1, "key");
assertEquals(
"KEY(4): key: lol\n",
path.traverse((SNode)root).toString());
}
@Test public void testTreeEnd() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
SNode node = getNodeAtPath(root, 0, 0, 1);
assertTreeText(editor, node,
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n"
);
node = getNodeAtPath(root, 0, 0, 0, 1, 0);
assertTreeText(editor, node,
"beer"
);
}
@Test public void testTreeEndKeyNodeNoChildren() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
SNode node = getNodeAtPath(root, 0, 0, 0);
assertTreeText(editor, node,
" europe:"
);
}
@Test public void testFind() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
assertFind(editor, root, "world:", 0, 0);
assertFind(editor, root, "europe:", 0, 0, 0);
assertFind(editor, root, "france:", 0, 0, 0, 0);
assertFind(editor, root, "cheese", 0, 0, 0, 0, 0);
assertFind(editor, root, "belgium:", 0, 0, 0, 1);
assertFind(editor, root, "beer", 0, 0, 0, 1, 0);
assertFind(editor, root, "canada:", 0, 0, 1);
assertFind(editor, root, "montreal: poutine", 0, 0, 1, 0);
assertFind(editor, root, "vancouver:", 0, 0, 1, 1);
assertFind(editor, root, "salmon", 0, 0, 1, 1, 0);
assertFind(editor, root, "moon:", 0, 1);
assertFind(editor, root, "moonbase-alfa:", 0, 1, 0);
assertFind(editor, root, "moonstone", 0, 1, 0, 0);
assertFindStart(editor, root, " europe:", 0, 0);
}
@Test public void testFindInMultiDoc() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
"---\n" +
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"---\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n" +
"..."
);
SRootNode root = editor.parseStructure();
assertFind(editor, root, "world:", 0, 0);
assertFind(editor, root, "europe:", 0, 0, 0);
assertFind(editor, root, "france:", 0, 0, 0, 0);
assertFind(editor, root, "cheese", 0, 0, 0, 0, 0);
assertFind(editor, root, "belgium:", 0, 0, 0, 1);
assertFind(editor, root, "beer", 0, 0, 0, 1, 0);
assertFind(editor, root, "canada:", 1, 0);
assertFind(editor, root, "montreal: poutine", 1, 0, 0);
assertFind(editor, root, "vancouver:", 1, 0, 1);
assertFind(editor, root, "salmon", 1, 0, 1, 0);
assertFind(editor, root, "moon:", 2, 0);
assertFind(editor, root, "moonbase-alfa:", 2, 0, 0);
assertFind(editor, root, "moonstone", 2, 0, 0, 0);
assertFindStart(editor, root, " canada:", 1);
}
@Test public void testFindInSequence() throws Exception {
YamlEditor editor = new YamlEditor(
"foo:\n" +
"- alchemy\n" +
"- bistro\n" +
"bar:\n" +
"- - - nice: text\n"+
"zor:\n" +
" - - - very: good\n" +
"end: END"
);
SRootNode root = editor.parseStructure();
assertFind (editor, root, "foo:", 0, 0);
assertFind (editor, root, "- alchemy", 0, 0, 0);
assertFind (editor, root, "- bistro", 0, 0, 1);
assertFind (editor, root, "bar:", 0, 1);
assertFindStart(editor, root, "- - - nice: text", 0, 1, 0);
assertFindStart(editor, root, " - - nice: text", 0, 1, 0);
assertFindStart(editor, root, "- - nice: text", 0, 1, 0, 0);
assertFindStart(editor, root, " - nice: text", 0, 1, 0, 0);
assertFindStart(editor, root, "- nice: text", 0, 1, 0, 0, 0);
assertFindStart(editor, root, " nice: text", 0, 1, 0, 0, 0);
assertFind (editor, root, "nice: text", 0, 1, 0, 0, 0, 0);
assertFind (editor, root, "zor:", 0, 2);
assertFindStart(editor, root, " - - - very: good", 0, 2);
assertFindStart(editor, root, " - - - very: good", 0, 2);
assertFindStart(editor, root, "- - - very: good", 0, 2, 0);
assertFindStart(editor, root, " - - very: good", 0, 2, 0);
assertFindStart(editor, root, "- - very: good", 0, 2, 0, 0);
assertFindStart(editor, root, " - very: good", 0, 2, 0, 0);
assertFindStart(editor, root, "- very: good", 0, 2, 0, 0, 0);
assertFindStart(editor, root, " very: good", 0, 2, 0, 0, 0);
assertFind (editor, root, "very: good", 0, 2, 0, 0, 0, 0);
}
@Test public void testGetKey() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
assertKey(editor, root, "world:", "world");
assertKey(editor, root, "europe:", "europe");
assertKey(editor, root, "montreal: poutine", "montreal");
}
@Test public void testIsInValue() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"foo:\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
assertValueRange(editor, root, "montreal: poutine", " poutine");
assertValueRange(editor, root, "europe:", "\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer");
assertValueRange(editor, root, "foo:", null);
}
private void assertValueRange(YamlEditor editor, SRootNode root, String nodeText, String expectedValue) throws Exception {
int start = editor.getText().indexOf(nodeText);
SKeyNode node = (SKeyNode) root.find(start);
int valueRangeStart;
int valueRangeEnd;
if (expectedValue==null) {
valueRangeStart = valueRangeEnd = start+nodeText.length();
} else {
valueRangeStart = editor.getRawText().lastIndexOf(expectedValue);
valueRangeEnd = valueRangeStart+expectedValue.length();
assertEquals(expectedValue, editor.textBetween(valueRangeStart, valueRangeEnd));
}
assertTrue(node.isInValue(valueRangeStart));
assertFalse(node.isInValue(valueRangeStart-1));
assertTrue(node.isInValue(valueRangeEnd));
assertFalse(node.isInValue(valueRangeEnd+1));
}
@Test public void testTraverse() throws Exception {
YamlEditor editor = new YamlEditor(
"world:\n" +
" europe:\n" +
" france:\n" +
" cheese\n" +
" belgium:\n" +
" beer\n" + //At same level as key, technically this is a syntax error but we tolerate it
" canada:\n" +
" montreal: poutine\n" +
" vancouver:\n" +
" salmon\n" +
"moon:\n" +
" moonbase-alfa:\n" +
" moonstone\n"
);
SRootNode root = editor.parseStructure();
YamlPath pathToFrance = pathWith(
0, "world", "europe", "france"
);
assertEquals(
"KEY(4): france:\n"+
" RAW(6): cheese\n",
pathToFrance.traverse((SNode)root).toString());
assertNull(pathWith(0, "world", "europe", "bogus").traverse((SNode)root));
}
@Test public void testGetFirstRealChild() throws Exception {
YamlEditor editor = new YamlEditor(
"no-children:\n" +
"unreal-children:\n" +
" #Unreal\n" +
"\n" +
" #comment only\n" +
"real-child:\n" +
" abc\n" +
"mixed-children:\n" +
"\n" +
"#comment\n" +
" def"
);
assertFirstRealChild(editor, "no-children", null);
assertFirstRealChild(editor, "unreal-children", null);
assertFirstRealChild(editor, "real-child", "abc");
assertFirstRealChild(editor, "mixed-children", "def");
}
@Test public void testDocumentSeparatorRegexp() throws Exception {
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "---");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "...");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "--- ");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "... ");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "--- #The next doc starts here");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "... #The previous doc ends here");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "---#The next doc starts here");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "...#The previous doc ends here");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "---#");
assertMatch(YamlStructureParser.DOCUMENT_SEPERATOR, "...#");
}
private void assertMatch(Pattern pat, String string) {
assertTrue("Doesn't match: '"+string+"'", pat.matcher(string).matches());
}
private void assertFirstRealChild(YamlEditor editor, String testNodeName, String expectedNodeSnippet) throws Exception {
SDocNode doc = getOnlyDocument(editor.parseStructure());
SKeyNode testNode = doc.getChildWithKey(testNodeName);
assertNotNull(testNode);
SNode expected = null;
if (expectedNodeSnippet!=null) {
int offset = editor.getRawText().indexOf(expectedNodeSnippet);
expected = doc.find(offset);
assertTrue(editor.textUnder(expected).contains(expectedNodeSnippet));
}
assertEquals(expected, testNode.getFirstRealChild());
}
private SDocNode getOnlyDocument(SRootNode root) {
assertEquals(1, root.getChildren().size());
return (SDocNode) root.getChildren().get(0);
}
private YamlPath pathWith(Object... keysOrIndexes) {
ArrayList<YamlPathSegment> segments = new ArrayList<YamlPathSegment>();
for (Object keyOrIndex : keysOrIndexes) {
if (keyOrIndex instanceof String) {
segments.add(YamlPathSegment.valueAt((String)keyOrIndex));
} else if (keyOrIndex instanceof Integer) {
segments.add(YamlPathSegment.valueAt((Integer)keyOrIndex));
} else {
fail("Unknown type of path element: "+keyOrIndex);
}
}
return new YamlPath(segments);
}
private void assertKey(YamlEditor editor, SRootNode root, String nodeText, String expectedKey) throws Exception {
int start = editor.getText().indexOf(nodeText);
SKeyNode node = (SKeyNode) root.find(start);
String key = node.getKey();
assertEquals(expectedKey, key);
//test the key range as well
int startOfKeyRange = node.getStart();
int keyRangeLen = key.length();
int endOfKeyRange = startOfKeyRange + keyRangeLen;
assertTrue(node.isInKey(startOfKeyRange));
assertFalse(node.isInKey(startOfKeyRange-1));
assertTrue(node.isInKey(endOfKeyRange));
assertFalse(node.isInKey(endOfKeyRange+1));
}
private void assertFind(YamlEditor editor, SRootNode root, String snippet, int... expectPath) {
int start = editor.getRawText().indexOf(snippet);
int end = start+snippet.length();
int middle = (start+end) / 2;
SNode expectNode = getNodeAtPath(root, expectPath);
assertEquals(expectNode, root.find(start));
assertEquals(expectNode, root.find(middle));
assertEquals(expectNode, root.find(end));
}
private void assertFindStart(YamlEditor editor, SRootNode root, String snippet, int... expectPath) {
int start = editor.getRawText().indexOf(snippet);
SNode expectNode = getNodeAtPath(root, expectPath);
assertEquals(expectNode, root.find(start));
}
private void assertTreeText(YamlEditor editor, SNode node, String expected) throws Exception {
String actual = editor.textBetween(node.getStart(), node.getTreeEnd());
assertEquals(expected.trim(), actual.trim());
}
private SNode getNodeAtPath(SNode node, int... childindices) {
int i = 0;
while (i<childindices.length) {
int child = childindices[i];
node = ((SChildBearingNode)node).getChildren().get(child);
i++;
}
return node;
}
}

View File

@@ -2,7 +2,7 @@ package org.springframework.ide.vscode.cloudfoundry.manifest.editor;
import java.io.StringReader;
import org.springframework.ide.vscode.commons.reconcile.IDocument;
import org.springframework.ide.vscode.util.IDocument;
import org.springframework.ide.vscode.yaml.ast.YamlASTProvider;
import org.springframework.ide.vscode.yaml.ast.YamlFileAST;
import org.yaml.snakeyaml.Yaml;