diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IDocument.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IDocument.java deleted file mode 100644 index eadcc2578..000000000 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IDocument.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.springframework.ide.vscode.commons.reconcile; - -public interface IDocument { - - String get(); - -} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IReconcileEngine.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IReconcileEngine.java index 0d57ffabf..ef9257a4b 100644 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IReconcileEngine.java +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/reconcile/IReconcileEngine.java @@ -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); } diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java new file mode 100644 index 000000000..e6e0a4667 --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java @@ -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); + } + +} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java new file mode 100644 index 000000000..0cac56565 --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java @@ -0,0 +1,14 @@ +package org.springframework.ide.vscode.util; + +import java.util.Collection; + +/** + * @author Kris De Volder + */ +public class CollectionUtil { + + public static boolean hasElements(Collection c) { + return c!=null && !c.isEmpty(); + } + +} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/DocumentUtil.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/DocumentUtil.java new file mode 100644 index 000000000..7a0eeb983 --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/DocumentUtil.java @@ -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=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; + } } diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java index f9fd476c8..8b3f95d13 100644 --- a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java @@ -13,5 +13,11 @@ public class Assert { throw new IllegalStateException(); } } - + + public static void isLegal(boolean b, String msg) { + if (!b) { + throw new IllegalStateException(msg); + } + } + } diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/StringUtil.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/StringUtil.java index 52de63069..cad4ffad5 100644 --- a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/StringUtil.java +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/StringUtil.java @@ -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; + } + } diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/ast/YamlASTProvider.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/ast/YamlASTProvider.java index 32e13e2cb..43088c28d 100644 --- a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/ast/YamlASTProvider.java +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/ast/YamlASTProvider.java @@ -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 { diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/KeyAliases.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/KeyAliases.java new file mode 100644 index 000000000..5bb948d7f --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/KeyAliases.java @@ -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. + *

+ * 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 getKeyAliases(String base) { + return Collections.emptyList(); + } + + public String toString() { return "KeyAliasses.NONE"; }; + }; + + Iterable getKeyAliases(String base); + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlNavigable.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlNavigable.java new file mode 100644 index 000000000..e3be2328a --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlNavigable.java @@ -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 traverse(YamlPathSegment s) throws Exception; +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPath.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPath.java new file mode 100644 index 000000000..5cbe2aea7 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPath.java @@ -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 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 segments = new ArrayList(); + 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> 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. + *

+ * 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> path) { + List segments = new ArrayList(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 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); + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPathSegment.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPathSegment.java new file mode 100644 index 000000000..aa5ee75da --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/path/YamlPathSegment.java @@ -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); + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlReconcileEngine.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlReconcileEngine.java index 862e20146..792953ca1 100644 --- a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlReconcileEngine.java +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlReconcileEngine.java @@ -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; diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlSchemaBasedReconcileEngine.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlSchemaBasedReconcileEngine.java index 91209febf..2ec6a0887 100644 --- a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlSchemaBasedReconcileEngine.java +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/reconcile/YamlSchemaBasedReconcileEngine.java @@ -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; diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlDocument.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlDocument.java new file mode 100644 index 000000000..2b7104d84 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlDocument.java @@ -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=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<<<<)"; + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParser.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParser.java new file mode 100644 index 000000000..1256e0cbf --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParser.java @@ -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. + *

+ * 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. + *

+ * I.e something like this: + *

+ * foo:
+ *   bar:
+ *     zor: Hello
+ *       this is
+ *       tex
+ *     more-keys:
+ *       - foo
+ *       - bar
+ * 
+ *

+ * When the parser encounters something it can not identify as a 'simple-key: ' + * 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 { + 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 segments = new ArrayList(); + buildPath(this, segments); + return new YamlPath(segments); + } + + private static void buildPath(SNode node, ArrayList 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 cs = getChildren(); + if (index>=0 && index + * 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 children = null; + private Map 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 getChildren() { + if (children!=null) { + return Collections.unmodifiableList(children); + } + return Collections.emptyList(); + } + public void addChild(SNode c) { + if (children==null) { + children = new ArrayList(); + } + children.add(c); + } + public SNode getLastChild() { + List 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 children = getChildren(); + if (index 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 keyMap() throws Exception { + if (keyMap==null) { + HashMap index = new HashMap(); + 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=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 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 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 getKeyAliases(String key) { + return keyAliases.getKeyAliases(key); + } + + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureProvider.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureProvider.java new file mode 100644 index 000000000..86effd4c8 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/structure/YamlStructureProvider.java @@ -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(); + } + }; + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java new file mode 100644 index 000000000..9ac9147ed --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java @@ -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. + *

+ * 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; + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java b/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java new file mode 100644 index 000000000..9e45a2460 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java @@ -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 segments = new ArrayList(); + 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