diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java index 271b8a949..f9c4b8d88 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java @@ -193,9 +193,13 @@ public class BoshDeploymentManifestSchema implements YamlSchema { YBeanType t_stemcell = f.ybean("Stemcell"); - YType t_stemcell_name_ref = f.yenumFromDynamicValues("StemcellName", (dc) -> stemcellsProvider.getModel(dc).getStemcellNames()); - YType t_stemcell_os_ref = f.yenumFromDynamicValues("StemcellOs", (dc) -> stemcellsProvider.getModel(dc).getStemcellOss()); - YType t_stemcell_version_ref = f.yenumFromDynamicValues("StemcellVersion", (dc) -> { + YType t_stemcell_name_ref = f.yenumFromDynamicValues("StemcellName", (dc) -> + stemcellsProvider.getModel(dc).getStemcellNames() + ); + YType t_stemcell_os_ref = f.yenumFromDynamicValues("StemcellOs", (dc) -> + stemcellsProvider.getModel(dc).getStemcellOss() + ); + YAtomicType t_stemcell_version_ref = f.yenumFromDynamicValues("StemcellVersion", (dc) -> { StemcellModel currentStemcell = getCurrentStemcell(dc); if (currentStemcell!=null) { return stemcellsProvider.getModel(dc).getStemcells().stream() @@ -207,6 +211,8 @@ public class BoshDeploymentManifestSchema implements YamlSchema { //Troubles determining the filter. So return all stemcells. return stemcellsProvider.getModel(dc).getVersions(); }); + t_stemcell_version_ref.addHints("latest"); + t_stemcell_version_ref.alsoAccept("latest"); addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true); addProp(t_stemcell, "version", t_stemcell_version_ref).isRequired(true); @@ -260,7 +266,7 @@ public class BoshDeploymentManifestSchema implements YamlSchema { YBeanType t_variable = f.ybean("Variable"); addProp(t_variable, "name", t_var_name_def).isPrimary(true); YType t_variable_type = f.yenum("VariableType", "certificate", "password", "rsa", "ssh") - .parseWith(ValueParsers.NE_STRING); //Overrid the parser -> no errors / warnings... in theory there could be other valid values. + .parseWith(ValueParsers.NE_STRING); //Override the parser -> no errors / warnings... in theory there could be other valid values. addProp(t_variable, "type", t_variable_type).isRequired(true); addProp(t_variable, "options", t_params); addProp(v2Schema, "variables", f.yseq(t_variable)); diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java index 0012df26b..9d11d7f73 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java @@ -954,6 +954,22 @@ public class BoshEditorTest { ); } + @SuppressWarnings("unchecked") + @Test public void contentAssistStemcellVersionNoDirector() throws Exception { + Editor editor; + stemcellsProvider = mock(DynamicModelProvider.class); + when(stemcellsProvider.getModel(any())).thenThrow(new IOException("Couldn't connect to bosh")); + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " name: centos-agent\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "latest<*>" + ); + } + @Test public void contentAssistStemcellVersionFromDirector() throws Exception { Editor editor; stemcellsProvider = provideStemcellsFrom( @@ -970,7 +986,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); editor = harness.newEditor( @@ -980,7 +996,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>" + "123.4<*>", "222.2<*>", "latest<*>" ); editor = harness.newEditor( @@ -989,7 +1005,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>", "333.3<*>" + "123.4<*>", "222.2<*>", "333.3<*>", "latest<*>" ); //when os or name are 'bogus' at least suggest proposals based on other prop @@ -1001,7 +1017,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); editor = harness.newEditor( @@ -1012,7 +1028,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); //when the os and name disagree, merge the proposals for both: @@ -1024,10 +1040,34 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>", "333.3<*>" + "123.4<*>", "222.2<*>", "333.3<*>", "latest<*>" ); } + @SuppressWarnings("unchecked") + @Test public void reconcileStemcellVersionNoDirector() throws Exception { + Editor editor; + stemcellsProvider = mock(DynamicModelProvider.class); + when(stemcellsProvider.getModel(any())).thenThrow(new IOException("Couldn't connect to bosh")); + editor = harness.newEditor( + "stemcells:\n" + + "- alias: aaa\n" + + " name: centos-agent\n" + + " version: 222.2\n" + + "- alias: bbb\n" + + " name: centos-agent\n" + + " version: 123.4\n" + + "- alias: ddd\n" + + " os: ubuntu\n" + + " version: 333.3\n" + + "- alias: eee\n" + + " os: ubuntu\n" + + " version: latest\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems(/*NONE*/); + } + @Test public void reconcileStemcellVersionFromDirector() throws Exception { Editor editor; stemcellsProvider = provideStemcellsFrom( @@ -1047,7 +1087,10 @@ public class BoshEditorTest { " version: 123.4\n" + "- alias: ddd\n" + " os: ubuntu\n" + - " version: 333.3\n" + " version: 333.3\n" + + "- alias: eee\n" + + " os: ubuntu\n" + + " version: latest\n" ); editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); editor.assertProblems( diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index 0c4455fbb..eff8b1eb2 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.yaml.schema; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,12 +25,12 @@ import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.function.BiFunction; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReplacementQuickfix; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.EnumValueParser; +import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParser; @@ -291,19 +292,30 @@ public class YTypeFactory { return null; } - public AbstractType addHintProvider(Callable> hintProvider) { - addHintProvider((DynamicSchemaContext dc) -> hintProvider); + public AbstractType setHintProvider(Callable> hintProvider) { + setHintProvider((DynamicSchemaContext dc) -> hintProvider); return this; } - public AbstractType addHintProvider(SchemaContextAware>> hintProvider) { + public AbstractType setHintProvider(SchemaContextAware>> hintProvider) { //TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware> this.hintProvider = hintProvider; return this; } public YValueHint[] getHintValues(DynamicSchemaContext dc) throws Exception { - Collection providerHints=getProviderHints(dc); + Collection providerHints = null; + try { + providerHints=getProviderHints(dc); + } catch (Exception e) { + if (!hints.isEmpty()) { + Log.log(e); + //Recover from error returning just the static hints. + return hints.toArray(new YValueHint[hints.size()]); + } else { + throw e; + } + } if (providerHints == null || providerHints.isEmpty()) { return hints.toArray(new YValueHint[hints.size()]); @@ -324,6 +336,9 @@ public class YTypeFactory { } } + /** + * Prevents adding additional hints. + */ public void sealHints() { hints = ImmutableList.copyOf(hints); } @@ -382,23 +397,16 @@ public class YTypeFactory { addProperty(new YTypedPropertyImpl(name, type)); } public AbstractType addHints(String... strings) { - if (strings != null) { - for (String value : strings) { - BasicYValueHint hint = new BasicYValueHint(value); - if (!hints.contains(hint)) { - hints.add(hint); - } - } - } - return this; + return addHints(Arrays.stream(strings).map(s -> new BasicYValueHint(s)).toArray(BasicYValueHint[]::new)); } - public void addHints(YValueHint... extraHints) { + public AbstractType addHints(YValueHint... extraHints) { for (YValueHint h : extraHints) { if (!hints.contains(h)) { hints.add(h); } } + return this; } public void parseWith(SchemaContextAware parser) { @@ -413,6 +421,29 @@ public class YTypeFactory { return parser; } + /** + * Modifies currently installed parser so it is guaranteed to accept at least given values. + * @return + */ + public AbstractType alsoAccept(String... _values) { + if (parser!=null) { + ImmutableSet values = ImmutableSet.copyOf(_values); + final SchemaContextAware oldParserProvider = parser; + parser = (dc) -> (s) -> { + if (values.contains(s)) { + return s; + } else { + ValueParser oldParser = oldParserProvider.safeWithContext(dc).orElse(null); + if (oldParser!=null) { + return oldParser.parse(s); + } + return s; + } + }; + } + return this; + } + public AbstractType require(Constraint dynamicConstraint) { this.constraints.add(dynamicConstraint); return this; @@ -849,7 +880,7 @@ public class YTypeFactory { public YAtomicType yenumFromHints(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); - t.addHintProvider((dc) -> () -> values.withContext(dc)); + t.setHintProvider((dc) -> () -> values.withContext(dc)); t.parseWith((DynamicSchemaContext dc) -> { Collection strings = YTypeFactory.values(values.withContext(dc)); return new EnumValueParser(name, strings) { @@ -867,9 +898,7 @@ public class YTypeFactory { //Error message formatter: (parseString, validValues) -> "'"+parseString+"' is an unknown '"+name+"'. Valid values are: "+validValues, //Hints provider: - (dc) -> - hints(values.withContext(dc) - ) + (dc) -> hints(values.withContext(dc)) ); } @@ -879,7 +908,7 @@ public class YTypeFactory { public YAtomicType yenum(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); - t.addHintProvider((dc) -> { + t.setHintProvider((dc) -> { Collection strings = values.withContext(dc); return strings==null ? null 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 5a08a955e..f14a9c75e 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 @@ -118,7 +118,7 @@ public class PipelineYmlSchema implements YamlSchema { public final YType t_location = f.yatomic("Location") //Note: we could have used f.yenum here too. But it saves memory if we don't keep the large set of ValueHints in memory. // That's why we attach custom hint provider and parser here that do essentially the same thing. - .addHintProvider(() -> { + .setHintProvider(() -> { return ZoneId.getAvailableZoneIds().stream() .map(BasicYValueHint::new) .collect(Collectors.toList()); @@ -187,7 +187,7 @@ public class PipelineYmlSchema implements YamlSchema { } ); t_maybe_resource_name = f.yatomic("ResourceName | TaskOutput"); - t_maybe_resource_name.addHintProvider((DynamicSchemaContext dc) -> { + t_maybe_resource_name.setHintProvider((DynamicSchemaContext dc) -> { //Putting the Callable into a local variable is strange, but the compiler doesn't like it if // we return it directly. Too much complexity for Java type-inference? Callable> callable = () -> YTypeFactory.hints(models.getResourceNames(dc)); diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index 7c85119fa..6f81233c8 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -122,25 +122,25 @@ public final class ManifestYmlSchema implements YamlSchema { YAtomicType t_buildpack = f.yatomic("Buildpack"); if (buildpackProvider != null) { - t_buildpack.addHintProvider(buildpackProvider); + t_buildpack.setHintProvider(buildpackProvider); // t_buildpack.parseWith(ManifestYmlValueParsers.fromHints(t_buildpack.toString(), buildpackProvider)); } YAtomicType t_stack = f.yatomic("Stack"); if (stacksProvider!=null) { - t_stack.addHintProvider(stacksProvider); + t_stack.setHintProvider(stacksProvider); t_stack.parseWith(ManifestYmlValueParsers.fromValueHints(stacksProvider, t_stack, ManifestYamlSchemaProblemsTypes.UNKNOWN_STACK_PROBLEM)); } YAtomicType t_domain = f.yatomic("Domain"); if (domainsProvider != null) { - t_domain.addHintProvider(domainsProvider); + t_domain.setHintProvider(domainsProvider); t_domain.parseWith(ManifestYmlValueParsers.fromValueHints(domainsProvider, t_domain, ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM)); } YAtomicType t_service = f.yatomic("Service"); if (servicesProvider != null) { - t_service.addHintProvider(servicesProvider); + t_service.setHintProvider(servicesProvider); t_service.parseWith(new CFServicesValueParser(t_service.toString(), YTypeFactory.valuesFromHintProvider(servicesProvider))); }