From 8af1d9f0c48cf2feb787742a814bffb2615d8555 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 18 Apr 2017 17:34:26 -0700 Subject: [PATCH] When completing a resource type name, also insert required properties --- .../VscodeCompletionEngineAdapter.java | 2 + .../util/SimpleLanguageServer.java | 61 ++++++--- .../languageserver/util/SnippetBuilder.java | 54 ++++++++ .../yaml/completion/YTypeAssistContext.java | 18 ++- .../yaml/completion/YamlAssistContext.java | 2 +- .../commons/yaml/schema/BasicYValueHint.java | 17 +++ .../commons/yaml/schema/YValueHint.java | 9 +- .../ide/vscode/commons/yaml/util/Streams.java | 4 +- .../languageserver/testharness/Editor.java | 35 ++++- .../concourse/ConcourseDefinitionFinder.java | 16 ++- .../concourse/ConcourseLanguageServer.java | 2 +- .../ide/vscode/concourse/ConcourseModel.java | 74 +++++++++-- .../concourse/ConcourseValueParsers.java | 7 +- .../vscode/concourse/PipelineYmlSchema.java | 36 ++---- .../vscode/concourse/ConcourseEditorTest.java | 121 +++++++++++++++--- 15 files changed, 365 insertions(+), 93 deletions(-) create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java index 06421f683..1abfc0640 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java @@ -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); } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index a96222064..1b7bb075b 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -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 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 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 safeGet(T deflt, Callable 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; diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java new file mode 100644 index 000000000..9c1cdf935 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java @@ -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). + *

+ * As there are different formats for placeholders, this method can + * be overridden by subclasses to support other formats. + *

+ * The default implementation creates place holder strings that + * match the undocumented format vscode currently supports. + *

+ * 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(); + } + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index faa6b75b4..6178a67fe 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -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 getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { String query = getPrefix(doc, node, offset); - List valueCompletions = getValueCompletions(doc, offset, query); + List valueCompletions = getValueCompletions(doc, node, offset, query); if (!valueCompletions.isEmpty()) { return valueCompletions; } @@ -150,7 +151,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { } } - private List getValueCompletions(YamlDocument doc, int offset, String query) { + private List 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 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)); } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java index 9a4360c84..68e306059 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java @@ -24,7 +24,7 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser * @author Kris De Volder */ public interface YamlAssistContext extends YamlNavigable { - Collection getCompletions(YamlDocument doc, SNode node, int offset) throws Exception; + Collection 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. diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/BasicYValueHint.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/BasicYValueHint.java index af70c3f91..2b06d0e98 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/BasicYValueHint.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/BasicYValueHint.java @@ -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 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 ei = this.extraInsertion; + return ei==null ? null : ei.get(); + } + + public BasicYValueHint setExtraInsertion(Supplier insertions) { + Assert.isLegal(this.extraInsertion==null); + this.extraInsertion = insertions; + return this; + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YValueHint.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YValueHint.java index fb69503d7..d0cf87420 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YValueHint.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YValueHint.java @@ -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(); } \ No newline at end of file diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/Streams.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/Streams.java index 81c16eb48..4c5f5f00d 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/Streams.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/Streams.java @@ -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 Stream of(T e) { return e==null ? Stream.empty() : Stream.of(e); diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index cb7f057b4..1082bd106 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -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 expectLabel) throws Exception { + List completions = getCompletions(); + Optional 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 completions = getCompletions(); + Optional 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 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); diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java index 5fd600986..e32131fdf 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java @@ -78,13 +78,15 @@ public class ConcourseDefinitionFinder extends SimpleDefinitionFinder 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 getResourceNames(IDocument doc) { - return getStringsFromAst(doc, RESOURCE_NAMES_PATH); + public Multiset 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 getJobNames(IDocument doc) { - return getStringsFromAst(doc, JOB_NAMES_PATH); + public Multiset getJobNames(DynamicSchemaContext dc) { + return getStringsFromAst(dc.getDocument(), JOB_NAMES_PATH); } private Multiset getStringsFromAst(IDocument doc, YamlPath path) { @@ -194,25 +208,61 @@ public class ConcourseModel { }); } - public Multiset getResourceTypeNames(IDocument doc) { - Collection hints = getResourceTypeNameHints(doc); + public Multiset getResourceTypeNames(DynamicSchemaContext dc) { + Collection hints = getResourceTypeNameHints(dc); if (hints!=null) { return ImmutableMultiset.copyOf(YTypeFactory.values(hints)); } return null; } - public Collection getResourceTypeNameHints(IDocument doc) { + public Collection getResourceTypeNameHints(DynamicSchemaContext dc) { + IDocument doc = dc.getDocument(); Multiset userDefined = getStringsFromAst(doc, RESOURCE_TYPE_NAMES_PATH); if (userDefined!=null) { Builder 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 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 getFromAst(IDocument doc, Function astFunction) { try { @@ -270,4 +320,8 @@ public class ConcourseModel { return new StepModel(stepType, stepNode); } + public void setResourceTypeRegistry(ResourceTypeRegistry resourceTypes) { + this.resourceTypes = resourceTypes; + } + } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java index f7885ff8c..00cba6348 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java @@ -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 acceptOnlyUniqueNames( - Function> getDefinedNameCounts, + Function> getDefinedNameCounts, String typeName ) { return acceptOnlyUniqueNames(getDefinedNameCounts, typeName, false); } public static SchemaContextAware acceptOnlyUniqueNames( - Function> getDefinedNameCounts, + Function> 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 resourceNames = getDefinedNameCounts.apply(dc.getDocument()); + Multiset resourceNames = getDefinedNameCounts.apply(dc); if (resourceNames.count(input)<=1) { //okay return input; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index dbe15c375..f580316c9 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -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 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)); diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index e1fd5553e..620890907 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -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:}}<*>" + ); + } //////////////////////////////////////////////////////////////////////////////