When completing a resource type name, also insert required properties
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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:}}<*>"
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
Reference in New Issue
Block a user