Merge branch 'master' into async_validations

Conflicts:
	headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java
This commit is contained in:
nsingh
2017-08-02 12:16:33 -07:00
55 changed files with 1837 additions and 408 deletions

View File

@@ -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')
);

View File

@@ -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",

View File

@@ -52,8 +52,8 @@ public class BootJavaHoverProvider implements HoverHandler {
@Override
public CompletableFuture<Hover> 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<Hover> 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<Hover> 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<Path> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<CloudConfigModel> cloudConfigProvider;
private DynamicModelProvider<StemcellsModel> stemcellsProvider;
private DynamicModelProvider<ReleasesModel> releasesProvider;
private List<Pair<YType, YType>> defAndRefTypes;
public BoshDeploymentManifestSchema(
YamlAstCache asts, ASTTypeCache astTypes,
DynamicModelProvider<CloudConfigModel> cloudConfigProvider,
DynamicModelProvider<StemcellsModel> stemcellsProvider
DynamicModelProvider<StemcellsModel> stemcellsProvider,
DynamicModelProvider<ReleasesModel> 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<String> 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<ReleaseData> 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<YType>() {
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<StemcellData> 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<StemcellData> 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<StemcellData> 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;

View File

@@ -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<CloudConfigModel> cloudConfigProvider, DynamicModelProvider<StemcellsModel> stemcellsProvider) {
public BoshLanguageServer(BoshCliConfig cliConfig,
DynamicModelProvider<CloudConfigModel> cloudConfigProvider,
DynamicModelProvider<StemcellsModel> stemcellsProvider,
DynamicModelProvider<ReleasesModel> 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;

View File

@@ -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)
));
}
}

View File

@@ -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<T> 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<T> implements DynamicModelPr
return blocks[0];
}
protected final ExternalCommand getCommand() {
List<String> 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<T> 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<T> 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<T> implements DynamicModelPr
YamlFileAST ast = yamlParser.getAST(doc);
return ast;
}
protected Collection<String> 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());
}
}

View File

@@ -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<CloudConfigModel> {
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"};
}
}

View File

@@ -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<ReleasesModel> {
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<ReleaseData> 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<String> getReleaseNames() {
return getNames(cursor, RELEASE_NAMES);
}
@Override
public Collection<String> getVersions() {
return getNames(cursor, RELEASE_VERSIONS);
}
};
}
@Override
protected String[] getBoshCommand() {
return COMMAND;
}
}

View File

@@ -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<String> getStemcellNames() {
return getNames(STEMCELL_NAMES);
}
private Collection<String> 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<String> getStemcellOss() {
return getNames(STEMCELL_OSS);
return getNames(cursor, STEMCELL_OSS);
}
@Override
public Collection<String> 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" };
}
}

View File

