Port YamlStructureParser and its tests to yaml-commons
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
package org.springframework.ide.vscode.commons.reconcile;
|
||||
|
||||
public interface IDocument {
|
||||
|
||||
String get();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,11 @@ public class Assert {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void isLegal(boolean b, String msg) {
|
||||
if (!b) {
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<<<<)";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user