When completing a resource type name, also insert required properties

This commit is contained in:
Kris De Volder
2017-04-18 17:34:26 -07:00
parent e34740b05c
commit 8af1d9f0c4
15 changed files with 365 additions and 93 deletions

View File

@@ -18,6 +18,7 @@ import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
@@ -131,6 +132,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
//TODO: cursor offset within newText? for now we assume its always at the end.
item.setTextEdit(vscodeEdit);
item.setInsertTextFormat(InsertTextFormat.Snippet);
}
}

View File

@@ -14,6 +14,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -90,6 +91,8 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl
private LanguageServerTestListener testListener;
private boolean hasCompletionSnippetSupport;
@Override
public void connect(LanguageClient _client) {
this.client = (STS4LanguageClient) _client;
@@ -102,28 +105,56 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl
return quickfixRegistry;
}
public SnippetBuilder createSnippetBuilder() {
//TODO: create a snippet builder adapted to client capabilities.
// There are 3 different cases to consider here:
// (1) the client capabilities indicates that client has snippet support
// (2) the client capabilities indicates that client has no snippet support
// (3) special case for vscode (undocumented snippet support using '{{}}' variables).
//At the moment default implementation is suited only for case (3)
return new SnippetBuilder();
}
public SimpleLanguageServer(String extensionId) {
this.EXTENSION_ID = extensionId;
}
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
// LOG.info("Initializing");
String rootPath = params.getRootPath();
if (rootPath==null) {
// LOG.warning("workspaceRoot NOT SET");
} else {
this.workspaceRoot= Paths.get(rootPath).toAbsolutePath().normalize();
// LOG.info("workspaceRoot = "+workspaceRoot);
}
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
LOG.info("Initializing");
String rootPath = params.getRootPath();
if (rootPath==null) {
LOG.warning("workspaceRoot NOT SET");
} else {
this.workspaceRoot= Paths.get(rootPath).toAbsolutePath().normalize();
this.hasCompletionSnippetSupport = safeGet(false, () -> params.getCapabilities().getTextDocument().getCompletion().getCompletionItem().getSnippetSupport());
LOG.info("workspaceRoot = "+workspaceRoot);
LOG.info("hasCompletionSnippetSupport = "+hasCompletionSnippetSupport);
}
InitializeResult result = new InitializeResult();
InitializeResult result = new InitializeResult();
ServerCapabilities cap = getServerCapabilities();
result.setCapabilities(cap);
ServerCapabilities cap = getServerCapabilities();
result.setCapabilities(cap);
return CompletableFuture.completedFuture(result);
}
return CompletableFuture.completedFuture(result);
}
/**
* Get some info safely. If there's any kind of exception, ignore it
* and retutn default value instead.
*/
private static <T> T safeGet(T deflt, Callable<T> getter) {
try {
T x = getter.call();
if (x!=null) {
return x;
}
} catch (Exception e) {
}
return deflt;
}
public void onError(String message, Throwable error) {
LanguageClient cl = this.client;

View File

@@ -0,0 +1,54 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.util;
public class SnippetBuilder {
private int nextPlaceHolderId = 1;
private StringBuilder buf = new StringBuilder();
public SnippetBuilder text(String text) {
buf.append(text);
return this;
}
/**
* Create a new `placeholder` and appends it to the snippet.
*/
public SnippetBuilder placeHolder() {
buf.append(createPlaceHolder(nextPlaceHolderId++));
return this;
}
/**
* Create a placeholder string (a 'tab stop' inside the snippet).
* <p>
* As there are different formats for placeholders, this method can
* be overridden by subclasses to support other formats.
* <p>
* The default implementation creates place holder strings that
* match the undocumented format vscode currently supports.
* <p>
* Note: this format is explicitly different from what the LSP
* specifies. So it is very likely we should change this implementation
* in the near future.
*/
protected String createPlaceHolder(int id) {
//Default implementation now only handes the undocumented snippet format that vscode supports.
return "{{"+id+":"+"}}";
}
@Override
public String toString() {
return buf.toString();
}
}

View File

@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
@@ -70,7 +71,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
@Override
public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
String query = getPrefix(doc, node, offset);
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, offset, query);
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, node, offset, query);
if (!valueCompletions.isEmpty()) {
return valueCompletions;
}
@@ -150,7 +151,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
}
}
private List<ICompletionProposal> getValueCompletions(YamlDocument doc, int offset, String query) {
private List<ICompletionProposal> getValueCompletions(YamlDocument doc, SNode node, int offset, String query) {
YValueHint[] values=null;
try {
values = typeUtil.getHintValues(type, getSchemaContext());
@@ -159,6 +160,15 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
}
if (values!=null) {
ArrayList<ICompletionProposal> completions = new ArrayList<>();
YamlIndentUtil indenter = new YamlIndentUtil(doc);
int referenceIndent;
try {
referenceIndent = getContextNode().getIndent();
} catch (Exception e) {
//Getting it from the node isn't always correct, but more often than not it is.
//So this fallback is better than nothing.
referenceIndent = node.getIndent();
}
for (YValueHint value : values) {
double score = FuzzyMatcher.matchScore(query, value.getValue());
if (score!=0 && !value.equals(query)) {
@@ -169,6 +179,10 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
edits.insert(offset, " ");
}
edits.insert(offset, value.getValue());
String extraInsertion = value.getExtraInsertion();
if (extraInsertion!=null) {
edits.insert(offset, indenter.applyIndentation(extraInsertion, referenceIndent));
}
completions.add(completionFactory().valueProposal(value.getValue(), query, value.getLabel(), type, score, edits, typeUtil));
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser
* @author Kris De Volder
*/
public interface YamlAssistContext extends YamlNavigable<YamlAssistContext> {
Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception;
Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode current, int offset) throws Exception;
//TODO: conceptually... the right thing would be to only implement the second of these
// two methods and get rid of the first one.

View File

@@ -10,10 +10,15 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.schema;
import org.springframework.ide.vscode.commons.util.Assert;
import com.google.common.base.Supplier;
public class BasicYValueHint implements YValueHint {
private final String value;
private String label;
private Supplier<String> extraInsertion = null;
public BasicYValueHint(String value, String label) {
this.value = value;
@@ -76,4 +81,16 @@ public class BasicYValueHint implements YValueHint {
public String toString() {
return "BasicYValueHint [value=" + value + ", label=" + label + "]";
}
@Override
public String getExtraInsertion() {
Supplier<String> ei = this.extraInsertion;
return ei==null ? null : ei.get();
}
public BasicYValueHint setExtraInsertion(Supplier<String> insertions) {
Assert.isLegal(this.extraInsertion==null);
this.extraInsertion = insertions;
return this;
}
}

View File

@@ -11,9 +11,14 @@
package org.springframework.ide.vscode.commons.yaml.schema;
public interface YValueHint {
String getValue();
String getLabel();
/**
* Returns an optional extra text to insert after the value, for a completion.
* If non-null value is returned, then it will be inserted after the value,
* on the next line and indented to line-up relative to the indentation of
* the line where the value itself is being inserted.
*/
String getExtraInsertion();
}

View File

@@ -14,9 +14,9 @@ package org.springframework.ide.vscode.commons.yaml.util;
import java.util.stream.Stream;
public class Streams {
/**
* Like Stream.of but returns Stream.empty of the element is null
* Like java.util.Stream.of but returns Stream.empty of the element is null
*/
public static <T> Stream<T> of(T e) {
return e==null ? Stream.empty() : Stream.of(e);

View File

@@ -24,8 +24,9 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.swing.text.BadLocationException;
@@ -45,8 +46,6 @@ import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.Assert;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.collect.ImmutableList;
@@ -516,11 +515,28 @@ public class Editor {
return it;
}
protected CompletionItem assertCompletionWithLabel(Predicate<String> expectLabel) throws Exception {
List<CompletionItem> completions = getCompletions();
Optional<CompletionItem> completion = completions.stream()
.filter((item) -> expectLabel.test(item.getLabel()))
.findFirst();
if (completion.isPresent()) {
return completion.get();
}
fail("Not found in "+ completions.stream().map(c -> c.getLabel()).collect(Collectors.toList()));
return null; //unreachable but compiler doesn't know.
}
protected CompletionItem assertCompletionWithLabel(String expectLabel) throws Exception {
return getCompletions().stream()
List<CompletionItem> completions = getCompletions();
Optional<CompletionItem> completion = completions.stream()
.filter((item) -> item.getLabel().equals(expectLabel))
.findFirst()
.get();
.findFirst();
if (completion.isPresent()) {
return completion.get();
}
fail("Not found '"+expectLabel+"' in "+ completions.stream().map(c -> c.getLabel()).collect(Collectors.toList()));
return null; //unreachable but compiler doesn't know.
}
public void assertCompletionWithLabel(String expectLabel, String expectedResult) throws Exception {
@@ -531,6 +547,13 @@ public class Editor {
setText(saveText);
}
public void assertCompletionWithLabel(Predicate<String> expectLabel, String expectedResult) throws Exception {
CompletionItem completion = assertCompletionWithLabel(expectLabel);
String saveText = getText();
apply(completion);
assertEquals(expectedResult, getText());
setText(saveText);
}
public void setSelection(int start, int end) {
Assert.assertTrue(start>=0);

View File

@@ -78,13 +78,15 @@ public class ConcourseDefinitionFinder extends SimpleDefinitionFinder<ConcourseL
TextDocument doc = server.getTextDocumentService().get(params);
if (doc!=null) {
YamlFileAST ast = models.getSafeAst(doc, false);
Node refNode = ast.findNode(doc.toOffset(params.getPosition()));
if (refNode!=null) {
YType type = astTypes.getType(ast, refNode);
if (type!=null) {
Handler handler = handlers.get(type);
if (handler!=null) {
return handler.handle(refNode, doc, ast);
if (ast!=null) {
Node refNode = ast.findNode(doc.toOffset(params.getPosition()));
if (refNode!=null) {
YType type = astTypes.getType(ast, refNode);
if (type!=null) {
Handler handler = handlers.get(type);
if (handler!=null) {
return handler.handle(refNode, doc, ast);
}
}
}
}

View File

@@ -44,7 +44,7 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT;
SimpleTextDocumentService documents = getTextDocumentService();
ConcourseModel models = new ConcourseModel(documents);
ConcourseModel models = new ConcourseModel(this);
YamlASTProvider currentAsts = models.getAstProvider(false);
private SchemaSpecificPieces forPipelines;
private SchemaSpecificPieces forTasks;

View File

@@ -15,10 +15,14 @@ import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.v
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocumentContentChange;
import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
@@ -29,7 +33,11 @@ import org.springframework.ide.vscode.commons.yaml.path.ASTRootCursor;
import org.springframework.ide.vscode.commons.yaml.path.NodeCursor;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty;
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
import org.springframework.ide.vscode.concourse.util.CollectorUtil;
import org.springframework.ide.vscode.concourse.util.StaleFallbackCache;
@@ -38,6 +46,7 @@ import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableMultiset.Builder;
import com.google.common.collect.Multiset;
@@ -121,9 +130,14 @@ public class ConcourseModel {
private final ASTTypeCache astTypes = new ASTTypeCache();
public ConcourseModel(SimpleTextDocumentService documents) {
private ResourceTypeRegistry resourceTypes;
private final Supplier<SnippetBuilder> snippetBuilderFactory;
public ConcourseModel(SimpleLanguageServer languageServer) {
Yaml yaml = new Yaml();
this.parser = new YamlParser(yaml);
this.snippetBuilderFactory = languageServer::createSnippetBuilder;
}
/**
@@ -135,8 +149,8 @@ public class ConcourseModel {
* names (e.g. because there hasn't been a successful parse yet and current document contents
* can not be parsed).
*/
public Multiset<String> getResourceNames(IDocument doc) {
return getStringsFromAst(doc, RESOURCE_NAMES_PATH);
public Multiset<String> getResourceNames(DynamicSchemaContext dc) {
return getStringsFromAst(dc.getDocument(), RESOURCE_NAMES_PATH);
}
/**
@@ -180,8 +194,8 @@ public class ConcourseModel {
* names (e.g. because there hasn't been a successful parse yet and current document contents
* can not be parsed).
*/
public Multiset<String> getJobNames(IDocument doc) {
return getStringsFromAst(doc, JOB_NAMES_PATH);
public Multiset<String> getJobNames(DynamicSchemaContext dc) {
return getStringsFromAst(dc.getDocument(), JOB_NAMES_PATH);
}
private Multiset<String> getStringsFromAst(IDocument doc, YamlPath path) {
@@ -194,25 +208,61 @@ public class ConcourseModel {
});
}
public Multiset<String> getResourceTypeNames(IDocument doc) {
Collection<YValueHint> hints = getResourceTypeNameHints(doc);
public Multiset<String> getResourceTypeNames(DynamicSchemaContext dc) {
Collection<YValueHint> hints = getResourceTypeNameHints(dc);
if (hints!=null) {
return ImmutableMultiset.copyOf(YTypeFactory.values(hints));
}
return null;
}
public Collection<YValueHint> getResourceTypeNameHints(IDocument doc) {
public Collection<YValueHint> getResourceTypeNameHints(DynamicSchemaContext dc) {
IDocument doc = dc.getDocument();
Multiset<String> userDefined = getStringsFromAst(doc, RESOURCE_TYPE_NAMES_PATH);
if (userDefined!=null) {
Builder<YValueHint> builder = ImmutableMultiset.builder();
builder.addAll(YTypeFactory.hints(userDefined));
builder.addAll(Arrays.asList(PipelineYmlSchema.BUILT_IN_RESOURCE_TYPES));
builder.addAll(
Arrays.stream(PipelineYmlSchema.BUILT_IN_RESOURCE_TYPES)
.map(h -> addExtraInsertion(h, dc))
.collect(Collectors.toList())
);
return builder.build();
}
return null;
}
public Node getParentPropertyNode(String propName, DynamicSchemaContext dc) {
YamlPath path = dc.getPath();
if (path!=null) {
YamlFileAST root = this.getSafeAst(dc.getDocument());
if (root!=null) {
return path.dropLast().append(YamlPathSegment.valueAt(propName)).traverseToNode(root);
}
}
return null;
}
private YValueHint addExtraInsertion(YValueHint h, DynamicSchemaContext dc) {
return new BasicYValueHint(h.getValue(), h.getLabel()).setExtraInsertion(() -> {
String resourceTypeName = h.getValue();
AbstractType sourceType = (AbstractType) resourceTypes.getSourceType(resourceTypeName);
if (sourceType!=null && getParentPropertyNode("source", dc)==null) { //don't auto insert what's already there!
List<YTypedProperty> requiredProps = sourceType.getProperties().stream().filter(p -> p.isRequired()).collect(Collectors.toList());
if (!requiredProps.isEmpty()) {
SnippetBuilder snippet = snippetBuilderFactory.get();
snippet.text("\nsource:");
for (YTypedProperty p : requiredProps) {
snippet.text("\n "+p.getName()+": ");
snippet.placeHolder();
}
return snippet.toString();
}
}
return null;
});
}
private <T> T getFromAst(IDocument doc, Function<YamlFileAST, T> astFunction) {
try {
@@ -270,4 +320,8 @@ public class ConcourseModel {
return new StepModel(stepType, stepNode);
}
public void setResourceTypeRegistry(ResourceTypeRegistry resourceTypes) {
this.resourceTypes = resourceTypes;
}
}

View File

@@ -17,6 +17,7 @@ import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.ValueParseException;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware;
import com.google.common.collect.Multiset;
@@ -53,14 +54,14 @@ public class ConcourseValueParsers {
}
public static SchemaContextAware<ValueParser> acceptOnlyUniqueNames(
Function<IDocument, Multiset<String>> getDefinedNameCounts,
Function<DynamicSchemaContext, Multiset<String>> getDefinedNameCounts,
String typeName
) {
return acceptOnlyUniqueNames(getDefinedNameCounts, typeName, false);
}
public static SchemaContextAware<ValueParser> acceptOnlyUniqueNames(
Function<IDocument, Multiset<String>> getDefinedNameCounts,
Function<DynamicSchemaContext, Multiset<String>> getDefinedNameCounts,
String typeName,
boolean allowEmptyName
) {
@@ -69,7 +70,7 @@ public class ConcourseValueParsers {
if (!allowEmptyName && !StringUtil.hasText(input)) {
throw new ValueParseException("'"+typeName +"' should not be blank");
}
Multiset<String> resourceNames = getDefinedNameCounts.apply(dc.getDocument());
Multiset<String> resourceNames = getDefinedNameCounts.apply(dc);
if (resourceNames.count(input)<=1) {
//okay
return input;

View File

@@ -51,8 +51,6 @@ import org.yaml.snakeyaml.nodes.Node;
import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Flux;
/**
* @author Kris De Volder
*/
@@ -79,12 +77,6 @@ public class PipelineYmlSchema implements YamlSchema {
hint("vagrant-cloud", "The vagrant-cloud resource can fetch and publish Vagrant boxes to Atlas.")
};
public static final Set<String> BUILT_IN_RESOURCE_TYPE_NAMES = Flux.fromArray(PipelineYmlSchema.BUILT_IN_RESOURCE_TYPES)
.map(YValueHint::getValue)
.collect(Collectors.toSet())
.block();
private final AbstractType TOPLEVEL_TYPE;
private final YTypeUtil TYPE_UTIL;
@@ -163,6 +155,7 @@ public class PipelineYmlSchema implements YamlSchema {
public PipelineYmlSchema(ConcourseModel models) {
this.models = models;
models.setResourceTypeRegistry(resourceTypes);
TYPE_UTIL = f.TYPE_UTIL;
// define schema types
@@ -176,7 +169,7 @@ public class PipelineYmlSchema implements YamlSchema {
return "The '"+parseString+"' Resource Type does not exist. Existing types: "+validValues;
},
(DynamicSchemaContext dc) -> {
return models.getResourceTypeNameHints(dc.getDocument());
return models.getResourceTypeNameHints(dc);
}
);
@@ -185,7 +178,7 @@ public class PipelineYmlSchema implements YamlSchema {
return "The '"+parseString+"' resource does not exist. Existing resources: "+validValues;
},
(DynamicSchemaContext dc) -> {
return (models.getResourceNames(dc.getDocument()));
return (models.getResourceNames(dc));
}
);
@@ -194,7 +187,7 @@ public class PipelineYmlSchema implements YamlSchema {
return "The '"+parseString+"' Job does not exist. Existing jobs: "+validValues;
},
(DynamicSchemaContext dc) -> {
return models.getJobNames(dc.getDocument());
return models.getJobNames(dc);
}
);
@@ -250,7 +243,7 @@ public class PipelineYmlSchema implements YamlSchema {
task.require((dc) -> {
String languageId = dc.getDocument().getLanguageId();
if (LanguageIds.CONCOURSE_PIPELINE.equals(languageId)) {
Node parentImageDef = getParentPropertyNode("image", models, dc);
Node parentImageDef = models.getParentPropertyNode("image", dc);
if (parentImageDef==null) {
return Constraints.requireOneOf("image_resource", "image");
} else {
@@ -266,7 +259,7 @@ public class PipelineYmlSchema implements YamlSchema {
});
AbstractType t_put_get_name = f.contextAware("Name", (dc) -> {
if (getParentPropertyNode("resource", models, dc)!=null) {
if (models.getParentPropertyNode("resource", dc)!=null) {
return null;
} else {
return t_resource_name;
@@ -612,9 +605,9 @@ public class PipelineYmlSchema implements YamlSchema {
}
private Node getResourceNameNode(String resourceNameProp, DynamicSchemaContext dc) {
Node resourceName = getParentPropertyNode("resource", models, dc);
Node resourceName = models.getParentPropertyNode("resource", dc);
if (resourceName==null) {
resourceName = getParentPropertyNode(resourceNameProp, models, dc);
resourceName = models.getParentPropertyNode(resourceNameProp, dc);
}
return resourceName;
}
@@ -637,7 +630,7 @@ public class PipelineYmlSchema implements YamlSchema {
}
private String getParentPropertyValue(String propName, ConcourseModel models, DynamicSchemaContext dc) {
return NodeUtil.asScalar(getParentPropertyNode(propName, models, dc));
return NodeUtil.asScalar(models.getParentPropertyNode(propName, dc));
}
private String getSiblingPropertyValue(DynamicSchemaContext dc, String propName) {
@@ -651,17 +644,6 @@ public class PipelineYmlSchema implements YamlSchema {
return null;
}
private Node getParentPropertyNode(String propName, ConcourseModel models, DynamicSchemaContext dc) {
YamlPath path = dc.getPath();
if (path!=null) {
YamlFileAST root = models.getSafeAst(dc.getDocument());
if (root!=null) {
return path.dropLast().append(YamlPathSegment.valueAt(propName)).traverseToNode(root);
}
}
return null;
}
private YTypedPropertyImpl prop(AbstractType beanType, String name, YType type) {
YTypedPropertyImpl prop = f.yprop(name, type);
prop.setDescriptionProvider(descriptionFor(beanType, name));

View File

@@ -10,7 +10,7 @@
*******************************************************************************/
package org.springframework.ide.vscode.concourse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains;
import java.io.InputStream;
@@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.ide.vscode.commons.languageserver.LanguageIds;
import org.springframework.ide.vscode.commons.util.IOUtil;
@@ -473,10 +474,11 @@ public class ConcourseEditorTest {
@Test
public void valueCompletions() throws Exception {
String [] builtInResourceTypes = {
"git", "hg", "time", "s3",
"archive", "semver", "github-release",
"docker-image", "tracker", "pool", "cf", "bosh-io-release",
"bosh-io-stemcell", "bosh-deployment", "vagrant-cloud"
"git", "hg", "time", "s3", "archive",
"semver", "github-release", "docker-image", "tracker",
"pool", "cf",
"bosh-io-release", "bosh-io-stemcell", "bosh-deployment",
"vagrant-cloud"
};
Arrays.sort(builtInResourceTypes);
@@ -484,12 +486,14 @@ public class ConcourseEditorTest {
for (int i = 0; i < expectedCompletions.length; i++) {
expectedCompletions[i] =
"resources:\n" +
"- type: "+builtInResourceTypes[i]+"<*>";
"- type: "+builtInResourceTypes[i]+"<*>\n" +
" source:";
}
assertCompletions(
"resources:\n" +
"- type: <*>"
"- type: <*>\n" +
" source:"
, //=>
expectedCompletions
);
@@ -2469,7 +2473,8 @@ public class ConcourseEditorTest {
" repository: cfcommunity/slack-notification-resource\n" +
" tag: latest\n" +
"resources:\n" +
"- type: <*>";
"- type: <*>\n" +
" source:\n";
//All the good names are accepted:
String[] expectedCompletions = new String[goodNames.length];
@@ -2940,14 +2945,14 @@ public class ConcourseEditorTest {
editor.assertCompletionLabels(
//completions for current (i.e Job) context:
"build_logs_to_retain",
"disable_manual_trigger",
"max_in_flight",
"serial",
"serial_groups",
"name",
"plan",
"public",
//Completions for nested context (i.e. task step)
"disable_manual_trigger",
"max_in_flight",
"serial",
"serial_groups",
"name",
"plan",
"public",
//Completions for nested context (i.e. task step)
"➔ attempts",
"➔ config",
"➔ ensure",
@@ -2962,7 +2967,7 @@ public class ConcourseEditorTest {
"➔ tags",
"➔ task",
"➔ timeout"
);
);
}
@Test public void gotoSymbolInPipeline() throws Exception {
@@ -3011,6 +3016,88 @@ public class ConcourseEditorTest {
editor.assertProblems("garbage|Expecting a 'Map'");
}
@Test public void noAutoInsertRequiredSourcePropertiesIfPresent() throws Exception {
Editor editor;
//Most common case
editor = harness.newEditor(
"resources:\n" +
"- name: source-repo\n" +
" type: <*>\n"+
" source:"
);
editor.assertCompletionWithLabel((l) -> l.startsWith("pool"),
"resources:\n" +
"- name: source-repo\n" +
" type: pool<*>\n" +
" source:"
);
}
@Test public void autoInsertRequiredSourceProperties() throws Exception {
Editor editor;
//Most common case
editor = harness.newEditor(
"resources:\n" +
"- name: source-repo\n" +
" type: <*>"
);
editor.assertCompletionWithLabel((l) -> l.startsWith("pool"),
"resources:\n" +
"- name: source-repo\n" +
" type: pool\n" +
" source:\n" +
" uri: {{1:}}\n" +
" branch: {{2:}}\n" +
" pool: {{3:}}<*>"
);
// What if we use somewhat different indentation style?
editor = harness.newEditor(
"resources:\n" +
" - name: source-repo\n" +
" type: <*>"
);
editor.assertCompletionWithLabel((l) -> l.startsWith("pool"),
"resources:\n" +
" - name: source-repo\n" +
" type: pool\n" +
" source:\n" +
" uri: {{1:}}\n" +
" branch: {{2:}}\n" +
" pool: {{3:}}<*>"
);
}
@Ignore
@Test public void autoInsertRequiredSourceProperties3() throws Exception {
//This case can not be implemented correctly because of the magic indentations that vscode
// automatically applies. The magic indents will allways indent the extra lines we insert after
// the value to be indented to the level of that value. So it is impossible to create an edit
// where the text on the lines following it is indented *less* than that value, which is what
// is required to implement this case correctly.
//What if the type was on a new line (this is odd, but anyhow)
Editor editor = harness.newEditor(
"resources:\n" +
"- name: source-repo\n" +
" type: \n" +
" <*>"
);
System.out.println(editor.getText());
editor.assertCompletionWithLabel((l) -> l.startsWith("pool"),
"resources:\n" +
"- name: source-repo\n" +
" type: \n" +
" pool\n" +
" source:\n" +
" uri: {{1:}}\n" +
" branch: {{2:}}\n" +
" pool: {{3:}}<*>"
);
}
//////////////////////////////////////////////////////////////////////////////