diff --git a/atom-extensions/atom-manifest-yaml/lib/main.js b/atom-extensions/atom-manifest-yaml/lib/main.js index 560cf6b09..24f88d4ca 100644 --- a/atom-extensions/atom-manifest-yaml/lib/main.js +++ b/atom-extensions/atom-manifest-yaml/lib/main.js @@ -5,7 +5,7 @@ class ManifestYamlLanguageClient extends JarLanguageClient { constructor() { super( - 'https://s3-us-west-1.amazonaws.com/s3-test.spring.io/sts4/fatjars/snapshots/manifest-yaml-language-server-0.0.9-201707201812.jar', + 'https://s3-us-west-1.amazonaws.com/s3-test.spring.io/sts4/fatjars/snapshots/manifest-yaml-language-server-0.0.9-201707270057.jar', path.join(__dirname, '..', 'server') ); diff --git a/atom-extensions/atom-manifest-yaml/package.json b/atom-extensions/atom-manifest-yaml/package.json index f7ca7ab48..a3624b053 100644 --- a/atom-extensions/atom-manifest-yaml/package.json +++ b/atom-extensions/atom-manifest-yaml/package.json @@ -9,7 +9,6 @@ "atom": ">=1.17.0" }, "dependencies": { - "atom-languageclient": "0.1.1", "decompress": "^4.2.0", "portfinder": "^1.0.13", "remote-file-size": "^3.0.3", diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java index f6b7d3379..8771d2dd5 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java @@ -52,8 +52,8 @@ public class BootJavaHoverProvider implements HoverHandler { @Override public CompletableFuture handle(TextDocumentPositionParams params) { SimpleTextDocumentService documents = server.getTextDocumentService(); - TextDocument doc = documents.get(params).copy(); - if (doc != null) { + if (documents.get(params) != null) { + TextDocument doc = documents.get(params).copy(); try { int offset = doc.toOffset(params.getPosition()); CompletableFuture hoverResult = provideHover(doc, offset); @@ -64,7 +64,7 @@ public class BootJavaHoverProvider implements HoverHandler { catch (Exception e) { } } - + return SimpleTextDocumentService.NO_HOVER; } @@ -77,7 +77,7 @@ public class BootJavaHoverProvider implements HoverHandler { parser.setStatementsRecovery(true); parser.setBindingsRecovery(true); parser.setResolveBindings(true); - + String[] classpathEntries = getClasspathEntries(document); String[] sourceEntries = new String[] {}; parser.setEnvironment(classpathEntries, sourceEntries, null, true); @@ -89,7 +89,7 @@ public class BootJavaHoverProvider implements HoverHandler { CompilationUnit cu = (CompilationUnit) parser.createAST(null); ASTNode node = NodeFinder.perform(cu, offset, 0); - + if (node != null) { System.out.println("AST node found: " + node.getClass().getName()); return provideHoverForAnnotation(node, offset, document); @@ -101,11 +101,11 @@ public class BootJavaHoverProvider implements HoverHandler { private CompletableFuture provideHoverForAnnotation(ASTNode node, int offset, TextDocument doc) { Annotation annotation = null; ASTNode exactNode = node; - + while (node != null && !(node instanceof Annotation)) { node = node.getParent(); } - + if (node != null) { annotation = (Annotation) node; ITypeBinding type = annotation.resolveTypeBinding(); @@ -116,7 +116,7 @@ public class BootJavaHoverProvider implements HoverHandler { } } } - + return null; } @@ -124,7 +124,7 @@ public class BootJavaHoverProvider implements HoverHandler { if (type.getQualifiedName().equals(SPRING_VALUE)) { return new ValueHoverProvider().provideHoverForValueAnnotation(node, annotation, type, offset, doc); } - + return null; } @@ -134,7 +134,7 @@ public class BootJavaHoverProvider implements HoverHandler { Stream classpathEntries = classpath.getClasspathEntries(); return classpathEntries .filter(path -> path.toFile().exists()) - .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); + .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshCliConfig.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshCliConfig.java new file mode 100644 index 000000000..65f183ff2 --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshCliConfig.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.bosh; + +import java.time.Duration; + +import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.Log; + +/** + * Provides access to configuration options that allow user to + * change the way bosh CLI commands are executed by the + * bosh language server. + * + * @author Kris De Volder + */ +public class BoshCliConfig { + + /** + * The settings object. This is obtained from 'didChangeConfiguration' events. + */ + private Settings settings = new Settings(null); + + public String getCommand() { + return (String) settings.getProperty("bosh", "cli", "command"); + } + public String getTarget() { + return (String) settings.getProperty("bosh", "cli", "target"); + } + public Duration getTimeout() { + Integer seconds = (Integer) settings.getProperty("cli", "timeout"); + return seconds == null ? Duration.ofSeconds(3) : Duration.ofSeconds(seconds); + } + + public void handleConfigurationChange(Settings newConfig) { + Log.info("BoshCliConfig changed: "+newConfig); + this.settings = newConfig; + } +} 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 3ba0128fe..c1aeec5e1 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 @@ -20,22 +20,26 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.ide.vscode.bosh.models.CachingModelProvider; import org.springframework.ide.vscode.bosh.models.CloudConfigModel; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; +import org.springframework.ide.vscode.bosh.models.ReleaseData; +import org.springframework.ide.vscode.bosh.models.ReleasesModel; import org.springframework.ide.vscode.bosh.models.StemcellData; import org.springframework.ide.vscode.bosh.models.StemcellModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.CollectorUtil; +import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.ValueParsers; +import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; -import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; @@ -96,24 +100,27 @@ public class BoshDeploymentManifestSchema implements YamlSchema { private final ASTTypeCache astTypes; private DynamicModelProvider cloudConfigProvider; private DynamicModelProvider stemcellsProvider; + private DynamicModelProvider releasesProvider; private List> defAndRefTypes; public BoshDeploymentManifestSchema( YamlAstCache asts, ASTTypeCache astTypes, DynamicModelProvider cloudConfigProvider, - DynamicModelProvider stemcellsProvider + DynamicModelProvider stemcellsProvider, + DynamicModelProvider releasesProvider ) { this.asts = asts; this.astTypes = astTypes; - this.cloudConfigProvider = new CachingModelProvider<>(cloudConfigProvider); - this.stemcellsProvider = new CachingModelProvider<>(stemcellsProvider); + this.cloudConfigProvider = new CachingModelProvider<>(cloudConfigProvider, CloudConfigModel.class); + this.stemcellsProvider = new CachingModelProvider<>(stemcellsProvider, StemcellsModel.class); + this.releasesProvider = new CachingModelProvider<>(releasesProvider, ReleasesModel.class); TYPE_UTIL = f.TYPE_UTIL; V2_TOPLEVEL_TYPE = createV2Schema(); V1_TOPLEVEL_TYPE = createV1Schema(V2_TOPLEVEL_TYPE); TOPLEVEL_TYPE = f.contextAware("DeploymenManifestV1orV2", (dc) -> { - boolean looksLikeV1 = dc.getDefinedProperties().stream().anyMatch(DEPRECATED_V1_PROPS::contains); + boolean looksLikeV1 = dc.getDefinedProperties().contains("networks"); return looksLikeV1 ? V1_TOPLEVEL_TYPE : V2_TOPLEVEL_TYPE; }); } @@ -146,10 +153,12 @@ public class BoshDeploymentManifestSchema implements YamlSchema { t_stemcell_alias_def = f.yatomic("StemcellAlias") .parseWith(ValueParsers.NE_STRING); - t_stemcell_alias_ref = f.yenumFromDynamicValues("StemcellAlias", (dc) -> astTypes.getDefinedNames(dc, t_stemcell_alias_def)); - t_release_name_def = f.yatomic("ReleaseName") - .parseWith(ValueParsers.NE_STRING); - t_release_name_ref = f.yenumFromDynamicValues("ReleaseName", (dc) -> astTypes.getDefinedNames(dc, t_release_name_def)); + t_stemcell_alias_ref = f.yenumFromDynamicValues("StemcellAlias", (dc) -> PartialCollection.compute(() -> astTypes.getDefinedNames(dc, t_stemcell_alias_def))); + t_release_name_def = f.yenumFromDynamicValues("ReleaseName", (dc) -> { + PartialCollection releaseNames = PartialCollection.compute(() -> releasesProvider.getModel(dc).getReleaseNames()); + return StringUtil.hasText(getCurrentEntityProperty(dc, "url")) ? releaseNames.addUncertainty() : releaseNames; + }); + t_release_name_ref = f.yenumFromDynamicValues("ReleaseName", (dc) -> PartialCollection.compute(() -> astTypes.getDefinedNames(dc, t_release_name_def))); t_var_name_def = f.yatomic("VariableName") .parseWith(ValueParsers.NE_STRING); @@ -161,12 +170,16 @@ public class BoshDeploymentManifestSchema implements YamlSchema { YAtomicType t_url = f.yatomic("URL"); t_url.parseWith(BoshValueParsers.url("http", "https", "file")); - YAtomicType t_network_name = f.yenumFromDynamicValues("NetworkName", (dc) -> cloudConfigProvider.getModel(dc).getNetworkNames()); - YAtomicType t_disk_type = f.yenumFromDynamicValues("DiskType", (dc) -> cloudConfigProvider.getModel(dc).getDiskTypes()); - YAtomicType t_vm_extension = f.yenumFromDynamicValues("VMExtension", (dc) -> cloudConfigProvider.getModel(dc).getVMExtensions()); - YAtomicType t_vm_type = f.yenumFromDynamicValues("VMType", (dc) -> cloudConfigProvider.getModel(dc).getVMTypes()); - - YAtomicType t_az = f.yenumFromDynamicValues("AvailabilityZone", (dc) -> cloudConfigProvider.getModel(dc).getAvailabilityZones()); + YAtomicType t_network_name = f.yenumFromDynamicValues("NetworkName", + (dc) -> PartialCollection.compute(() -> cloudConfigProvider.getModel(dc).getNetworkNames())); + YAtomicType t_disk_type = f.yenumFromDynamicValues("DiskType", + (dc) -> PartialCollection.compute(() -> cloudConfigProvider.getModel(dc).getDiskTypes())); + YAtomicType t_vm_extension = f.yenumFromDynamicValues("VMExtension", + (dc) -> PartialCollection.compute(() -> cloudConfigProvider.getModel(dc).getVMExtensions())); + YAtomicType t_vm_type = f.yenumFromDynamicValues("VMType", + (dc) -> PartialCollection.compute(() -> cloudConfigProvider.getModel(dc).getVMTypes())); + YAtomicType t_az = f.yenumFromDynamicValues("AvailabilityZone", + (dc) -> PartialCollection.compute(() -> cloudConfigProvider.getModel(dc).getAvailabilityZones())); YBeanType t_network = f.ybean("Network"); addProp(t_network, "name", t_network_name).isRequired(true); @@ -177,59 +190,66 @@ public class BoshDeploymentManifestSchema implements YamlSchema { addProp(t_instance_group_env, "bosh", t_params); addProp(t_instance_group_env, "password", t_ne_string); - YAtomicType t_version = f.yatomic("Version"); - t_version.addHints("latest"); - t_version.parseWith(ValueParsers.NE_STRING); + YType t_release_version = f.yenumFromDynamicValues("ReleaseVersion", + //message formatter: + dc -> (s, values) -> { + String name = getCurrentEntityProperty(dc, "name"); + if (StringUtil.hasText(name)) { + return "'"+s+"' is an unknown 'ReleaseVersion[name="+name+"]'. Valid values are: "+values; + } else { + return "'"+s+"' is an unknown 'ReleaseVersion'. Valid values are: "+values; + } + }, + //value provider: + dc -> { + PartialCollection releases = PartialCollection.compute(() -> releasesProvider.getModel(dc).getReleases()); + if (StringUtil.hasText(getCurrentEntityProperty(dc, "url"))) { + releases = releases.addUncertainty(); + } else { + String name = getCurrentEntityProperty(dc, "name"); + if (StringUtil.hasText(name)) { + releases = releases.map(r -> name.equals(r.getName()) ? r : null); + } + } + return releases.map(r -> r.getVersion()).add("latest"); + } + ); YBeanType t_release = f.ybean("Release"); addProp(t_release, "name", t_release_name_def).isPrimary(true); - addProp(t_release, "version", t_version); - //TODO: the checking here is just 'my best guess'. Unclarity remains: - // See: https://github.com/cloudfoundry/docs-bosh/issues/330 + addProp(t_release, "version", t_release_version).isRequired(true); addProp(t_release, "url", t_url); addProp(t_release, "sha1", t_ne_string); - addProp(v2Schema, "releases", f.yseq(t_release)).isRequired(true); - t_release.require(Constraints.requireAtLeastOneOf("url", "version")); - // ^^^^^^^ allthough docs seem to imply you shouldn't - // define both url and version it seems that bosh tolerates it. t_release.require(BoshConstraints.SHA1_REQUIRED_FOR_HTTP_URL); + addProp(v2Schema, "releases", f.yseq(t_release)).isRequired(true); YBeanType t_stemcell = f.ybean("Stemcell"); YType t_stemcell_name_ref = f.yenumFromDynamicValues("StemcellName", (dc) -> - stemcellsProvider.getModel(dc).getStemcellNames() + PartialCollection.compute(() -> stemcellsProvider.getModel(dc).getStemcellNames()) ); YType t_stemcell_os_ref = f.yenumFromDynamicValues("StemcellOs", (dc) -> - stemcellsProvider.getModel(dc).getStemcellOss() + PartialCollection.compute(() -> stemcellsProvider.getModel(dc).getStemcellOss()) ); - YType t_stemcell_version_ref = f.contextAware("StemcellVersion", new SchemaContextAware() { - - YAtomicType baseType = f.yenumFromDynamicValues("StemcellVersion", (dc) -> stemcellsProvider.getModel(dc).getVersions()); - { - baseType.addHints("latest"); - baseType.alsoAccept("latest"); - } - - @Override - public YType withContext(DynamicSchemaContext dc) throws Exception { - StemcellModel currentStemcell = getCurrentStemcell(dc); - if (StringUtil.hasText(currentStemcell.getName())||StringUtil.hasText(currentStemcell.getOs())) { - Predicate filter = currentStemcell.createVersionFilter(); - YAtomicType filteredType = f.yenumFromDynamicValues("StemcellVersion["+filter+"]", (_dc) -> { - //Note: it doesn't really matter whether we use _dc or dc in code below as they should be the same. - return stemcellsProvider.getModel(dc).getStemcells().stream() - .filter(sc -> StringUtil.hasText(sc.getVersion())) - .filter(currentStemcell.createVersionFilter()) - .map(sc -> sc.getVersion()) - .collect(CollectorUtil.toImmutableSet()); - }); - filteredType.addHints("latest"); - filteredType.alsoAccept("latest"); - return filteredType; + YType t_stemcell_version_ref = f.yenumFromDynamicValues("StemcellVersion", + (dc) -> (parseString, validValues) -> { + try { + Predicate filter = getCurrentStemcell(dc).createVersionFilter(); + if (filter!=StemcellModel.ALLWAYS_TRUE_FILTER) { + return "'"+parseString+"' is an unknown 'StemcellVersion["+filter+"]'. Valid values are: "+validValues; + } + } catch (Exception e) { + //ignore (parse error most likely) } - return baseType; + return "'"+parseString+"' is an unknown 'StemcellVersion'. Valid values are: "+validValues; + }, + (dc) -> { + Predicate filter = getCurrentStemcell(dc).createVersionFilter(); + return PartialCollection.compute(() -> stemcellsProvider.getModel(dc).getStemcells()) + .map(sc -> filter.test(sc) ? sc.getVersion() : null) + .add("latest"); } - }).treatAsAtomic(); + ); addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true); addProp(t_stemcell, "version", t_stemcell_version_ref).isRequired(true); @@ -306,6 +326,15 @@ public class BoshDeploymentManifestSchema implements YamlSchema { return new StemcellModel(path.dropLast().traverseToNode(ast)); } + private String getCurrentEntityProperty(DynamicSchemaContext dc, String propName) { + YamlPath path = dc.getPath(); + YamlFileAST ast = asts.getSafeAst(dc.getDocument(), true); + if (ast!=null) { + return NodeUtil.asScalar(path.dropLast().thenValAt(propName).traverseToNode(ast)); + } + return null; + } + @Override public YType getTopLevelType() { return TOPLEVEL_TYPE; diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java index 9f92d41b6..b53c9e078 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java @@ -12,14 +12,18 @@ package org.springframework.ide.vscode.bosh; import org.springframework.ide.vscode.bosh.models.CloudConfigModel; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; +import org.springframework.ide.vscode.bosh.models.ReleasesModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider; import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngine; import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; +import org.springframework.ide.vscode.commons.languageserver.util.Settings; 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.SimpleWorkspaceService; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; @@ -37,17 +41,23 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvid public class BoshLanguageServer extends SimpleLanguageServer { private final VscodeCompletionEngineAdapter completionEngine; + private BoshDeploymentManifestSchema schema; - public BoshLanguageServer(DynamicModelProvider cloudConfigProvider, DynamicModelProvider stemcellsProvider) { + public BoshLanguageServer(BoshCliConfig cliConfig, + DynamicModelProvider cloudConfigProvider, + DynamicModelProvider stemcellsProvider, + DynamicModelProvider releasesProvider + ) { super("vscode-bosh"); YamlAstCache asts = new YamlAstCache(); SimpleTextDocumentService documents = getTextDocumentService(); ASTTypeCache astTypeCache = new ASTTypeCache(); - BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(asts, astTypeCache, cloudConfigProvider, stemcellsProvider); + schema = new BoshDeploymentManifestSchema(asts, astTypeCache, cloudConfigProvider, stemcellsProvider, releasesProvider); YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); + enableSnippets(true); YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider, YamlCompletionEngineOptions.DEFAULT); completionEngine = createCompletionEngineAdapter(this, yamlCompletionEngine); HoverInfoProvider infoProvider = new YamlHoverInfoProvider(asts.getAstProvider(true), structureProvider, contextProvider); @@ -64,6 +74,11 @@ public class BoshLanguageServer extends SimpleLanguageServer { documents.onCompletionResolve(completionEngine::resolveCompletion); documents.onHover(hoverEngine ::getHover); documents.onDefinition(new BoshDefintionFinder(this, schema, asts, astTypeCache)); + + SimpleWorkspaceService workspace = getWorkspaceService(); + workspace.onDidChangeConfiguraton((Settings settings) -> { + cliConfig.handleConfigurationChange(settings); + }); } private void validateOnDocumentChange(IReconcileEngine engine, TextDocument doc) { @@ -74,6 +89,14 @@ public class BoshLanguageServer extends SimpleLanguageServer { } } + public void enableSnippets(boolean enable) { + if (enable) { + schema.f.setSnippetProvider(new SchemaBasedSnippetGenerator(schema.getTypeUtil(), this::createSnippetBuilder)); + } else { + schema.f.setSnippetProvider(null); + } + } + public BoshLanguageServer setMaxCompletions(int maxCompletions) { completionEngine.setMaxCompletions(maxCompletions); return this; diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java index 911376510..3c2ff1c9c 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java @@ -13,14 +13,18 @@ package org.springframework.ide.vscode.bosh; import java.io.IOException; import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider; +import org.springframework.ide.vscode.bosh.models.BoshCommandReleasesProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandStemcellsProvider; import org.springframework.ide.vscode.commons.languageserver.LaunguageServerApp; public class Main { public static void main(String[] args) throws IOException, InterruptedException { + BoshCliConfig cliConfig = new BoshCliConfig(); LaunguageServerApp.start(() -> new BoshLanguageServer( - new BoshCommandCloudConfigProvider(), - new BoshCommandStemcellsProvider() + cliConfig, + new BoshCommandCloudConfigProvider(cliConfig), + new BoshCommandStemcellsProvider(cliConfig), + new BoshCommandReleasesProvider(cliConfig) )); } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandBasedModelProvider.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandBasedModelProvider.java index d6227aaa9..1e321de26 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandBasedModelProvider.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandBasedModelProvider.java @@ -11,16 +11,24 @@ package org.springframework.ide.vscode.bosh.models; import java.io.File; -import java.time.Duration; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import org.springframework.ide.vscode.bosh.BoshCliConfig; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.ExternalProcess; import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; import org.springframework.ide.vscode.commons.yaml.ast.YamlParser; +import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.representer.Representer; @@ -37,9 +45,10 @@ public abstract class BoshCommandBasedModelProvider implements DynamicModelPr private final YamlParser yamlParser; protected final ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - protected Duration CMD_TIMEOUT = Duration.ofSeconds(3); + private final BoshCliConfig config; - protected BoshCommandBasedModelProvider() { + protected BoshCommandBasedModelProvider(BoshCliConfig config) { + this.config = config; Representer representer = new Representer(); representer.getPropertyUtils().setSkipMissingProperties(true); yamlParser = new YamlParser(new Yaml()); @@ -69,6 +78,24 @@ public abstract class BoshCommandBasedModelProvider implements DynamicModelPr return blocks[0]; } + protected final ExternalCommand getCommand() { + List commandAndArgs = new ArrayList<>(); + String command = config.getCommand(); + if (command==null) { + return null; + } + commandAndArgs.add(command); + String target = config.getTarget(); + if (target!=null) { + commandAndArgs.add("-e"); + commandAndArgs.add(target); + } + for (String s : getBoshCommand()) { + commandAndArgs.add(s); + } + return new ExternalCommand(commandAndArgs.toArray(new String[commandAndArgs.size()])); + } + protected JsonNode getJsonTree() throws Exception { String out = executeCommand(getCommand()); return mapper.readTree(out); @@ -76,8 +103,11 @@ public abstract class BoshCommandBasedModelProvider implements DynamicModelPr protected String executeCommand(ExternalCommand command) throws Exception { Log.info("executing cmd: "+command); + if (command==null) { + throw new IOException("bosh cli based editor features are disabled"); + } try { - ExternalProcess process = new ExternalProcess(getWorkingDir(), command, true, CMD_TIMEOUT); + ExternalProcess process = new ExternalProcess(getWorkingDir(), command, true, config.getTimeout()); Log.info("executing cmd SUCCESS: "+process); String out = process.getOut(); return out; @@ -91,7 +121,7 @@ public abstract class BoshCommandBasedModelProvider implements DynamicModelPr return new File(".").getAbsoluteFile(); } - protected abstract ExternalCommand getCommand(); + protected abstract String[] getBoshCommand(); protected YamlFileAST parseYaml(String block) throws Exception { TextDocument doc = new TextDocument(null, LanguageId.BOSH_CLOUD_CONFIG); @@ -99,4 +129,17 @@ public abstract class BoshCommandBasedModelProvider implements DynamicModelPr YamlFileAST ast = yamlParser.getAST(doc); return ast; } + + protected Collection getNames(JSONCursor _cursor, YamlTraversal path) { + return path.traverseAmbiguously(_cursor) + .flatMap((cursor) -> { + String text = cursor.target.asText(); + if (StringUtil.hasText(text)) { + return Stream.of(text); + } else { + return Stream.empty(); + } + }) + .collect(CollectorUtil.toImmutableSet()); + } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProvider.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProvider.java index 095188251..047a6b99c 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProvider.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProvider.java @@ -14,6 +14,7 @@ import java.time.Duration; import java.util.Collection; import java.util.stream.Stream; +import org.springframework.ide.vscode.bosh.BoshCliConfig; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.StringUtil; @@ -31,7 +32,8 @@ import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; */ public class BoshCommandCloudConfigProvider extends BoshCommandBasedModelProvider { - public BoshCommandCloudConfigProvider() { + public BoshCommandCloudConfigProvider(BoshCliConfig config) { + super(config); } private static final YamlTraversal VM_TYPE_NAMES = YamlPath.EMPTY @@ -103,18 +105,9 @@ public class BoshCommandCloudConfigProvider extends BoshCommandBasedModelProvide }; } - /** - * Configure how long we wait for the command to fetch cloud config before - * raising timeout exception. (The command may block for long amounts of time - * of the director is unreachable on the network). - */ - public void setCommandTimeout(Duration duration) { - this.CMD_TIMEOUT = duration; - } - @Override - protected ExternalCommand getCommand() { - return new ExternalCommand("bosh", "cloud-config", "--json"); + protected String[] getBoshCommand() { + return new String[] {"cloud-config", "--json"}; } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProvider.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProvider.java new file mode 100644 index 000000000..97a8a7a24 --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProvider.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * 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.bosh.models; + +import java.util.Collection; +import java.util.List; + +import org.springframework.ide.vscode.bosh.BoshCliConfig; +import org.springframework.ide.vscode.commons.util.CollectorUtil; +import org.springframework.ide.vscode.commons.yaml.path.YamlPath; +import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal; +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; + +public class BoshCommandReleasesProvider extends BoshCommandBasedModelProvider { + + private static final String[] COMMAND = new String[] { "releases", "--json" }; + + private static final YamlTraversal RELEASES = YamlPath.EMPTY + .thenValAt("Tables") + .thenAnyChild() + .thenValAt("Rows") + .thenAnyChild(); + + private static final YamlTraversal RELEASE_NAMES = RELEASES + .thenValAt("name"); + + private static final YamlTraversal RELEASE_VERSIONS = RELEASES + .thenValAt("version"); + + public BoshCommandReleasesProvider(BoshCliConfig config) { + super(config); + } + + @Override + public ReleasesModel getModel(DynamicSchemaContext dc) throws Exception { + JSONCursor cursor = new JSONCursor(getJsonTree()); + return new ReleasesModel() { + @Override + public List getReleases() { + return RELEASES.traverseAmbiguously(cursor) + .map(c -> new ReleaseData( + getStringProperty(c, "name"), + getStringProperty(c, "version") + )) + .collect(CollectorUtil.toImmutableList()); + } + + private String getStringProperty(JSONCursor c, String prop) { + c = YamlPath.EMPTY.thenValAt(prop).traverse(c); + if (c!=null) { + return c.target.asText(); + } + return null; + } + + @Override + public Collection getReleaseNames() { + return getNames(cursor, RELEASE_NAMES); + } + + @Override + public Collection getVersions() { + return getNames(cursor, RELEASE_VERSIONS); + } + }; + } + + @Override + protected String[] getBoshCommand() { + return COMMAND; + } + +} \ No newline at end of file diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProvider.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProvider.java index 3d7cf95a1..c0c337842 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProvider.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProvider.java @@ -11,11 +11,10 @@ package org.springframework.ide.vscode.bosh.models; import java.util.Collection; -import java.util.stream.Stream; +import org.springframework.ide.vscode.bosh.BoshCliConfig; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.ExternalCommand; -import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; @@ -37,6 +36,10 @@ public class BoshCommandStemcellsProvider extends BoshCommandBasedModelProvider< private static final YamlTraversal STEMCELL_VERSIONS = STEMCELLS .thenValAt("version"); + public BoshCommandStemcellsProvider(BoshCliConfig config) { + super(config); + } + @Override public StemcellsModel getModel(DynamicSchemaContext dc) throws Exception { JSONCursor cursor = new JSONCursor(getJsonTree()); @@ -44,20 +47,7 @@ public class BoshCommandStemcellsProvider extends BoshCommandBasedModelProvider< @Override public Collection getStemcellNames() { - return getNames(STEMCELL_NAMES); - } - - private Collection getNames(YamlTraversal path) { - return path.traverseAmbiguously(cursor) - .flatMap((cursor) -> { - String text = cursor.target.asText(); - if (StringUtil.hasText(text)) { - return Stream.of(text); - } else { - return Stream.empty(); - } - }) - .collect(CollectorUtil.toImmutableSet()); + return getNames(cursor, STEMCELL_NAMES); } @Override @@ -81,19 +71,19 @@ public class BoshCommandStemcellsProvider extends BoshCommandBasedModelProvider< @Override public Collection getStemcellOss() { - return getNames(STEMCELL_OSS); + return getNames(cursor, STEMCELL_OSS); } @Override public Collection getVersions() { - return getNames(STEMCELL_VERSIONS); + return getNames(cursor, STEMCELL_VERSIONS); } }; } @Override - protected ExternalCommand getCommand() { - return new ExternalCommand("bosh", "stemcells", "--json"); + protected String[] getBoshCommand() { + return new String[] { "stemcells", "--json" }; } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/CachingModelProvider.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/CachingModelProvider.java index 964d93814..c5679b43f 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/CachingModelProvider.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/CachingModelProvider.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.bosh.models; +import java.lang.reflect.Proxy; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -35,9 +36,11 @@ public class CachingModelProvider implements DynamicModelProvider { private Cache> cache = createCache(); private final DynamicModelProvider delegate; + private Class modelInterface; - public CachingModelProvider(DynamicModelProvider delegate) { + public CachingModelProvider(DynamicModelProvider delegate, Class modelInterface) { this.delegate = delegate; + this.modelInterface = modelInterface; } /** @@ -72,15 +75,43 @@ public class CachingModelProvider implements DynamicModelProvider { synchronized (this) { cached = cache.get(key, () -> { try { - return CompletableFuture.completedFuture(delegate.getModel(dc)); + return CompletableFuture.completedFuture(wrapWithCachingProxy(delegate.getModel(dc))); } catch (Throwable e) { - CompletableFuture failed = new CompletableFuture<>(); - failed.completeExceptionally(e); - return failed; + return failed(e); } }); } return cached.get(); } + private static CompletableFuture failed(Throwable e) { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(e); + return failed; + } + + @SuppressWarnings("unchecked") + private T wrapWithCachingProxy(T model) { + if (model==null) { + //Special case for 'no model' we'll create a model that always returns null + return (T) Proxy.newProxyInstance(modelInterface.getClassLoader(), new Class[] {modelInterface}, (o, m, a) -> { + return null; + }); + } + Cache> attributesCache = CacheBuilder.newBuilder().build(); + return (T) Proxy.newProxyInstance(modelInterface.getClassLoader(), new Class[] {modelInterface}, (o, m, a) -> { + //We only support caching results for methods that have no arguments (for now, its all we need). + if (m.getParameterTypes().length==0) { + return attributesCache.get(m.getName(), () -> { + try { + return CompletableFuture.completedFuture((T)m.invoke(model, a)); + } catch (Throwable e) { + return failed(e); + } + }).get(); + } + return m.invoke(model, a); + }); + } + } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleaseData.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleaseData.java new file mode 100644 index 000000000..eea48b654 --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleaseData.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * 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.bosh.models; + +public class ReleaseData { + + private String name; + private String version; + + public ReleaseData(String name, String version) { + super(); + this.name = name; + this.version = version; + } + + public ReleaseData() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ReleaseData other = (ReleaseData) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + return true; + } + + @Override + public String toString() { + return "ReleaseData [name=" + name + ", version=" + version + "]"; + } + + +} diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleasesModel.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleasesModel.java new file mode 100644 index 000000000..244b396c5 --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/ReleasesModel.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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.bosh.models; + +import java.util.Collection; +import java.util.List; + +public interface ReleasesModel { + List getReleases(); + Collection getReleaseNames(); + Collection getVersions(); +} diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java index 47aad85f1..9d3266dfd 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java @@ -21,6 +21,16 @@ import org.yaml.snakeyaml.nodes.Node; */ public class StemcellModel { + public static final Predicate ALLWAYS_TRUE_FILTER = new Predicate() { + @Override + public boolean test(StemcellData sc) { + return true; + } + @Override + public String toString() { + return "true"; + } + }; private Node node; public StemcellModel(Node node) { @@ -79,16 +89,7 @@ public class StemcellModel { } }; } else { - return new Predicate() { - @Override - public boolean test(StemcellData sc) { - return true; - } - @Override - public String toString() { - return "true"; - } - }; + return ALLWAYS_TRUE_FILTER; } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java new file mode 100644 index 000000000..746326fc7 --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.bosh.snippets; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.util.CollectorUtil; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; +import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; +import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; + +import com.google.common.base.Supplier; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * An implementation of {@link TypeBasedSnippetProvider} that generates snippets + * automatically from schema types. + */ +public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { + + private YTypeUtil typeUtil; + private Supplier snippetBuilderFactory; + + public SchemaBasedSnippetGenerator(YTypeUtil typeUtil, Supplier snippetBuilderFactory) { + super(); + this.typeUtil = typeUtil; + this.snippetBuilderFactory = snippetBuilderFactory; + } + + private Cache> cache = CacheBuilder.newBuilder().build(); + private int maxNesting = Integer.MAX_VALUE; + + @Override + public Collection getSnippets(YType type) { + try { + return cache.get(type, () -> generateSnippets(type)); + } catch (ExecutionException e) { + Log.log(e); + return ImmutableList.of(); + } + } + + private Collection generateSnippets(YType type) { + ImmutableList.Builder snippets = ImmutableList.builder(); + //Generate a 'full' snippet that defines all required properties of the current type. + Snippet snippet = generateFullSnippet(type, 0); + if (snippet!=null) { + snippets.add(snippet); + } + //Generate single property snippets that only define a single properties (with 'mega snippets' for nested types) + for (YTypedProperty p : typeUtil.getProperties(type)) { + String propName = p.getName(); + SnippetBuilder builder = snippetBuilderFactory.get(); + generateBeanSnippet(ImmutableList.of(p), builder, 0, maxNesting); + if (builder.getPlaceholderCount()>=2) { + snippets.add(new Snippet(p.getName()+" Snippet", builder.toString(), (dc) -> + !dc.getDefinedProperties().contains(propName) + )); + } + } + return snippets.build(); + } + + private Snippet generateFullSnippet(YType type, int indent) { + if (typeUtil.isBean(type)) { + SnippetBuilder builder = snippetBuilderFactory.get(); + List requiredProps = typeUtil.getProperties(type).stream() + .filter(p -> p.isPrimary() || p.isRequired()) + .collect(CollectorUtil.toImmutableList()); + if (!requiredProps.isEmpty()) { + generateBeanSnippet(requiredProps, builder, indent, maxNesting); + } + if (builder.getPlaceholderCount()>=2) { + return new Snippet(typeUtil.niceTypeName(type)+" Snippet", builder.toString(), (dc) -> + requiredProps.stream().noneMatch(p -> dc.getDefinedProperties().contains(p.getName())) + ); + } + } + return null; + } + + private void generateBeanSnippet(List props, SnippetBuilder builder, int indent, int nestingLimit) { + if (nestingLimit>0) { + boolean first = true; + for (YTypedProperty p : props) { + if (!first) { + builder.newline(indent); + } + builder.text(p.getName()); + builder.text(":"); + generateNestedSnippet(false, p.getType(), builder, indent, nestingLimit-1); + first = false; + } + } else { + //reached the limit of bean number of nested property expansions allowed. + builder.placeHolder(); + } + } + + + private void generateNestedSnippet(boolean parentIsSeq, YType type, SnippetBuilder builder, int indent, int nestingLimit) { + if (type==null) { + //Assume its some kind of pojo bean + builder.newline(indent+YamlIndentUtil.INDENT_BY); + builder.placeHolder(); + } else if (typeUtil.isBean(type) || typeUtil.isMap(type)) { + if (!parentIsSeq) { + //ready to enter nested keys on next line + indent += YamlIndentUtil.INDENT_BY; + builder.newline(indent); + } + //Insert required keys + List requiredProps = typeUtil.getProperties(type).stream() + .filter(p -> p.isPrimary() || p.isRequired()) + .collect(Collectors.toList()); + generateBeanSnippet(requiredProps, builder, indent, nestingLimit); + } else if (typeUtil.isSequencable(type)) { + //ready to enter sequence element on next line + builder.newline(indent); + builder.text("- "); + indent += YamlIndentUtil.INDENT_BY; + generateNestedSnippet(true, typeUtil.getDomainType(type), builder, indent, nestingLimit); + } else { //Treat like atomic + //ready to enter whatever on the same line + builder.text(" "); + builder.placeHolder(); + } + } + +} diff --git a/headless-services/bosh-language-server/src/main/resources/desc/Release/sha1.md b/headless-services/bosh-language-server/src/main/resources/desc/Release/sha1.md index fb04fe343..6db8dcd1b 100644 --- a/headless-services/bosh-language-server/src/main/resources/desc/Release/sha1.md +++ b/headless-services/bosh-language-server/src/main/resources/desc/Release/sha1.md @@ -1 +1 @@ -The SHA1 of the release tarball. SHA1 is only required when using HTTP(s) URLs. \ No newline at end of file +The SHA1 of the release tarball. SHA1 is recommended when using HTTP(s) URLs. \ No newline at end of file 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 15ceb599a..ac8c1e902 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 @@ -11,50 +11,55 @@ package org.springframework.ide.vscode.bosh; import static org.junit.Assert.assertEquals; -import static org.springframework.ide.vscode.languageserver.testharness.Editor.PLAIN_COMPLETION; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.*; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; -import static org.mockito.Mockito.*; import java.io.IOException; -import java.util.Collection; +import java.util.List; import java.util.concurrent.TimeoutException; -import javax.management.modelmbean.ModelMBeanAttributeInfo; - import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; -import org.mockito.Mockito; import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; +import org.springframework.ide.vscode.bosh.models.BoshCommandReleasesProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandStemcellsProvider; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; +import org.springframework.ide.vscode.bosh.models.ReleaseData; +import org.springframework.ide.vscode.bosh.models.ReleasesModel; import org.springframework.ide.vscode.bosh.models.StemcellData; import org.springframework.ide.vscode.bosh.models.StemcellsModel; import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; -import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; public class BoshEditorTest { - LanguageServerHarness harness; + LanguageServerHarness harness; - private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider(); - private DynamicModelProvider stemcellsProvider = Mockito.mock(DynamicModelProvider.class); + private BoshCliConfig cliConfig = new BoshCliConfig(); + private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider(cliConfig); + private DynamicModelProvider stemcellsProvider = mock(DynamicModelProvider.class); + private DynamicModelProvider releasesProvider = mock(DynamicModelProvider.class); @Before public void setup() throws Exception { - harness = new LanguageServerHarness(() -> { - return new BoshLanguageServer(cloudConfigProvider, (dc) -> stemcellsProvider.getModel(dc)) - .setMaxCompletions(100); + harness = new LanguageServerHarness(() -> { + return new BoshLanguageServer(cliConfig, cloudConfigProvider, + (dc) -> stemcellsProvider.getModel(dc), + (dc) -> releasesProvider.getModel(dc) + ) + .setMaxCompletions(100); }, LanguageId.BOSH_DEPLOYMENT ); @@ -152,6 +157,7 @@ public class BoshEditorTest { } @Test public void toplevelPropertyCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "<*>" ); @@ -196,6 +202,7 @@ public class BoshEditorTest { } @Test public void stemcellCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "stemcells:\n" + "- <*>" @@ -249,6 +256,7 @@ public class BoshEditorTest { @Test public void releasesBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "releases:\n" + "- <*>" @@ -285,12 +293,15 @@ public class BoshEditorTest { "- name: some-release\n" + " url: https://my.releases.com/funky.tar.gz\n" + "- name: other-relase\n" + + " version: other-version\n" + " url: file:///root/releases/a-nice-file.tar.gz\n" + "- name: bad-url\n" + + " version: more-version\n" + " url: proto://something.com\n" + "#x" ); editor.assertProblems( + "^-^ name: some-release|'version' is required", "url|'sha1' is recommended when the 'url' is http(s)", "proto|Url scheme must be one of [http, https, file]", "x|are required" @@ -328,6 +339,7 @@ public class BoshEditorTest { } @Test public void instanceGroupsCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "instance_groups:\n" + "- <*>" @@ -511,6 +523,7 @@ public class BoshEditorTest { } @Test public void updateBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "update:\n" + " <*>" @@ -550,6 +563,7 @@ public class BoshEditorTest { } @Test public void variablesBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "variables:\n" + "- <*>" @@ -954,6 +968,25 @@ public class BoshEditorTest { ); } + @Test public void contentAssistStemcellNameNoDirector() 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: <*>\n" + ); + List completions = editor.getCompletions(); + assertEquals(1, completions.size()); + CompletionItem c = completions.get(0); + c = harness.resolveCompletionItem(c); + assertContains("Couldn't connect to bosh", c.getDocumentation()); + System.out.println("label = " + c.getLabel()); + System.out.println("detail = " + c.getDetail()); + System.out.println("doc = " + c.getDocumentation()); + } + @SuppressWarnings("unchecked") @Test public void contentAssistStemcellVersionNoDirector() throws Exception { Editor editor; @@ -1094,13 +1127,182 @@ public class BoshEditorTest { ); editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); editor.assertProblems( - "123.4|unknown 'StemcellVersion[name=centos-agent]'. Valid values are: [222.2, 333.3]", - "333.3|unknown 'StemcellVersion[os=ubuntu]'. Valid values are: [123.4, 222.2]" + "123.4|unknown 'StemcellVersion[name=centos-agent]'. Valid values are: [222.2, 333.3, latest]", + "333.3|unknown 'StemcellVersion[os=ubuntu]'. Valid values are: [123.4, 222.2, latest]" ); } private DynamicModelProvider provideStemcellsFrom(StemcellData... stemcellData) { - return new BoshCommandStemcellsProvider() { + return new BoshCommandStemcellsProvider(cliConfig) { + @Override + protected String executeCommand(ExternalCommand command) throws Exception { + String rows = mapper.writeValueAsString(stemcellData); + return "{\n" + + " \"Tables\": [\n" + + " {\n" + + " \"Rows\": " + rows + + " }\n" + + " ]\n" + + "}"; + } + }; + } + + @Test public void contentAssistReleaseNameDef() throws Exception { + releasesProvider = provideReleasesFrom( + new ReleaseData("foo", "123.4"), + new ReleaseData("foo", "222.2"), + new ReleaseData("bar", "222.2"), + new ReleaseData("bar", "333.3") + ); + + Editor editor = harness.newEditor( + "releases:\n" + + "- name: <*>" + ); + editor.assertContextualCompletions("<*>", + "bar<*>", + "foo<*>" + ); + } + + @Test public void reconcileReleaseNameDef() throws Exception { + releasesProvider = provideReleasesFrom( + new ReleaseData("foo", "123.4"), + new ReleaseData("foo", "222.2"), + new ReleaseData("bar", "222.2"), + new ReleaseData("bar", "333.3") + ); + + Editor editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + "- name: bar\n" + + "- name: bogus\n" + + "- name: url-makes-this-ok\n" + + " url: file://blah" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems("bogus|unknown 'ReleaseName'. Valid values are: [foo, bar]"); + } + + @Test public void contentAssistReleaseVersion() throws Exception { + releasesProvider = provideReleasesFrom( + new ReleaseData("foo", "123.4"), + new ReleaseData("foo", "222.2"), + new ReleaseData("bar", "222.2"), + new ReleaseData("bar", "333.3") + ); + + Editor editor = harness.newEditor( + "releases:\n" + + "- version: <*>" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", + "222.2<*>", + "333.3<*>", + "latest<*>" + ); + + editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + " version: <*>" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", + "222.2<*>", + "latest<*>" + ); + + //Still get all suggestions even when 'url' property is added + editor = harness.newEditor( + "releases:\n" + + "- version: <*>\n" + + " url: blah" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", + "222.2<*>", + "333.3<*>", + "latest<*>" + ); + + editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + " url: blah\n" + + " version: <*>" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", + "222.2<*>", + "333.3<*>", + "latest<*>" + ); + + } + + @Test public void reconcileReleaseVersion() throws Exception { + Editor editor; + releasesProvider = provideReleasesFrom( + new ReleaseData("foo", "123.4"), + new ReleaseData("foo", "222.2"), + new ReleaseData("bar", "222.2"), + new ReleaseData("bar", "333.3") + ); + + editor = harness.newEditor( + "releases:\n" + + "- version: bogus\n" + + "- version: url-makes-this-possibly-correct\n" + + " url: file:///relesease-folder/blah-release.tar.gz\n"+ + "- name: bar\n" + + " version: url-makes-this-also-possibly-correct\n" + + " url: file:///relesease-folder/other-release.tar.gz" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems("bogus|unknown 'ReleaseVersion'. Valid values are: [123.4, 222.2, 333.3, latest]"); + + editor = harness.newEditor( + "releases:\n" + + "- version: 123.4\n" + + "- version: latest\n" + + "- version: bogus\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems("bogus|unknown 'ReleaseVersion'. Valid values are: [123.4, 222.2, 333.3, latest]"); + + editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + " version: 123.4\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems(/*NONE*/); + + editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + " version: 222.2\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems(/*NONE*/); + + editor = harness.newEditor( + "releases:\n" + + "- name: foo\n" + + " version: 333.3\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems("333.3|unknown 'ReleaseVersion[name=foo]'. Valid values are: [123.4, 222.2, latest]"); + + } + + + private DynamicModelProvider provideReleasesFrom(ReleaseData... stemcellData) { + return new BoshCommandReleasesProvider(cliConfig) { @Override protected String executeCommand(ExternalCommand command) throws Exception { String rows = mapper.writeValueAsString(stemcellData); @@ -1391,4 +1593,176 @@ public class BoshEditorTest { ); } + @Test public void bug_149769913() throws Exception { + Editor editor = harness.newEditor( + "releases:\n" + + "- name: learn-bosh\n" + + " url: file:///blah\n" + + " version:\n" + + "- name: blah-blah\n" + + " version:" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + + editor.assertProblems( + "version:^^|cannot be blank", + "version:^^|cannot be blank" + ); + } + + @Test public void snippet_toplevel() throws Exception { + Editor editor = harness.newEditor("<*>"); + editor.assertCompletions(SNIPPET_COMPLETION, + "name: $1\n" + + "releases:\n" + + "- name: $2\n" + + " version: $3\n" + + "stemcells:\n" + + "- alias: $4\n" + + " version: $5\n" + + "update:\n" + + " canaries: $6\n" + + " max_in_flight: $7\n" + + " canary_watch_time: $8\n" + + " update_watch_time: $9\n" + + "instance_groups:\n" + + "- name: $10\n" + + " azs:\n" + + " - $11\n" + + " instances: $12\n" + + " jobs:\n" + + " - name: $13\n" + + " release: $14\n" + + " vm_type: $15\n" + + " stemcell: $16\n" + + " networks:\n" + + " - name: $17<*>" + , // ------------------ + "instance_groups:\n" + + "- name: $1\n" + + " azs:\n" + + " - $2\n" + + " instances: $3\n" + + " jobs:\n" + + " - name: $4\n" + + " release: $5\n" + + " vm_type: $6\n" + + " stemcell: $7\n" + + " networks:\n" + + " - name: $8<*>" + , // ---------------- + "releases:\n" + + "- name: $1\n" + + " version: $2<*>" + , // ---------------- + "stemcells:\n" + + "- alias: $1\n" + + " version: $2<*>" + , // ---------------- + "update:\n" + + " canaries: $1\n" + + " max_in_flight: $2\n" + + " canary_watch_time: $3\n" + + " update_watch_time: $4<*>" + , // ----------------- + "variables:\n" + + "- name: $1\n" + + " type: $2<*>" + ); + } + + @Test public void snippet_disabledWhenPropertiesAlreadyDefined() throws Exception { + Editor editor = harness.newEditor( + "name:\n" + + "releases:\n" + + "stemcells:\n" + + "<*>" + ); + + editor.assertCompletionLabels(SNIPPET_COMPLETION, + //"BoshDeploymentManifest Snippet", + "instance_groups Snippet", +// "releases Snippet", +// "stemcells Snippet" + "update Snippet", + "variables Snippet", + "- Stemcell Snippet" + ); + } + + @Test public void snippet_nested_plain() throws Exception { + Editor editor; + //Plain exact completion + editor = harness.newEditor( + "instance_groups:\n" + + "- name: blah\n" + + " <*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("jobs Snippet"), + "jo<*>" + , // ------ + "jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + editor.assertCompletionWithLabel("jobs Snippet", + "instance_groups:\n" + + "- name: blah\n" + + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + } + + @Test public void snippet_nested_indenting() throws Exception { + Editor editor; + //With extra indent: + editor = harness.newEditor( + "instance_groups:\n" + + "- name: blah\n" + + "<*>" + ); + editor.assertCompletionWithLabel("→ jobs Snippet", + "instance_groups:\n" + + "- name: blah\n" + + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs Snippet"), + "jo<*>" + , // ------ + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + } + + @Test public void relaxedCAmoreSpaces() throws Exception { + Editor editor = harness.newEditor( + "name: foo\n" + + "instance_groups:\n" + + "- name: \n" + + "<*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs"), + "jo<*>" + , // ==> + " jobs:\n" + + " - <*>" + ); + } + + @Test @Ignore public void keyCompletionThatNeedANewline() throws Exception { + Editor editor = harness.newEditor( + "name: foo\n" + + "update: canwa<*>" + ); + editor.assertCompletions( + "name: foo\n" + + "update: \n" + + " canary_watch_time: <*>" + ); + } + } diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshLanguageServerTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshLanguageServerTest.java index c30d1195f..4be96d03b 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshLanguageServerTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshLanguageServerTest.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.bosh; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import java.io.File; import java.net.URISyntaxException; @@ -19,21 +20,23 @@ import java.nio.file.Paths; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.junit.Test; -import org.mockito.Mockito; import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; +@SuppressWarnings("unchecked") public class BoshLanguageServerTest { public static File getTestResource(String name) throws URISyntaxException { return Paths.get(BoshLanguageServerTest.class.getResource(name).toURI()).toFile(); } + private BoshCliConfig cliConfig = new BoshCliConfig(); + @Test public void createAndInitializeServerWithWorkspace() throws Exception { LanguageServerHarness harness = new LanguageServerHarness(() -> - new BoshLanguageServer(new MockCloudConfigProvider(), Mockito.mock(DynamicModelProvider.class)) + new BoshLanguageServer(cliConfig, new MockCloudConfigProvider(cliConfig), mock(DynamicModelProvider.class), mock(DynamicModelProvider.class)) ); File workspaceRoot = getTestResource("/workspace/"); assertExpectedInitResult(harness.intialize(workspaceRoot)); @@ -43,7 +46,7 @@ public class BoshLanguageServerTest { public void createAndInitializeServerWithoutWorkspace() throws Exception { File workspaceRoot = null; LanguageServerHarness harness = new LanguageServerHarness(() -> - new BoshLanguageServer(new MockCloudConfigProvider(), Mockito.mock(DynamicModelProvider.class)) + new BoshLanguageServer(cliConfig, new MockCloudConfigProvider(cliConfig), mock(DynamicModelProvider.class), mock(DynamicModelProvider.class)) ); assertExpectedInitResult(harness.intialize(workspaceRoot)); } diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java new file mode 100644 index 000000000..b743995ec --- /dev/null +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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.bosh; + +import org.junit.Test; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; +import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; +import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; + +public class SchemaBasedSnippetGeneratorTest { + + private ASTTypeCache astTypes = new ASTTypeCache(); + private YamlAstCache asts = new YamlAstCache(); + private BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(asts, astTypes, (dc) -> null, (dc) -> null, (dc) -> null); + private YTypeUtil typeUtil = schema.getTypeUtil(); + private SchemaBasedSnippetGenerator generator = new SchemaBasedSnippetGenerator(typeUtil, SnippetBuilder::new); + + @Test + public void toplevelSnippet() throws Exception { + YType v2Schema = typeUtil.inferMoreSpecificType(schema.getTopLevelType(), DynamicSchemaContext.NULL); + System.out.println(generator.getSnippets(v2Schema).iterator().next()); + } + +} diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/mocks/MockCloudConfigProvider.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/mocks/MockCloudConfigProvider.java index e70e76fd2..4b8883151 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/mocks/MockCloudConfigProvider.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/mocks/MockCloudConfigProvider.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.bosh.mocks; import java.util.concurrent.Callable; +import org.springframework.ide.vscode.bosh.BoshCliConfig; import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProviderTest; import org.springframework.ide.vscode.commons.util.ExternalCommand; @@ -29,6 +30,10 @@ public final class MockCloudConfigProvider extends BoshCommandCloudConfigProvide private Callable reader = null; + public MockCloudConfigProvider(BoshCliConfig config) { + super(config); + } + /** * Override with a 'fake' which just returns some mock data. That way we can unit-test * without requiring a real bosh setup. diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProviderTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProviderTest.java index 9193e58a3..faebc6288 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProviderTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandCloudConfigProviderTest.java @@ -14,6 +14,7 @@ import static org.junit.Assert.assertEquals; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.ide.vscode.bosh.BoshCliConfig; import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; @@ -21,7 +22,8 @@ import com.google.common.collect.ImmutableMultiset; public class BoshCommandCloudConfigProviderTest { - public final MockCloudConfigProvider provider = new MockCloudConfigProvider(); + private BoshCliConfig cliConfig = new BoshCliConfig(); + public final MockCloudConfigProvider provider = new MockCloudConfigProvider(cliConfig); // For local testing only... in CI builds we don't have the means to use a real bosh director and cli. // private BoshCommandCloudConfigProvider realProvider = new BoshCommandCloudConfigProvider(); diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProviderTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProviderTest.java new file mode 100644 index 000000000..91b491851 --- /dev/null +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandReleasesProviderTest.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.bosh.models; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.ide.vscode.bosh.BoshCliConfig; +import org.springframework.ide.vscode.commons.util.IOUtil; +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; + +import com.google.common.collect.ImmutableList; + +public class BoshCommandReleasesProviderTest { + + private static final String MOCK_DATA_RSRC = "/cmd-out/releases.json"; + private BoshCliConfig cliConfig = new BoshCliConfig(); + public BoshCommandReleasesProvider provider = Mockito.spy(new BoshCommandReleasesProvider(cliConfig)); + + @Before + public void setup() throws Exception { + Mockito.doReturn(IOUtil.toString(BoshCommandCloudConfigProviderTest.class.getResourceAsStream(MOCK_DATA_RSRC))) + .when(provider).executeCommand(Mockito.any()); + } + + @Test + public void getReleases() throws Exception { + assertEquals(ImmutableList.of(new ReleaseData("learn-bosh", "0+dev.2")), + provider.getModel(mock(DynamicSchemaContext.class)).getReleases() + ); + } + +} diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProviderTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProviderTest.java index 097228ed5..a34c501a0 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProviderTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/BoshCommandStemcellsProviderTest.java @@ -11,24 +11,34 @@ package org.springframework.ide.vscode.bosh.models; import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import java.util.Map; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.ide.vscode.bosh.BoshCliConfig; +import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.IOUtil; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; public class BoshCommandStemcellsProviderTest { - private static final String MOCK_DATA_RSRC = "/cmd-out/stemcells.json";; - public BoshCommandStemcellsProvider provider = Mockito.spy(BoshCommandStemcellsProvider.class); + private static final String MOCK_DATA_RSRC = "/cmd-out/stemcells.json"; + private BoshCliConfig cliConfig = new BoshCliConfig(); + public BoshCommandStemcellsProvider provider = Mockito.spy(new BoshCommandStemcellsProvider(cliConfig)); @Before - public void settup() throws Exception { + public void setup() throws Exception { Mockito.doReturn(IOUtil.toString(BoshCommandCloudConfigProviderTest.class.getResourceAsStream(MOCK_DATA_RSRC))) .when(provider).executeCommand(Mockito.any()); } @@ -62,5 +72,48 @@ public class BoshCommandStemcellsProviderTest { provider.getModel(mock(DynamicSchemaContext.class)).getVersions()); } +// @Test public void defaultCliConfig() throws Exception { +// assertEquals(ImmutableList.of( +// new StemcellData("bosh-vsphere-esxi-centos-7-go_agent", "3421.11", "centos-7"), +// new StemcellData("bosh-vsphere-esxi-ubuntu-trusty-go_agent", "3421.11", "ubuntu-trusty") +// ), +// provider.getModel(mock(DynamicSchemaContext.class)).getStemcells() +// ); +// verify(provider).executeCommand(eq(new ExternalCommand("bosh", "stemcells", "--json"))); +// } + + @Test public void obeysCliConfigCommand() throws Exception { + Map settings = ImmutableMap.of("bosh", ImmutableMap.of("cli", + ImmutableMap.of( + "command", "alternate-command" + ) + )); + + cliConfig.handleConfigurationChange(new Settings(settings)); + assertEquals(ImmutableList.of( + new StemcellData("bosh-vsphere-esxi-centos-7-go_agent", "3421.11", "centos-7"), + new StemcellData("bosh-vsphere-esxi-ubuntu-trusty-go_agent", "3421.11", "ubuntu-trusty") + ), + provider.getModel(mock(DynamicSchemaContext.class)).getStemcells() + ); + verify(provider).executeCommand(eq(new ExternalCommand("alternate-command", "stemcells", "--json"))); + } + + @Test public void obeysCliConfigTarget() throws Exception { + Map settings = ImmutableMap.of("bosh", ImmutableMap.of("cli", + ImmutableMap.of( + "command", "alternate-command", + "target", "explicit-target" + ) + )); + cliConfig.handleConfigurationChange(new Settings(settings)); + assertEquals(ImmutableList.of( + new StemcellData("bosh-vsphere-esxi-centos-7-go_agent", "3421.11", "centos-7"), + new StemcellData("bosh-vsphere-esxi-ubuntu-trusty-go_agent", "3421.11", "ubuntu-trusty") + ), + provider.getModel(mock(DynamicSchemaContext.class)).getStemcells() + ); + verify(provider).executeCommand(eq(new ExternalCommand("alternate-command", "-e", "explicit-target", "stemcells", "--json"))); + } } diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/CachingModelProviderTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/CachingModelProviderTest.java index 86d7ff2da..66b07df60 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/CachingModelProviderTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/models/CachingModelProviderTest.java @@ -12,9 +12,12 @@ package org.springframework.ide.vscode.bosh.models; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import static org.mockito.Mockito.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.junit.Test; @@ -23,24 +26,31 @@ import org.springframework.ide.vscode.commons.util.ExceptionUtil; @SuppressWarnings("unchecked") public class CachingModelProviderTest { + interface BoxModel { + String getContents(); + } + @Test public void goodValuesAreCached() throws Exception { - DynamicModelProvider modelProvider = mock(DynamicModelProvider.class); - when(modelProvider.getModel(any())).thenReturn("RESULT"); + DynamicModelProvider modelProvider = mock(DynamicModelProvider.class); + BoxModel model = mock(BoxModel.class); + when(modelProvider.getModel(any())).thenReturn(model); + when(model.getContents()).thenReturn("RESULT"); - DynamicModelProvider cached = new CachingModelProvider<>(modelProvider); + DynamicModelProvider cached = new CachingModelProvider<>(modelProvider, BoxModel.class); - assertEquals("RESULT", cached.getModel(null)); - assertEquals("RESULT", cached.getModel(null)); - assertEquals("RESULT", cached.getModel(null)); + assertEquals("RESULT", cached.getModel(null).getContents()); + assertEquals("RESULT", cached.getModel(null).getContents()); + assertEquals("RESULT", cached.getModel(null).getContents()); verify(modelProvider, times(1)).getModel(any()); + verify(model, times(1)).getContents(); //model itself is also wrapped in a cache! } @Test public void timeoutExceptionsAreCached() throws Exception { DynamicModelProvider modelProvider = mock(DynamicModelProvider.class); when(modelProvider.getModel(any())).thenThrow(new TimeoutException("timed out")); - DynamicModelProvider cached = new CachingModelProvider<>(modelProvider); + DynamicModelProvider cached = new CachingModelProvider<>(modelProvider, String.class); for (int i = 0; i < 3; i++) { try { cached.getModel(null); diff --git a/headless-services/bosh-language-server/src/test/resources/cmd-out/releases.json b/headless-services/bosh-language-server/src/test/resources/cmd-out/releases.json new file mode 100644 index 000000000..0d2f18cf2 --- /dev/null +++ b/headless-services/bosh-language-server/src/test/resources/cmd-out/releases.json @@ -0,0 +1,28 @@ +{ + "Tables": [ + { + "Content": "releases", + "Header": { + "commit_hash": "Commit Hash", + "name": "Name", + "version": "Version" + }, + "Rows": [ + { + "commit_hash": "5c612c4+", + "name": "learn-bosh", + "version": "0+dev.2" + } + ], + "Notes": [ + "(*) Currently deployed", + "(+) Uncommitted changes" + ] + } + ], + "Blocks": null, + "Lines": [ + "Using environment '10.194.4.35' as client 'admin'", + "Succeeded" + ] +} \ No newline at end of file diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 8a3a1b544..7b8f6acea 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -47,6 +47,8 @@ public class DocumentEdits implements ProposalApplier { private static final Pattern NON_WS_CHAR = Pattern.compile("\\S"); + private boolean isRelativeIndent = false; + // Note: for small number of edits this implementation is okay. // for large number of edits it is potentially slow because of the // way it transforms edit coordinates (a growing chain of 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 ee377f932..d730fde76 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 @@ -117,8 +117,8 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine { private Mono getCompletionsMono(TextDocumentPositionParams params) { SimpleTextDocumentService documents = server.getTextDocumentService(); - TextDocument doc = documents.get(params).copy(); - if (doc!=null) { + if (documents.get(params) != null) { + TextDocument doc = documents.get(params).copy(); return Mono.fromCallable(() -> { if (resolver!=null) { //Assumes we don't have more than one completion request in flight from the client. diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java index 360455bd1..7de697520 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java @@ -52,31 +52,33 @@ public class SimpleDefinitionFinder implements D * currently pointed at in the current document using String.indexOf. */ protected Flux findDefinitions(TextDocumentPositionParams params) { - try { + try { TextDocument doc = server.getTextDocumentService().get(params); - int offset = doc.toOffset(params.getPosition()); - int start = offset; - while (Character.isLetter(doc.getSafeChar(start))) { - start--; - } - start = start+1; - int end = offset; - while (Character.isLetter(doc.getSafeChar(end))) { - end++; - } - String word = doc.textBetween(start, end); - Log.log("Looking for definition of '"+word+"'"); - String text = doc.get(); - int def = text.indexOf(word); - if (def>=0) { - return Flux.just( - new Location(params.getTextDocument().getUri(), - doc.toRange(def, word.length()) + if (doc != null) { + int offset = doc.toOffset(params.getPosition()); + int start = offset; + while (Character.isLetter(doc.getSafeChar(start))) { + start--; + } + start = start+1; + int end = offset; + while (Character.isLetter(doc.getSafeChar(end))) { + end++; + } + String word = doc.textBetween(start, end); + Log.log("Looking for definition of '"+word+"'"); + String text = doc.get(); + int def = text.indexOf(word); + if (def>=0) { + return Flux.just( + new Location(params.getTextDocument().getUri(), + doc.toRange(def, word.length()) + ) ) - ) - .doOnNext((Location loc) -> { - Log.log("definition: "+loc); - }); + .doOnNext((Location loc) -> { + Log.log("definition: "+loc); + }); + } } } catch (Exception e) { Log.log(e); diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/Settings.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/Settings.java index a9378bb8e..ad72eec18 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/Settings.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/Settings.java @@ -8,7 +8,6 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ - package org.springframework.ide.vscode.commons.languageserver.util; import java.util.Map; @@ -18,9 +17,9 @@ import java.util.Map; * retrieve properties from the settings object. */ public class Settings { - + private Object settings; - + public Settings(Object settings) { this.settings = settings; } @@ -32,7 +31,7 @@ public class Settings { } return null; } - + public Object getProperty(String... names) { return getProperty(settings, names, 0); } @@ -49,6 +48,8 @@ public class Settings { } } - - + @Override + public String toString() { + return settings.toString(); + } } 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 index ce57b8eb8..850d152a0 100644 --- 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 @@ -35,11 +35,7 @@ public class SnippetBuilder { * 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. + * match format specified by LSP 3.0. */ protected String createPlaceHolder(int id) { return "$"+id; @@ -50,4 +46,24 @@ public class SnippetBuilder { return buf.toString(); } + public void newline(int indent) { + buf.append("\n"); + for (int i = 0; i < indent; i++) { + buf.append(' '); + } + } + + public void ensureSpace() { + if (buf.length()>0 && !Character.isWhitespace(buf.charAt(buf.length()-1))) { + buf.append(' '); + } + } + + /** + * @return The number of placeholder that where inserted in the snippet. + */ + public int getPlaceholderCount() { + return nextPlaceHolderId-1; + } + } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java index e1ee565ee..a60ceddef 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java @@ -26,8 +26,10 @@ import com.google.common.collect.ImmutableSet; public class EnumValueParser implements ValueParser { private String typeName; - private Provider> values; - private final boolean longRunning; + + private Provider> values; + private final boolean longRunning; + public EnumValueParser(String typeName, String... values) { this(typeName, ImmutableSet.copyOf(values)); @@ -37,16 +39,28 @@ public class EnumValueParser implements ValueParser { this(typeName, false /* not long running by default */, provider(values)); } + private static Provider> provider(Collection values) { + return () -> PartialCollection.compute(() -> values); + } + + private static Provider> provider(Callable> values) { + return () -> PartialCollection.compute(() -> values.call()); + } + public EnumValueParser(String typeName, boolean longRunning, Callable> values) { this(typeName, longRunning, provider(values)); } - public EnumValueParser(String typeName, boolean longRunning, Provider> values) { + public EnumValueParser(String typeName, boolean longRunning, Provider> values) { this.typeName = typeName; this.values = values; this.longRunning = longRunning; } + public EnumValueParser(String name, PartialCollection values) { + this(name, false /* not long running by default */, () -> values); + } + @Override public Object parse(String str) throws Exception { // IMPORTANT: check the text FIRST before fetching values @@ -56,13 +70,13 @@ public class EnumValueParser implements ValueParser { throw errorOnBlank(createBlankTextErrorMessage()); } - Collection values = this.values.get(); + PartialCollection values = this.values.get(); - // If values is not known (null) then just assume the str is acceptable. - if (values == null || values.contains(str)) { + // If values is not fully known then just assume the str is acceptable. + if (values == null || !values.isComplete() || values.getElements().contains(str)) { return str; } else { - throw errorOnParse(createErrorMessage(str, values)); + throw errorOnParse(createErrorMessage(str, values.getElements())); } } @@ -81,21 +95,6 @@ public class EnumValueParser implements ValueParser { protected Exception errorOnBlank(String message) { return new ValueParseException(message); } - - private static Provider provider(T values) { - return () -> values; - } - - private static Provider provider(Callable values) { - return () -> { - try { - return values.call(); - } catch (Exception e) { - // Ignore - return null; - } - }; - } public boolean longRunning() { return this.longRunning ; diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ExternalCommand.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ExternalCommand.java index 8999daccd..4b3fd40b9 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ExternalCommand.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ExternalCommand.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.util; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.concurrent.TimeoutException; /** @@ -76,4 +77,27 @@ public class ExternalCommand { // org.junit.Assert.assertEquals(0, process.getExitValue()); } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(command); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ExternalCommand other = (ExternalCommand) obj; + if (!Arrays.equals(command, other.command)) + return false; + return true; + } + + } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/PartialCollection.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/PartialCollection.java new file mode 100644 index 000000000..93efa7477 --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/PartialCollection.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; + +/** + * A partial collection instance represents collection of + * elements which may not be entirely known. + *

+ * For unknown collection, an optional explanation, in the + * form of a caught exception may be stored as well. + */ +public class PartialCollection { + + private static final PartialCollection UNKNOWN = new PartialCollection<>(ImmutableSet.of(), false); + private static final PartialCollection EMPTY = new PartialCollection<>(ImmutableSet.of(), true); + + final private ImmutableCollection knownElements; + final private boolean isComplete; + final private Throwable explanation; + + private PartialCollection(ImmutableCollection knownElements, boolean isComplete, Throwable error) { + this.knownElements = knownElements; + this.isComplete = isComplete; + this.explanation = error; + } + + private PartialCollection(ImmutableCollection knownElements, boolean isComplete) { + this.knownElements = knownElements; + this.isComplete = isComplete; + this.explanation = null; + } + + private PartialCollection(ImmutableCollection knownElements, Throwable error) { + this.knownElements = knownElements; + this.isComplete = error==null; + this.explanation = error; + } + + /** + * Create a {@link PartialCollection} by executing some computation that returs a collectioon. + * If the computation throws the resulting collection will be completely unknown, otherwise + * it will be completely known. + */ + public static PartialCollection compute(Callable> computer) { + try { + Collection allValues = computer.call(); + if (allValues==null) { + return PartialCollection.unknown(); + } + return new PartialCollection<>(ImmutableSet.copyOf(allValues), true); + } catch (Exception e) { + return new PartialCollection<>(ImmutableSet.of(), e); + } + } + + /** + * Create a {@link PartialCollection} by executing some computation that returs a collectioon. + * If the computation throws the resulting collection will be completely unknown, otherwise + * it will be completely known. + */ + public static PartialCollection fromCallable(Callable> computer) { + try { + return computer.call(); + } catch (Exception e) { + return new PartialCollection<>(ImmutableSet.of(), e); + } + } + + + /** + * @return All the known elements of this partial collection. + */ + public Collection getElements() { + return knownElements; + } + + public boolean isComplete() { + return isComplete; + } + + /** + * Returns the totally unknown collection. I.e. a unknown collection with no known elements + */ + @SuppressWarnings("unchecked") + public static PartialCollection unknown() { + return (PartialCollection) UNKNOWN; + } + + /** + * Like map on streams, but silently drops any null elements returned by the mapper. + */ + public PartialCollection map(Function mapper) { + ImmutableSet mappedElements = getElements().stream().map((x) -> mapper.apply(x)).filter(x -> x!=null).collect(CollectorUtil.toImmutableSet()); + return new PartialCollection(mappedElements, isComplete, explanation); + } + + /** + * Returns a empty collection (i.e. the collection is know to be empty). + */ + @SuppressWarnings("unchecked") + public static PartialCollection empty() { + return (PartialCollection) EMPTY; + } + + /** + * Make a copy of this collection that has the same known elements but also has unknown elements. + */ + public PartialCollection addUncertainty() { + if (!this.isComplete()) { + return this; //No need to make a copy. Current collection is already only partially known. + } + return new PartialCollection<>(knownElements, false); + } + + /** + * A completely unknown collection with a given exception explaining the reason. + */ + public static PartialCollection unknown(Exception e) { + Assert.isLegal(e!=null); + return new PartialCollection<>(ImmutableSet.of(), e); + } + + public PartialCollection addAll(Collection moreElements) { + ImmutableSet.Builder elements = ImmutableSet.builder(); + elements.addAll(getElements()); + elements.addAll(moreElements); + return new PartialCollection<>(elements.build(), isComplete, explanation); + } + + public Throwable getExplanation() { + return explanation; + } + + public PartialCollection add(@SuppressWarnings("unchecked") T... values) { + return addAll(Arrays.asList(values)); + } +} diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java index ec79fe6b4..0e61073cc 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java @@ -21,6 +21,7 @@ import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.linetracker.DefaultLineTracker; import org.springframework.ide.vscode.commons.util.text.linetracker.ILineTracker; @@ -88,11 +89,14 @@ public class TextDocument implements IDocument { public synchronized void apply(DidChangeTextDocumentParams params) throws BadLocationException { int newVersion = params.getTextDocument().getVersion(); - Assert.isLegal(version completions = getValueCompletions(doc, node, offset, query); if (completions.isEmpty()) { completions = getKeyCompletions(doc, offset, query); + TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider(); + if (snippetProvider!=null) { + Collection snippets = snippetProvider.getSnippets(type); + YamlIndentUtil indenter = new YamlIndentUtil(doc); + for (Snippet snippet : snippets) { + String snippetName = snippet.getName(); + double score = FuzzyMatcher.matchScore(query, snippetName); + if (score!=0.0 && snippet.isApplicable(getSchemaContext())) { + DocumentEdits edits = new DocumentEdits(doc.getDocument()); + int start = offset - query.length(); + edits.delete(start, query); + int referenceIndent = doc.getColumn(start); + boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(start-1)); + if (needsSpace) { + referenceIndent++; + edits.insert(start, " "); + } + edits.insert(start, indenter.applyIndentation(snippet.getSnippet(), referenceIndent)); + completions.add(completionFactory().valueProposal(snippetName, query, snippetName, type, null, score, edits, typeUtil)); + } + } + } } if (typeUtil.isSequencable(type)) { completions = new ArrayList<>(completions); @@ -107,7 +132,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { public List getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception { int queryOffset = offset - query.length(); - SNode contextNode = getContextNode(); DynamicSchemaContext dynamicCtxt = getSchemaContext(); List allProperties = typeUtil.getProperties(type); if (CollectionUtil.hasElements(allProperties)) { @@ -115,6 +139,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { Set definedProps = dynamicCtxt.getDefinedProperties(); List proposals = new ArrayList<>(); boolean suggestDeprecated = typeUtil.suggestDeprecatedProperties(); + YamlIndentUtil indenter = new YamlIndentUtil(doc); for (List thisTier : tieredProperties) { List undefinedProps = thisTier.stream() .filter(p -> !definedProps.contains(p.getName()) && (suggestDeprecated || !p.isDeprecated())) @@ -124,15 +149,17 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { String name = p.getName(); double score = FuzzyMatcher.matchScore(query, name); if (score!=0) { - YamlPath relativePath = YamlPath.fromSimpleProperty(name); - YamlPathEdits edits = new YamlPathEdits(doc); + DocumentEdits edits = new DocumentEdits(doc.getDocument()); YType YType = p.getType(); edits.delete(queryOffset, query); + int referenceIndent = doc.getColumn(queryOffset); if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) { //See https://www.pivotaltracker.com/story/show/137722057 edits.insert(queryOffset, " "); + referenceIndent++; } - edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType)); + String snippet = p.getName()+":" +appendTextFor(YType); + edits.insert(queryOffset, indenter.applyIndentation(snippet, referenceIndent)); ICompletionProposal completion = completionFactory().beanProperty(doc.getDocument(), contextPath.toPropString(), getType(), query, p, score, edits, typeUtil); @@ -197,16 +224,11 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { } private List getValueCompletions(YamlDocument doc, SNode node, int offset, String query) { - YValueHint[] values=null; - try { - values = typeUtil.getHintValues(type, getSchemaContext()); - } catch (Exception e) { - if (!Boolean.getBoolean("lsp.yaml.completions.errors.disable")) { - return ImmutableList.of(completionFactory().errorMessage(query, getMessage(e))); - } else { - Log.warn(query, e); - } + PartialCollection _values = typeUtil.getHintValues(type, getSchemaContext()); + if (_values.getExplanation()!=null && _values.getElements().isEmpty() && !Boolean.getBoolean("lsp.yaml.completions.errors.disable")) { + return ImmutableList.of(completionFactory().errorMessage(query, getMessage(_values.getExplanation()))); } + Collection values = _values.getElements(); if (values!=null) { ArrayList completions = new ArrayList<>(); YamlIndentUtil indenter = new YamlIndentUtil(doc); @@ -243,7 +265,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { return Collections.emptyList(); } - private String getMessage(Exception _e) { + private String getMessage(Throwable _e) { Throwable e = ExceptionUtil.getDeepestCause(_e); // If value parse exception, do not append any additional information @@ -384,18 +406,20 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { @Override protected DocumentEdits transformEdit(DocumentEdits textEdit) { textEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> { + YamlIndentUtil indenter = new YamlIndentUtil("\n"); if (needNewline(textEdit)) { return insertText.substring(0, offset) + "\n" +Strings.repeat(" ", node.getIndent())+"- " - + insertText.substring(offset); + + indenter.applyIndentation(insertText.substring(offset), YamlIndentUtil.INDENT_BY); } else if (offset > 2) { String prefix = insertText.substring(offset-2, offset); if (" ".equals(prefix)) { //special case don't add the "- " in front, but replace the inserted spaces instead. - return insertText.substring(0, offset-2)+"- "+insertText.substring(offset); + return insertText.substring(0, offset-2) + + "- "+ insertText.substring(offset); } } - return insertText.substring(0, offset) + "- "+insertText.substring(offset); + return insertText.substring(0, offset) + "- "+indenter.applyIndentation(insertText.substring(offset), YamlIndentUtil.INDENT_BY); }); return textEdit; } @@ -404,11 +428,9 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { //value proposals which are inserted right after a key will not automatically include a newline, as // its not required for them. So we should add it along with the dash. try { - if (original instanceof ValueProposal) { - Integer insertAt = textEdit.getFirstEditStart(); - if (insertAt!=null) { - return !"".equals(doc.getLineTextBefore(insertAt).trim()); - } + Integer insertAt = textEdit.getFirstEditStart(); + if (insertAt!=null) { + return !"".equals(doc.getLineTextBefore(insertAt).trim()); } } catch (Exception e) { Log.log(e); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index e5bce3e1d..6cb30cc96 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -52,7 +52,7 @@ import com.google.common.collect.ImmutableList; * @author Kris De Volder */ public class YamlCompletionEngine implements ICompletionEngine { - + Pattern SPACES = Pattern.compile("[ ]+"); final static Logger logger = LoggerFactory.getLogger(YamlCompletionEngine.class); @@ -110,7 +110,7 @@ public class YamlCompletionEngine implements ICompletionEngine { protected Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent, double deempasizeBy) { try { - return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), + return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), current, contextNode, baseIndent, deempasizeBy); } catch (Exception e) { Log.log(e); @@ -118,7 +118,7 @@ public class YamlCompletionEngine implements ICompletionEngine { return ImmutableList.of(); } - protected Collection fixIndentations(Collection completions, SNode currentNode, + protected Collection fixIndentations(Collection completions, SNode currentNode, SNode contextNode, int baseIndent, double deempasizeBy) { if (!completions.isEmpty()) { int dashyIndent = getTargetIndent(contextNode, currentNode, true); @@ -144,7 +144,7 @@ public class YamlCompletionEngine implements ICompletionEngine { if (isExtraIndentRelaxable(contextNode, fixIndentBy)) { return indented(p, Strings.repeat(" ", fixIndentBy)); } - } else { // fixIndentBy < 0 + } else { // fixIndentBy < 0 if (isLesserIndentRelaxable(currentNode, contextNode)) { return dedented(p, -fixIndentBy, contextNode.getDocument()); } @@ -168,7 +168,7 @@ public class YamlCompletionEngine implements ICompletionEngine { } /** - * Determine the indentation level needed to line up with other contextNode children. + * Determine the indentation level needed to line up with other contextNode children. * If the contextNode has no children, then compute a proper default indentation where * a new child could be added. */ @@ -182,8 +182,8 @@ public class YamlCompletionEngine implements ICompletionEngine { if (child.isPresent()) { return child.get().getIndent(); } - return (dashy || contextNode.getNodeType()==SNodeType.DOC) - ? contextNode.getIndent() + return (dashy || contextNode.getNodeType()==SNodeType.DOC) + ? contextNode.getIndent() : contextNode.getIndent() + YamlIndentUtil.INDENT_BY; } @@ -212,7 +212,7 @@ public class YamlCompletionEngine implements ICompletionEngine { }; transformed.deemphasize(DEEMP_DEDENTED_PROPOSAL*numArrows); return transformed; - } + } // we can't dedent the proposal by the requested amount of space. So err on the safe // side and ignore the proposal. (Otherwise me might end up deleting non-space chars // in our attempt to de-dent.) @@ -226,7 +226,13 @@ public class YamlCompletionEngine implements ICompletionEngine { return Strings.repeat(Unicodes.RIGHT_ARROW+" ", numArrows) + originalLabel; } @Override public DocumentEdits transformEdit(DocumentEdits originalEdit) { - originalEdit.indentFirstEdit(indentStr); +// originalEdit.indentFirstEdit(indentStr); + YamlIndentUtil indenter = new YamlIndentUtil("\n"); + originalEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> { + String prefix = insertText.substring(0, offset); + String target = insertText.substring(offset); + return prefix + indentStr + indenter.applyIndentation(target, indentStr); + }); return originalEdit; } }; @@ -306,14 +312,14 @@ public class YamlCompletionEngine implements ICompletionEngine { } return null; } - + /** * Get context node candidates taking into account that we want to have a 'relaxed' interpretation * of the context node with respect to the current indentation where we ask for a completion. - * To allow for the ambiguity in indentation a list of context nodes is returned instead of a + * To allow for the ambiguity in indentation a list of context nodes is returned instead of a * single node. (Note we may still return a singleton list for cases where relaxed indentation * doesn't seem desirable). - * @param baseIndent + * @param baseIndent */ protected List getContextNodes(YamlDocument doc, SNode node, int offset, int baseIndent) { if (node==null) { @@ -340,7 +346,7 @@ public class YamlCompletionEngine implements ICompletionEngine { //This node has flexibility around indentation. So this is where me need to build a list of candidates! ImmutableList.Builder contextNodes = ImmutableList.builder(); while (node!=null ) { - //Any node that represents a 'step' between contexts and is not too deeply nested is kept. + //Any node that represents a 'step' between contexts and is not too deeply nested is kept. if (node.getSegment()!=null && node.getIndent()<=baseIndent) { contextNodes.add(node); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java index d5c74d5a2..d4a1a537a 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java @@ -13,15 +13,15 @@ package org.springframework.ide.vscode.commons.yaml.completion; public interface YamlCompletionEngineOptions { /** * Whether the completion engine includes 'less indented' proposals (i.e. proposals - * that aren't valid at the current CA position, but are valid if we delete - * some spaces in front of the cursor first. + * that aren't valid at the current CA position, but are valid if we delete + * some spaces in front of the cursor first. */ - default boolean includeDeindentedProposals() { + default boolean includeDeindentedProposals() { //Disabled by default for now because of bug introduced in VSCode 1.12: //https://github.com/Microsoft/vscode/issues/26096 return true; } - + YamlCompletionEngineOptions DEFAULT = new YamlCompletionEngineOptions() {}; YamlCompletionEngineOptions TEST_DEFAULT = new YamlCompletionEngineOptions() { @Override public boolean includeDeindentedProposals() { return true; } 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 eff8b1eb2..7a937cae5 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 @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -29,14 +28,16 @@ import java.util.stream.Collectors; 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.CollectorUtil; import org.springframework.ide.vscode.commons.util.EnumValueParser; -import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; @@ -56,6 +57,7 @@ public class YTypeFactory { private boolean enableTieredOptionalPropertyProposals = true; private boolean suggestDeprecatedProperties = true; + private TypeBasedSnippetProvider snippetProvider = null; private static class Deprecation { final String errorMsg; @@ -185,7 +187,7 @@ public class YTypeFactory { } @Override - public YValueHint[] getHintValues(YType type, DynamicSchemaContext dc) throws Exception { + public PartialCollection getHintValues(YType type, DynamicSchemaContext dc) { return ((AbstractType)type).getHintValues(dc); } @@ -235,6 +237,11 @@ public class YTypeFactory { return ((AbstractType)type).getCustomContentAssistant(); } + @Override + public TypeBasedSnippetProvider getSnippetProvider() { + return snippetProvider; + } + @Override public boolean tieredOptionalPropertyProposals() { return enableTieredOptionalPropertyProposals; @@ -257,7 +264,7 @@ public class YTypeFactory { private List propertyList = new ArrayList<>(); private List hints = new ArrayList<>(); private Map cachedPropertyMap; - private SchemaContextAware>> hintProvider; + private SchemaContextAware> hintProvider; //TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware> private List constraints = new ArrayList<>(2); @@ -293,47 +300,19 @@ public class YTypeFactory { } public AbstractType setHintProvider(Callable> hintProvider) { - setHintProvider((DynamicSchemaContext dc) -> hintProvider); + setHintProvider((DynamicSchemaContext dc) -> PartialCollection.compute(hintProvider)); return this; } - public AbstractType setHintProvider(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 = 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()]); - } else { - // Only merge if there are provider hints to merge - Set mergedHints = new LinkedHashSet<>(); - - // Add type hints first - for (YValueHint val : hints) { - mergedHints.add(val); - } - - // merge the provider hints - for (YValueHint val : providerHints) { - mergedHints.add(val); - } - return mergedHints.toArray(new YValueHint[mergedHints.size()]); - } + public PartialCollection getHintValues(DynamicSchemaContext dc) { + return getProviderHints(dc) + .addAll(hints); } /** @@ -343,14 +322,15 @@ public class YTypeFactory { hints = ImmutableList.copyOf(hints); } - private Collection getProviderHints(DynamicSchemaContext dc) throws Exception { + private PartialCollection getProviderHints(DynamicSchemaContext dc) { if (hintProvider != null) { - Callable> withContext = hintProvider.withContext(dc); - if (withContext != null) { - return withContext.call(); + try { + return hintProvider.withContext(dc); + } catch (Exception e) { + return PartialCollection.unknown(e); } } - return ImmutableList.of(); + return PartialCollection.empty(); } public List getConstraints() { @@ -417,7 +397,7 @@ public class YTypeFactory { parseWith((DynamicSchemaContext dc) -> parser); return this; } - private SchemaContextAware getParser() { + public SchemaContextAware getParser() { return parser; } @@ -878,25 +858,41 @@ public class YTypeFactory { return ((YTypedPropertyImpl)prop).copy(); } - public YAtomicType yenumFromHints(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { + public YAtomicType yenumFromHints(String name, SchemaContextAware, String>> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); - t.setHintProvider((dc) -> () -> values.withContext(dc)); + t.setHintProvider(values); t.parseWith((DynamicSchemaContext dc) -> { - Collection strings = YTypeFactory.values(values.withContext(dc)); - return new EnumValueParser(name, strings) { + PartialCollection hints = PartialCollection.fromCallable(() -> values.withContext(dc)); + return new EnumValueParser(name, hints.map(h -> h.getValue())) { @Override protected String createErrorMessage(String parseString, Collection values) { - return errorMessageFormatter.apply(parseString, values); + try { + return errorMessageFormatter.withContext(dc).apply(parseString, values); + } catch (Exception e) { + return super.createErrorMessage(parseString, values); + } } }; }); return t; } - public YAtomicType yenumFromDynamicValues(String name, SchemaContextAware> values) { + public YAtomicType yenumFromDynamicValues(String name, + SchemaContextAware, String>> errorMessageFormatter, + SchemaContextAware> values + ) { return yenumFromHints(name, //Error message formatter: - (parseString, validValues) -> "'"+parseString+"' is an unknown '"+name+"'. Valid values are: "+validValues, + errorMessageFormatter, + //Hints provider: + (dc) -> hints(values.withContext(dc)) + ); + } + + public YAtomicType yenumFromDynamicValues(String name, SchemaContextAware> values) { + return yenumFromHints(name, + //Error message formatter: + (dc) -> (parseString, validValues) -> "'"+parseString+"' is an unknown '"+name+"'. Valid values are: "+validValues, //Hints provider: (dc) -> hints(values.withContext(dc)) ); @@ -906,30 +902,35 @@ public class YTypeFactory { return new EnumTypeBuilder(name, values); } - public YAtomicType yenum(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { + public YAtomicType yenum(String name, SchemaContextAware, String>> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); t.setHintProvider((dc) -> { - Collection strings = values.withContext(dc); - return strings==null - ? null - : () -> strings.stream() - .map((s) -> new BasicYValueHint(s)) - .collect(Collectors.toSet()); + return PartialCollection.compute(() -> values.withContext(dc)) + .map(BasicYValueHint::new); }); t.parseWith((DynamicSchemaContext dc) -> { EnumValueParser enumParser = new EnumValueParser(name, values.withContext(dc)) { @Override protected String createErrorMessage(String parseString, Collection values) { - return errorMessageFormatter.apply(parseString, values); + try { + return errorMessageFormatter.withContext(dc).apply(parseString, values); + } catch (Exception e) { + return super.createErrorMessage(parseString, values); + } } }; return enumParser; }); return t; + + } + + public YAtomicType yenum(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { + return yenum(name, (dc) -> errorMessageFormatter, values); } public static Collection values(Collection hints) { - return hints.stream().map(YValueHint::getValue).collect(Collectors.toList()); + return hints == null ? null : hints.stream().map(YValueHint::getValue).collect(Collectors.toList()); } public YAtomicType yenum(String name, String... values) { @@ -960,10 +961,16 @@ public class YTypeFactory { } public static Collection hints(Collection values) { + return values.stream() + .map(YTypeFactory::hint) + .collect(CollectorUtil.toMultiset()); + } + + public static PartialCollection hints(PartialCollection values) { if (values!=null) { - return values.stream().map(YTypeFactory::hint).collect(Collectors.toList()); + return values.map(YTypeFactory::hint); } - return null; + return PartialCollection.unknown(); } public YTypeFactory enableTieredProposals(boolean enable) { @@ -976,5 +983,10 @@ public class YTypeFactory { return this; } + public YTypeFactory setSnippetProvider(TypeBasedSnippetProvider snippetProvider) { + this.snippetProvider = snippetProvider; + return this; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java index 7688e9099..5517a3c8f 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java @@ -10,11 +10,15 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.schema; +import java.util.Collection; import java.util.List; import java.util.Map; +import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; /** * An implementation of YTypeUtil provides implementations of various @@ -30,7 +34,7 @@ public interface YTypeUtil { boolean isSequencable(YType type); boolean isBean(YType type); YType getDomainType(YType type); - YValueHint[] getHintValues(YType yType, DynamicSchemaContext dc) throws Exception; + PartialCollection getHintValues(YType yType, DynamicSchemaContext dc); String niceTypeName(YType type); YType getKeyType(YType type); SchemaContextAware getValueParser(YType type); @@ -47,9 +51,17 @@ public interface YTypeUtil { */ YType inferMoreSpecificType(YType type, DynamicSchemaContext dc); List getConstraints(YType type); - + ISubCompletionEngine getCustomContentAssistant(YType type); - + + /** + * Config option for type-bases complection enging. Snippets can be + * associated with schema types. These snippets will be suggested as + * additional completions based on the type of value expected in + * a context. + */ + TypeBasedSnippetProvider getSnippetProvider(); + /** * Config option for type-based completion engine. This enables the * 'tiered' proposals feature (so that optional properties are not @@ -58,7 +70,7 @@ public interface YTypeUtil { boolean tieredOptionalPropertyProposals(); /** * Config option for type-based completion engine. This enables/disables - * whether engine should generate proposals for deprecated properties (true), + * whether engine should generate proposals for deprecated properties (true), * or suppress them (false). */ boolean suggestDeprecatedProperties(); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java new file mode 100644 index 000000000..e5a074434 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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.yaml.snippet; + +import java.util.function.Predicate; + +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; + +public class Snippet { + + private final String name; + private final String snippet; + private final Predicate applicability; + + public Snippet(String name, String snippet, Predicate applicability) { + super(); + this.name = name; + this.snippet = snippet; + this.applicability = applicability; + } + public String getName() { + return name; + } + public String getSnippet() { + return snippet; + } + + @Override + public String toString() { + return "Snippet [ name="+name+",\n" +snippet +"\n]"; + } + public Predicate getApplicability() { + return applicability; + } + public boolean isApplicable(DynamicSchemaContext dc) { + return applicability==null || applicability.test(dc); + } +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java new file mode 100644 index 000000000..235730073 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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.yaml.snippet; + +import java.util.Collection; + +import org.springframework.ide.vscode.commons.yaml.schema.YType; + +public interface TypeBasedSnippetProvider { + + Collection getSnippets(YType contextType); + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java index 4952c4b05..8fe013109 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.yaml.util; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import com.google.common.base.Strings; @@ -42,7 +43,11 @@ public class YamlIndentUtil { } public YamlIndentUtil(YamlDocument doc) { - this(doc.getDocument().getDefaultLineDelimiter()); + this(doc.getDocument()); + } + + public YamlIndentUtil(IDocument doc) { + this(doc.getDefaultLineDelimiter()); } /** @@ -89,6 +94,10 @@ public class YamlIndentUtil { return text.replaceAll("\\n", newlineWithIndent(indentBy)); } + public String applyIndentation(String text, String indentStr) { + return text.replaceAll("\\n", "\n"+indentStr); + } + /** * Increase offset by indentation. Take care when 'indent' is -1 (unkownn) to * just return offset unmodified. 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 fe3e73d13..4e81b51a8 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 @@ -63,6 +63,7 @@ public class Editor { public static final Predicate PLAIN_COMPLETION = c -> !RELAXED_COMPLETION.test(c); public static final Predicate DEDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.LEFT_ARROW+" "); public static final Predicate INDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" "); + public static final Predicate SNIPPET_COMPLETION = c -> c.getLabel().endsWith("Snippet"); static class EditorState { String documentContents; @@ -327,6 +328,10 @@ public class Editor { } public List assertCompletionLabels(String... expectedLabels) throws Exception { + return assertCompletionLabels(c -> true, expectedLabels); + } + + public List assertCompletionLabels(Predicate isInteresting, String... expectedLabels) throws Exception { StringBuilder expect = new StringBuilder(); StringBuilder actual = new StringBuilder(); for (String label : expectedLabels) { @@ -336,8 +341,10 @@ public class Editor { List completions; for (CompletionItem completion : completions = getCompletions()) { - actual.append(completion.getLabel()); - actual.append("\n"); + if (isInteresting.test(completion)) { + actual.append(completion.getLabel()); + actual.append("\n"); + } } assertEquals(expect.toString(), actual.toString()); return completions; @@ -417,19 +424,25 @@ public class Editor { String docText = doc.getText(); if (edit!=null) { String replaceWith = edit.getNewText(); - //Apply indentfix, this is magic vscode seems to apply to edits returned by language server. So our harness has to - // mimick that behavior. See https://github.com/Microsoft/language-server-protocol/issues/83 - int referenceLine = edit.getRange().getStart().getLine(); - int cursorOffset = edit.getRange().getStart().getCharacter(); - String referenceIndent = doc.getLineIndentString(referenceLine); - if (cursorOffset=0) { - replaceWith = replaceWith.substring(0, cursorReplaceOffset) + replaceWith.substring(cursorReplaceOffset+VS_CODE_CURSOR_MARKER.length()); + if (!Boolean.getBoolean("lsp.completions.indentation.enable")) { + //Apply indentfix, this is magic vscode seems to apply to edits returned by language server. So our harness has to + // mimick that behavior. See https://github.com/Microsoft/language-server-protocol/issues/83 + int referenceLine = edit.getRange().getStart().getLine(); + int cursorOffset = edit.getRange().getStart().getCharacter(); + String referenceIndent = doc.getLineIndentString(referenceLine); + if (cursorOffset= 0) { + replaceWith = replaceWith.substring(0, cursorReplaceOffset) + + replaceWith.substring(cursorReplaceOffset + VS_CODE_CURSOR_MARKER.length()); } else { cursorReplaceOffset = replaceWith.length(); } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 05e5f6fc4..07830f3b0 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -92,16 +92,16 @@ import com.google.common.collect.ImmutableList; import reactor.core.publisher.Mono; -public class LanguageServerHarness { +public class LanguageServerHarness { //Warning this 'harness' is incomplete. Growing it as needed. private Random random = new Random(); - private Callable factory; + private Callable factory; private LanguageId defaultLanguageId; - private SimpleLanguageServer server; + private S server; private InitializeResult initResult; @@ -110,12 +110,12 @@ public class LanguageServerHarness { private List activeEditors = new ArrayList<>(); - public LanguageServerHarness(Callable factory, LanguageId defaultLanguageId) { + public LanguageServerHarness(Callable factory, LanguageId defaultLanguageId) { this.factory = factory; this.defaultLanguageId = defaultLanguageId; } - public LanguageServerHarness(Callable factory) throws Exception { + public LanguageServerHarness(Callable factory) throws Exception { this(factory, LanguageId.PLAINTEXT); } @@ -184,9 +184,9 @@ public class LanguageServerHarness { workspaceCap.setExecuteCommand(exeCap); clientCap.setWorkspace(workspaceCap); initParams.setCapabilities(clientCap); - initResult = server.initialize(initParams).get(); - if (server instanceof LanguageClientAware) { - ((LanguageClientAware) server).connect(new STS4LanguageClient() { + initResult = getServer().initialize(initParams).get(); + if (getServer() instanceof LanguageClientAware) { + ((LanguageClientAware) getServer()).connect(new STS4LanguageClient() { @Override public void telemetryEvent(Object object) { // TODO Auto-generated method stub @@ -248,15 +248,15 @@ public class LanguageServerHarness { }); } - server.initialized(); + getServer().initialized(); return initResult; } public TextDocumentInfo openDocument(TextDocumentInfo documentInfo) throws Exception { DidOpenTextDocumentParams didOpen = new DidOpenTextDocumentParams(); didOpen.setTextDocument(documentInfo.getDocument()); - if (server!=null) { - server.getTextDocumentService().didOpen(didOpen); + if (getServer()!=null) { + getServer().getTextDocumentService().didOpen(didOpen); } return documentInfo; } @@ -295,8 +295,8 @@ public class LanguageServerHarness { default: throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode()); } - if (server!=null) { - server.getTextDocumentService().didChange(didChange); + if (getServer()!=null) { + getServer().getTextDocumentService().didChange(didChange); } return documents.get(uri); } @@ -320,8 +320,8 @@ public class LanguageServerHarness { default: throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode()); } - if (server!=null) { - server.getTextDocumentService().didChange(didChange); + if (getServer()!=null) { + getServer().getTextDocumentService().didChange(didChange); } return documents.get(uri); } @@ -339,7 +339,7 @@ public class LanguageServerHarness { } public PublishDiagnosticsParams getDiagnostics(TextDocumentInfo doc) throws Exception { - this.server.waitForReconcile(); + this.getServer().waitForReconcile(); return diagnostics.get(doc.getUri()); } @@ -376,8 +376,8 @@ public class LanguageServerHarness { TextDocumentPositionParams params = new TextDocumentPositionParams(); params.setPosition(cursor); params.setTextDocument(doc.getId()); - server.waitForReconcile(); - Either, CompletionList> completions = server.getTextDocumentService().completion(params).get(); + getServer().waitForReconcile(); + Either, CompletionList> completions = getServer().getTextDocumentService().completion(params).get(); if (completions.isLeft()) { List list = completions.getLeft(); return new CompletionList(false, list); @@ -391,14 +391,14 @@ public class LanguageServerHarness { TextDocumentPositionParams params = new TextDocumentPositionParams(); params.setPosition(cursor); params.setTextDocument(document.getId()); - return server.getTextDocumentService().hover(params ).get(); + return getServer().getTextDocumentService().hover(params ).get(); } public CompletionItem resolveCompletionItem(CompletionItem maybeUnresolved) { - if (server.hasLazyCompletionResolver()) { + if (getServer().hasLazyCompletionResolver()) { try { - return server.getTextDocumentService().resolveCompletionItem(maybeUnresolved).get(); + return getServer().getTextDocumentService().resolveCompletionItem(maybeUnresolved).get(); } catch (Exception e) { throw new RuntimeException(e); } @@ -477,14 +477,14 @@ public class LanguageServerHarness { } public List getDefinitions(TextDocumentPositionParams params) throws Exception { - server.waitForReconcile(); //goto definitions relies on reconciler infos! Must wait or race condition breaking tests occasionally. - return server.getTextDocumentService().definition(params).get(); + getServer().waitForReconcile(); //goto definitions relies on reconciler infos! Must wait or race condition breaking tests occasionally. + return getServer().getTextDocumentService().definition(params).get(); } public List getCodeActions(TextDocumentInfo doc, Diagnostic problem) throws Exception { CodeActionContext context = new CodeActionContext(ImmutableList.of(problem)); List actions = - server.getTextDocumentService().codeAction(new CodeActionParams(doc.getId(), problem.getRange(), context)).get(); + getServer().getTextDocumentService().codeAction(new CodeActionParams(doc.getId(), problem.getRange(), context)).get(); return actions.stream() .map((command) -> new CodeAction(this, command)) .collect(Collectors.toList()); @@ -498,7 +498,7 @@ public class LanguageServerHarness { //Note convert the params to a 'typeless' Object because that is more representative on how it will be // received when we get it in a real client/server setting (i.e. parsed from json). List untypedParams = mapper.convertValue(args, List.class); - server.getWorkspaceService() + getServer().getWorkspaceService() .executeCommand(new ExecuteCommandParams(command.getCommand(), untypedParams)) .get(); } @@ -544,9 +544,9 @@ public class LanguageServerHarness { } public List getDocumentSymbols(TextDocumentInfo document) throws Exception { - server.waitForReconcile(); //TODO: if the server works properly this shouldn't be needed it should do that internally itself somehow. + getServer().waitForReconcile(); //TODO: if the server works properly this shouldn't be needed it should do that internally itself somehow. DocumentSymbolParams params = new DocumentSymbolParams(document.getId()); - return server.getTextDocumentService().documentSymbol(params).get(); + return getServer().getTextDocumentService().documentSymbol(params).get(); } /** @@ -554,7 +554,7 @@ public class LanguageServerHarness { */ public SynchronizationPoint reconcilerThreadStart() { CompletableFuture blocker = new CompletableFuture<>(); - server.setTestListener(new LanguageServerTestListener() { + getServer().setTestListener(new LanguageServerTestListener() { @Override public void reconcileStarted(String uri, int version) { try { @@ -585,4 +585,8 @@ public class LanguageServerHarness { return newEditor(IOUtil.toString(is)); } } + + public S getServer() { + return server; + } } 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 f14a9c75e..71af2b38a 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 @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.util.MimeTypes; +import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParseException; @@ -170,11 +171,11 @@ public class PipelineYmlSchema implements YamlSchema { t_version.addHints("latest", "every"); t_resource_type_name = f.yenumFromHints("ResourceType Name", - (parseString, validValues) -> { + (dc) -> (parseString, validValues) -> { return "The '"+parseString+"' Resource Type does not exist. Existing types: "+validValues; }, (DynamicSchemaContext dc) -> { - return models.getResourceTypeNameHints(dc); + return PartialCollection.compute(() -> models.getResourceTypeNameHints(dc)); } ); @@ -190,8 +191,8 @@ public class PipelineYmlSchema implements YamlSchema { 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)); - return callable; + PartialCollection hints = PartialCollection.compute(() -> YTypeFactory.hints(models.getResourceNames(dc))); + return hints; }); t_maybe_resource_name.parseWith(ValueParsers.NE_STRING); diff --git a/vscode-extensions/vscode-bosh/README.md b/vscode-extensions/vscode-bosh/README.md index 7cc1d4061..0ba295497 100644 --- a/vscode-extensions/vscode-bosh/README.md +++ b/vscode-extensions/vscode-bosh/README.md @@ -3,35 +3,6 @@ This extension provides validation, content assist and documentation hovers for editing [Bosh](https://bosh.io/) Deployment Manifest files. -## Usage - -### Activating the Editor - -The Bosh editor automatically activates when the name of the `.yml` file you are editing -follows a certain pattern: - - - `**/*deployment*.yml` : activates support for bosh manifest file. - -You can also define your own patterns and map them to the language-id `bosh-deployment-manifest` -by defining `files.associations` in workspace settings. -See [vscode documentation](https://code.visualstudio.com/Docs/languages/overview#_adding-a-file-extension-to-a-language) for details. - -### Targetting a specific Director - -Some Validation and Content Assist use information dymanically retrieved from an active Bosh director. -For these feature to work it is required that you - -- have the bosh cli V2 installed (information is obtained by executing commands using the V2 cli) -- target a director by setting the `BOSH_ENVIROMENT` variable. - -You can verify that you have set things up right by executing command: - -``` -bosh cloud-config --json -``` - -If setup correctly, it should return information about the cloud-config on your intended bosh director/environment. - ## Functionality ### Validation @@ -85,13 +56,56 @@ When you use attributes from the V1 schema the editor will detect this however a In this mode, V1 properties are accepted but marked with deprecation warnings and V2 properties are marked as (unknown property) errors. +## Usage + +### Activating the Editor + +The Bosh editor automatically activates when the name of the `.yml` file you are editing +follows a certain pattern: + + - `**/*deployment*.yml` : activates support for bosh manifest file. + +You can also define your own patterns and map them to the language-id `bosh-deployment-manifest` +by defining `files.associations` in workspace settings. +See [vscode documentation](https://code.visualstudio.com/Docs/languages/overview#_adding-a-file-extension-to-a-language) for details. + +### Targetting a specific Director + +Some of the Validations and Content Assist depend on information dymanically retrieved from an active Bosh director. +The editor retreives information by executing commands using the Bosh CLI. For this to work the CLI (V2 +CLI is required) and editor have to be installed and configured correctly. + +There are two ways to set things up to make this work: + +#### Explicitly Configure the CLI: + +From vscode, press `CTRL-SHIFT-P` and type `Settings` then select either `Open User Settings` or `Open Workspace Settings`. +The bosh cli is configured by specifying keys of the form `bosh.cli.XXX`. Content assist and hover docs show you the +the available keys and their meaning. + +#### Implictly Configure the CLI: + +If the bosh cli is not explicitly configured it will, by default, try to execute commands like `bosh cloud-config --json` +and `bosh stemcells --json` without an explicit `-e ...` argument. This works only if you ensure that the editor +process executes with a proper set of OS environment variables: + +- `PATH`: must be set so that `bosh` cli executable can be found and refers to the V2 CLI. +- `BOSH_ENVIRONMENT`: must be set to point to the bosh director you want to target. + +If you start vscode from a terminal, you can verify that things are setup correctly by executing command: + + bosh cloud-config + +If that command executes without any errors and returns the cloud-config you expected, then things are setup correctly. +If subsequently launch vscode from that same terminal the dynamic CA and linting should work correctly. + ## Issues and Feature Requests Please report bugs, issues and feature requests on the [Github STS4 issue tracker](https://github.com/spring-projects/sts4/issues). -[linting]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/linting.png -[ca1]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png -[ca2]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png -[hovers]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/hover.png -[peek]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/peek.png -[goto_symbol]: https://raw.githubusercontent.com/spring-projects/sts4/master/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png +[linting]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/linting.png +[ca1]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png +[ca2]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png +[hovers]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/hover.png +[peek]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/peek.png +[goto_symbol]: https://raw.githubusercontent.com/spring-projects/sts4/7e3cf4808095f8b126bf1e5a90c09f3917f60fa4/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png diff --git a/vscode-extensions/vscode-bosh/lib/Main.ts b/vscode-extensions/vscode-bosh/lib/Main.ts index ce00b67e9..3f33a2182 100644 --- a/vscode-extensions/vscode-bosh/lib/Main.ts +++ b/vscode-extensions/vscode-bosh/lib/Main.ts @@ -36,7 +36,10 @@ export function activate(context: VSCode.ExtensionContext) { fatJarFile: 'jars/language-server.jar', jvmHeap: "48m", clientOptions: { - documentSelector: [ BOSH_DEPLOYMENT_LANGUAGE_ID ] + documentSelector: [ BOSH_DEPLOYMENT_LANGUAGE_ID ], + synchronize: { + configurationSection: "bosh" + } } }; let clientPromise = commons.activate(options, context); diff --git a/vscode-extensions/vscode-bosh/package.json b/vscode-extensions/vscode-bosh/package.json index 35ddf7045..4495cdddc 100644 --- a/vscode-extensions/vscode-bosh/package.json +++ b/vscode-extensions/vscode-bosh/package.json @@ -44,7 +44,28 @@ "scopeName": "source.yaml", "path": "./yaml-support/yaml.tmLanguage" } - ] + ], + "configuration": { + "type": "object", + "title": "Bosh CLI Configuration", + "properties": { + "bosh.cli.command": { + "type": ["string" , "null"], + "default": "bosh", + "description": "Path to an executable to launch the bosh cli V2. A V2 cli is required! Set this to null to completely disable all editor features that require access to the bosh director" + }, + "bosh.cli.target": { + "type": ["string", "null"], + "default": null, + "description": "Specifies the director/environment to target when executing bosh cli commands. I.e. this value is passed to the CLI via `-e` parameter." + }, + "bosh.cli.timeout": { + "type": "integer", + "default": 3, + "description": "Number of seconds before CLI commands are terminated with a timeout" + } + } + } }, "main": "./out/lib/Main", "scripts": { diff --git a/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png b/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png index c43f39bb6..e2e0c2ec8 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png and b/vscode-extensions/vscode-bosh/readme-imgs/content-assist-1.png differ diff --git a/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png b/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png index ff4e1c964..234c2b491 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png and b/vscode-extensions/vscode-bosh/readme-imgs/content-assist-2.png differ diff --git a/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png b/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png index a9e544665..e7ebe8681 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png and b/vscode-extensions/vscode-bosh/readme-imgs/goto-symbol.png differ diff --git a/vscode-extensions/vscode-bosh/readme-imgs/hover.png b/vscode-extensions/vscode-bosh/readme-imgs/hover.png index ca14b9d44..459936256 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/hover.png and b/vscode-extensions/vscode-bosh/readme-imgs/hover.png differ diff --git a/vscode-extensions/vscode-bosh/readme-imgs/linting.png b/vscode-extensions/vscode-bosh/readme-imgs/linting.png index 93860ed82..e6d9ff911 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/linting.png and b/vscode-extensions/vscode-bosh/readme-imgs/linting.png differ diff --git a/vscode-extensions/vscode-bosh/readme-imgs/peek.png b/vscode-extensions/vscode-bosh/readme-imgs/peek.png index c8e025510..2167faef6 100644 Binary files a/vscode-extensions/vscode-bosh/readme-imgs/peek.png and b/vscode-extensions/vscode-bosh/readme-imgs/peek.png differ