@@ -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<T> implements DynamicModelProvider<T> {
private Cache<Object, CompletableFuture<T>> cache = createCache();
private final DynamicModelProvider<T> delegate;
private Class<T> modelInterface;
public CachingModelProvider(DynamicModelProvider<T> delegate) {
public CachingModelProvider(DynamicModelProvider<T> delegate, Class<T> modelInterface) {
this.delegate = delegate;
this.modelInterface = modelInterface;
}
/**
@@ -72,15 +75,43 @@ public class CachingModelProvider<T> implements DynamicModelProvider<T> {
synchronized (this) {
cached = cache.get(key, () -> {
try {
return CompletableFuture.completedFuture(delegate.getModel(dc));
return CompletableFuture.completedFuture(wrapWithCachingProxy(delegate.getModel(dc)));
} catch (Throwable e) {
CompletableFuture<T> failed = new CompletableFuture<>();
failed.completeExceptionally(e);
return failed;
return failed(e);
}
});
}
return cached.get();
}
private static <T> CompletableFuture<T> failed(Throwable e) {
CompletableFuture<T> 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<String, CompletableFuture<Object>> 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);
});
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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<ReleaseData> getReleases();
Collection<String> getReleaseNames();
Collection<String> getVersions();
}

View File

@@ -21,6 +21,16 @@ import org.yaml.snakeyaml.nodes.Node;
*/
public class StemcellModel {
public static final Predicate<StemcellData> ALLWAYS_TRUE_FILTER = new Predicate<StemcellData>() {
@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<StemcellData>() {
@Override
public boolean test(StemcellData sc) {
return true;
}
@Override
public String toString() {
return "true";
}
};
return ALLWAYS_TRUE_FILTER;
}
}

View File

@@ -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<SnippetBuilder> snippetBuilderFactory;
public SchemaBasedSnippetGenerator(YTypeUtil typeUtil, Supplier<SnippetBuilder> snippetBuilderFactory) {
super();
this.typeUtil = typeUtil;
this.snippetBuilderFactory = snippetBuilderFactory;
}
private Cache<YType, Collection<Snippet>> cache = CacheBuilder.newBuilder().build();
private int maxNesting = Integer.MAX_VALUE;
@Override
public Collection<Snippet> getSnippets(YType type) {
try {
return cache.get(type, () -> generateSnippets(type));
} catch (ExecutionException e) {
Log.log(e);
return ImmutableList.of();
}
}
private Collection<Snippet> generateSnippets(YType type) {
ImmutableList.Builder<Snippet> 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<YTypedProperty> 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<YTypedProperty> 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<YTypedProperty> 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();
}
}
}

View File

@@ -1 +1 @@
The SHA1 of the release tarball. SHA1 is only required when using HTTP(s) URLs.
The SHA1 of the release tarball. SHA1 is recommended when using HTTP(s) URLs.

View File

@@ -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<BoshLanguageServer> harness;
private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider();
private DynamicModelProvider<StemcellsModel> stemcellsProvider = Mockito.mock(DynamicModelProvider.class);
private BoshCliConfig cliConfig = new BoshCliConfig();
private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider(cliConfig);
private DynamicModelProvider<StemcellsModel> stemcellsProvider = mock(DynamicModelProvider.class);
private DynamicModelProvider<ReleasesModel> 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<BoshLanguageServer>(() -> {
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<CompletionItem> 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<StemcellsModel> 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<ReleasesModel> 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: <*>"
);
}
}

View File

@@ -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));
}

View File

@@ -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());
}
}

View File

@@ -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<String> 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.

View File

@@ -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();

View File

@@ -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()
);
}
}

View File

@@ -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<String, Object> 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<String, Object> 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")));
}
}

View File

@@ -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<String> modelProvider = mock(DynamicModelProvider.class);
when(modelProvider.getModel(any())).thenReturn("RESULT");
DynamicModelProvider<BoxModel> modelProvider = mock(DynamicModelProvider.class);
BoxModel model = mock(BoxModel.class);
when(modelProvider.getModel(any())).thenReturn(model);
when(model.getContents()).thenReturn("RESULT");
DynamicModelProvider<String> cached = new CachingModelProvider<>(modelProvider);
DynamicModelProvider<BoxModel> 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<String> modelProvider = mock(DynamicModelProvider.class);
when(modelProvider.getModel(any())).thenThrow(new TimeoutException("timed out"));
DynamicModelProvider<String> cached = new CachingModelProvider<>(modelProvider);
DynamicModelProvider<String> cached = new CachingModelProvider<>(modelProvider, String.class);
for (int i = 0; i < 3; i++) {
try {
cached.getModel(null);

View File

@@ -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"
]
}

View File

@@ -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

View File

@@ -117,8 +117,8 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
private Mono<CompletionList> 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.

View File

@@ -52,31 +52,33 @@ public class SimpleDefinitionFinder<T extends SimpleLanguageServer> implements D
* currently pointed at in the current document using String.indexOf.
*/
protected Flux<Location> 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);

View File

@@ -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();
}
}

View File

@@ -35,11 +35,7 @@ public class SnippetBuilder {
* be overridden by subclasses to support other formats.
* <p>
* The default implementation creates place holder strings that
* match the undocumented format vscode currently supports.
* <p>
* Note: this format is explicitly different from what the LSP
* specifies. So it is very likely we should change this implementation
* in the near future.
* 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;
}
}

View File

@@ -26,8 +26,10 @@ import com.google.common.collect.ImmutableSet;
public class EnumValueParser implements ValueParser {
private String typeName;
private Provider<Collection<String>> values;
private final boolean longRunning;
private Provider<PartialCollection<String>> 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 <T> Provider<PartialCollection<T>> provider(Collection<T> values) {
return () -> PartialCollection.compute(() -> values);
}
private static <T> Provider<PartialCollection<T>> provider(Callable<Collection<T>> values) {
return () -> PartialCollection.compute(() -> values.call());
}
public EnumValueParser(String typeName, boolean longRunning, Callable<Collection<String>> values) {
this(typeName, longRunning, provider(values));
}
public EnumValueParser(String typeName, boolean longRunning, Provider<Collection<String>> values) {
public EnumValueParser(String typeName, boolean longRunning, Provider<PartialCollection<String>> values) {
this.typeName = typeName;
this.values = values;
this.longRunning = longRunning;
}
public EnumValueParser(String name, PartialCollection<String> 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<String> values = this.values.get();
PartialCollection<String> 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 <T> Provider<T> provider(T values) {
return () -> values;
}
private static <T> Provider<T> provider(Callable<T> values) {
return () -> {
try {
return values.call();
} catch (Exception e) {
// Ignore
return null;
}
};
}
public boolean longRunning() {
return this.longRunning ;

View File

@@ -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;
}
}

View File

@@ -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.
* <p>
* For unknown collection, an optional explanation, in the
* form of a caught exception may be stored as well.
*/
public class PartialCollection<T> {
private static final PartialCollection<?> UNKNOWN = new PartialCollection<>(ImmutableSet.of(), false);
private static final PartialCollection<?> EMPTY = new PartialCollection<>(ImmutableSet.of(), true);
final private ImmutableCollection<T> knownElements;
final private boolean isComplete;
final private Throwable explanation;
private PartialCollection(ImmutableCollection<T> knownElements, boolean isComplete, Throwable error) {
this.knownElements = knownElements;
this.isComplete = isComplete;
this.explanation = error;
}
private PartialCollection(ImmutableCollection<T> knownElements, boolean isComplete) {
this.knownElements = knownElements;
this.isComplete = isComplete;
this.explanation = null;
}
private PartialCollection(ImmutableCollection<T> 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 <T> PartialCollection<T> compute(Callable<Collection<T>> computer) {
try {
Collection<T> 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 <T> PartialCollection<T> fromCallable(Callable<PartialCollection<T>> 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<T> 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 <T> PartialCollection<T> unknown() {
return (PartialCollection<T>) UNKNOWN;
}
/**
* Like map on streams, but silently drops any null elements returned by the mapper.
*/
public <R> PartialCollection<R> map(Function<? super T, ? extends R> mapper) {
ImmutableSet<R> mappedElements = getElements().stream().map((x) -> mapper.apply(x)).filter(x -> x!=null).collect(CollectorUtil.toImmutableSet());
return new PartialCollection<R>(mappedElements, isComplete, explanation);
}
/**
* Returns a empty collection (i.e. the collection is know to be empty).
*/
@SuppressWarnings("unchecked")
public static <T> PartialCollection<T> empty() {
return (PartialCollection<T>) EMPTY;
}
/**
* Make a copy of this collection that has the same known elements but also has unknown elements.
*/
public PartialCollection<T> 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 <T> PartialCollection<T> unknown(Exception e) {
Assert.isLegal(e!=null);
return new PartialCollection<>(ImmutableSet.of(), e);
}
public PartialCollection<T> addAll(Collection<T> moreElements) {
ImmutableSet.Builder<T> elements = ImmutableSet.builder();
elements.addAll(getElements());
elements.addAll(moreElements);
return new PartialCollection<>(elements.build(), isComplete, explanation);
}
public Throwable getExplanation() {
return explanation;
}
public PartialCollection<T> add(@SuppressWarnings("unchecked") T... values) {
return addAll(Arrays.asList(values));
}
}

View File

@@ -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<newVersion);
for (TextDocumentContentChangeEvent change : params.getContentChanges()) {
apply(change);
if (version<newVersion) {
for (TextDocumentContentChangeEvent change : params.getContentChanges()) {
apply(change);
}
this.version = newVersion;
} else {
Log.warn("Change event with bad version ignored: "+params);
}
this.version = newVersion;
}
/**

View File

@@ -10,7 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.completion;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DASH_PROPOSAL;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DEPRECATION;
import java.util.ArrayList;
import java.util.Collection;
@@ -30,9 +31,9 @@ import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
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.ValueParseException;
import org.springframework.ide.vscode.commons.yaml.completion.DefaultCompletionFactory.ValueProposal;
import org.springframework.ide.vscode.commons.yaml.hover.YPropertyInfoTemplates;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
@@ -44,6 +45,8 @@ 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.schema.YValueHint;
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.structure.YamlDocument;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
@@ -97,6 +100,28 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
List<ICompletionProposal> completions = getValueCompletions(doc, node, offset, query);
if (completions.isEmpty()) {
completions = getKeyCompletions(doc, offset, query);
TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider();
if (snippetProvider!=null) {
Collection<Snippet> 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<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception {
int queryOffset = offset - query.length();
SNode contextNode = getContextNode();
DynamicSchemaContext dynamicCtxt = getSchemaContext();
List<YTypedProperty> allProperties = typeUtil.getProperties(type);
if (CollectionUtil.hasElements(allProperties)) {
@@ -115,6 +139,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
Set<String> definedProps = dynamicCtxt.getDefinedProperties();
List<ICompletionProposal> proposals = new ArrayList<>();
boolean suggestDeprecated = typeUtil.suggestDeprecatedProperties();
YamlIndentUtil indenter = new YamlIndentUtil(doc);
for (List<YTypedProperty> thisTier : tieredProperties) {
List<YTypedProperty> 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<ICompletionProposal> 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<YValueHint> _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<YValueHint> values = _values.getElements();
if (values!=null) {
ArrayList<ICompletionProposal> 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);

View File

@@ -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<? extends ICompletionProposal> 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<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> completions, SNode currentNode,
protected Collection<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> 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<SNode> 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<SNode> 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);
}

View File

@@ -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; }

View File

@@ -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<YValueHint> 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<YTypedProperty> propertyList = new ArrayList<>();
private List<YValueHint> hints = new ArrayList<>();
private Map<String, YTypedProperty> cachedPropertyMap;
private SchemaContextAware<Callable<Collection<YValueHint>>> hintProvider;
private SchemaContextAware<PartialCollection<YValueHint>> hintProvider;
//TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware<Collection<YValueHint>>
private List<Constraint> constraints = new ArrayList<>(2);
@@ -293,47 +300,19 @@ public class YTypeFactory {
}
public AbstractType setHintProvider(Callable<Collection<YValueHint>> hintProvider) {
setHintProvider((DynamicSchemaContext dc) -> hintProvider);
setHintProvider((DynamicSchemaContext dc) -> PartialCollection.compute(hintProvider));
return this;
}
public AbstractType setHintProvider(SchemaContextAware<Callable<Collection<YValueHint>>> hintProvider) {
public AbstractType setHintProvider(SchemaContextAware<PartialCollection<YValueHint>> hintProvider) {
//TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware<Collection<YValueHint>>
this.hintProvider = hintProvider;
return this;
}
public YValueHint[] getHintValues(DynamicSchemaContext dc) throws Exception {
Collection<YValueHint> 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<YValueHint> 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<YValueHint> getHintValues(DynamicSchemaContext dc) {
return getProviderHints(dc)
.addAll(hints);
}
/**
@@ -343,14 +322,15 @@ public class YTypeFactory {
hints = ImmutableList.copyOf(hints);
}
private Collection<YValueHint> getProviderHints(DynamicSchemaContext dc) throws Exception {
private PartialCollection<YValueHint> getProviderHints(DynamicSchemaContext dc) {
if (hintProvider != null) {
Callable<Collection<YValueHint>> 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<Constraint> getConstraints() {
@@ -417,7 +397,7 @@ public class YTypeFactory {
parseWith((DynamicSchemaContext dc) -> parser);
return this;
}
private SchemaContextAware<ValueParser> getParser() {
public SchemaContextAware<ValueParser> getParser() {
return parser;
}
@@ -878,25 +858,41 @@ public class YTypeFactory {
return ((YTypedPropertyImpl)prop).copy();
}
public YAtomicType yenumFromHints(String name, BiFunction<String, Collection<String>, String> errorMessageFormatter, SchemaContextAware<Collection<YValueHint>> values) {
public YAtomicType yenumFromHints(String name, SchemaContextAware<BiFunction<String, Collection<String>, String>> errorMessageFormatter, SchemaContextAware<PartialCollection<YValueHint>> values) {
YAtomicType t = yatomic(name);
t.setHintProvider((dc) -> () -> values.withContext(dc));
t.setHintProvider(values);
t.parseWith((DynamicSchemaContext dc) -> {
Collection<String> strings = YTypeFactory.values(values.withContext(dc));
return new EnumValueParser(name, strings) {
PartialCollection<YValueHint> hints = PartialCollection.fromCallable(() -> values.withContext(dc));
return new EnumValueParser(name, hints.map(h -> h.getValue())) {
@Override
protected String createErrorMessage(String parseString, Collection<String> 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<Collection<String>> values) {
public YAtomicType yenumFromDynamicValues(String name,
SchemaContextAware<BiFunction<String, Collection<String>, String>> errorMessageFormatter,
SchemaContextAware<PartialCollection<String>> 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<PartialCollection<String>> 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, Collection<String>, String> errorMessageFormatter, SchemaContextAware<Collection<String>> values) {
public YAtomicType yenum(String name, SchemaContextAware<BiFunction<String, Collection<String>, String>> errorMessageFormatter, SchemaContextAware<Collection<String>> values) {
YAtomicType t = yatomic(name);
t.setHintProvider((dc) -> {
Collection<String> 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<String> 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, Collection<String>, String> errorMessageFormatter, SchemaContextAware<Collection<String>> values) {
return yenum(name, (dc) -> errorMessageFormatter, values);
}
public static Collection<String> values(Collection<YValueHint> 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<YValueHint> hints(Collection<String> values) {
return values.stream()
.map(YTypeFactory::hint)
.collect(CollectorUtil.toMultiset());
}
public static PartialCollection<YValueHint> hints(PartialCollection<String> 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;
}
}

View File

@@ -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<YValueHint> getHintValues(YType yType, DynamicSchemaContext dc);
String niceTypeName(YType type);
YType getKeyType(YType type);
SchemaContextAware<ValueParser> getValueParser(YType type);
@@ -47,9 +51,17 @@ public interface YTypeUtil {
*/
YType inferMoreSpecificType(YType type, DynamicSchemaContext dc);
List<Constraint> 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();

View File

@@ -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<DynamicSchemaContext> applicability;
public Snippet(String name, String snippet, Predicate<DynamicSchemaContext> 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<DynamicSchemaContext> getApplicability() {
return applicability;
}
public boolean isApplicable(DynamicSchemaContext dc) {
return applicability==null || applicability.test(dc);
}
}

View File

@@ -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<Snippet> getSnippets(YType contextType);
}

View File

@@ -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.

View File

@@ -63,6 +63,7 @@ public class Editor {
public static final Predicate<CompletionItem> PLAIN_COMPLETION = c -> !RELAXED_COMPLETION.test(c);
public static final Predicate<CompletionItem> DEDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.LEFT_ARROW+" ");
public static final Predicate<CompletionItem> INDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" ");
public static final Predicate<CompletionItem> SNIPPET_COMPLETION = c -> c.getLabel().endsWith("Snippet");
static class EditorState {
String documentContents;
@@ -327,6 +328,10 @@ public class Editor {
}
public List<CompletionItem> assertCompletionLabels(String... expectedLabels) throws Exception {
return assertCompletionLabels(c -> true, expectedLabels);
}
public List<CompletionItem> assertCompletionLabels(Predicate<CompletionItem> 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<CompletionItem> 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<referenceIndent.length()) {
referenceIndent = referenceIndent.substring(0, cursorOffset);
}
replaceWith = replaceWith.replaceAll("\\n", "\n"+referenceIndent);
int cursorReplaceOffset = 0;
int cursorReplaceOffset = replaceWith.indexOf(VS_CODE_CURSOR_MARKER);
if (cursorReplaceOffset>=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<referenceIndent.length()) {
referenceIndent = referenceIndent.substring(0, cursorOffset);
}
replaceWith = replaceWith.replaceAll("\\n", "\n"+referenceIndent);
}
// Replace the cursor string
cursorReplaceOffset = replaceWith.indexOf(VS_CODE_CURSOR_MARKER);
if (cursorReplaceOffset >= 0) {
replaceWith = replaceWith.substring(0, cursorReplaceOffset)
+ replaceWith.substring(cursorReplaceOffset + VS_CODE_CURSOR_MARKER.length());
} else {
cursorReplaceOffset = replaceWith.length();
}

View File

@@ -92,16 +92,16 @@ import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Mono;
public class LanguageServerHarness {
public class LanguageServerHarness<S extends SimpleLanguageServer> {
//Warning this 'harness' is incomplete. Growing it as needed.
private Random random = new Random();
private Callable<? extends SimpleLanguageServer> factory;
private Callable<S> factory;
private LanguageId defaultLanguageId;
private SimpleLanguageServer server;
private S server;
private InitializeResult initResult;
@@ -110,12 +110,12 @@ public class LanguageServerHarness {
private List<Editor> activeEditors = new ArrayList<>();
public LanguageServerHarness(Callable<? extends SimpleLanguageServer> factory, LanguageId defaultLanguageId) {
public LanguageServerHarness(Callable<S> factory, LanguageId defaultLanguageId) {
this.factory = factory;
this.defaultLanguageId = defaultLanguageId;
}
public LanguageServerHarness(Callable<? extends SimpleLanguageServer> factory) throws Exception {
public LanguageServerHarness(Callable<S> 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<List<CompletionItem>, CompletionList> completions = server.getTextDocumentService().completion(params).get();
getServer().waitForReconcile();
Either<List<CompletionItem>, CompletionList> completions = getServer().getTextDocumentService().completion(params).get();
if (completions.isLeft()) {
List<CompletionItem> 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<? extends Location> 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<CodeAction> getCodeActions(TextDocumentInfo doc, Diagnostic problem) throws Exception {
CodeActionContext context = new CodeActionContext(ImmutableList.of(problem));
List<? extends Command> 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<? extends SymbolInformation> 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<Void> 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;
}
}

View File

@@ -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<Collection<YValueHint>> callable = () -> YTypeFactory.hints(models.getResourceNames(dc));
return callable;
PartialCollection<YValueHint> hints = PartialCollection.compute(() -> YTypeFactory.hints(models.getResourceNames(dc)));
return hints;
});
t_maybe_resource_name.parseWith(ValueParsers.NE_STRING);

View File

@@ -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

View File

@@ -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);

View File

@@ -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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 102 KiB