From 01edae7366b9c9c94bd8f227390e164596d9b561 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 11 Aug 2017 16:42:40 -0700 Subject: [PATCH] Missing property quickfix now uses snippet generator --- .../bosh/BoshDeploymentManifestSchema.java | 2 +- .../ide/vscode/bosh/BoshLanguageServer.java | 2 +- .../ide/vscode/bosh/BoshEditorTest.java | 47 +++++++++ .../bosh/SchemaBasedSnippetGeneratorTest.java | 2 +- .../util/PlaceHolderString.java | 96 +++++++++++++++++++ .../languageserver/util/SnippetBuilder.java | 30 +++++- .../yaml/completion/YTypeAssistContext.java | 2 +- .../yaml/completion/YamlPathEdits.java | 2 +- .../commons/yaml/quickfix/YamlQuickfixes.java | 25 +++-- .../yaml/reconcile/MissingPropertiesData.java | 47 ++++++++- .../SchemaBasedYamlASTReconciler.java | 23 +++-- .../yaml/reconcile/YamlSchemaProblems.java | 8 +- .../snippet}/SchemaBasedSnippetGenerator.java | 38 +++----- .../vscode/commons/yaml/snippet/Snippet.java | 13 ++- .../snippet/TypeBasedSnippetProvider.java | 8 +- .../commons/yaml/util/YamlIndentUtil.java | 6 ++ .../vscode/concourse/ConcourseEditorTest.java | 6 +- 17 files changed, 300 insertions(+), 57 deletions(-) create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java rename headless-services/{bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets => commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet}/SchemaBasedSnippetGenerator.java (82%) diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java index 37dc4035a..c0cd2bf07 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java @@ -26,7 +26,6 @@ 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; @@ -52,6 +51,7 @@ 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.YamlSchema; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; +import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java index 7256e2877..6991f9621 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java @@ -15,7 +15,6 @@ 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; @@ -37,6 +36,7 @@ import org.springframework.ide.vscode.commons.yaml.quickfix.YamlQuickfixes; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlSymbolHandler; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaBasedReconcileEngine; +import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; public class BoshLanguageServer extends SimpleLanguageServer { diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java index ed96cd463..f73163268 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java @@ -39,6 +39,7 @@ 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.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; @@ -2475,6 +2476,52 @@ public class BoshEditorTest { ); editor.assertContextualCompletions(PLAIN_COMPLETION, "<*>", "cloud_properties:\n <*>"); + } + @Test public void missingPropertiesQuickfix() throws Exception { + Editor editor = harness.newEditor( + "name: blah\n" + + "stemcells:\n" + + "- alias: ubuntu\n" + + " os: ubuntu-trusty\n" + + " version: 3421.11\n" + + "- alias: centos\n" + + " os: centos-7\n" + + " version: latest" + ); + Diagnostic problem = editor.assertProblems("t|Properties [instance_groups, releases, update] are required").get(0); + CodeAction quickfix = editor.assertCodeAction(problem); + assertEquals("Add properties: [instance_groups, releases, update]", quickfix.getLabel()); + quickfix.perform(); + editor.assertText( + "name: blah\n" + + "stemcells:\n" + + "- alias: ubuntu\n" + + " os: ubuntu-trusty\n" + + " version: 3421.11\n" + + "- alias: centos\n" + + " os: centos-7\n" + + " version: latest\n" + + "releases:\n" + + "- name: <*>\n" + + " version: \n" + + "update:\n" + + " canaries: \n" + + " max_in_flight: \n" + + " canary_watch_time: \n" + + " update_watch_time: \n" + + "instance_groups:\n" + + "- name: \n" + + " azs:\n" + + " - \n" + + " instances: \n" + + " jobs:\n" + + " - name: \n" + + " release: \n" + + " vm_type: \n" + + " stemcell: \n" + + " networks:\n" + + " - name: " + ); } } diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java index 71647b0b7..afdfcb212 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java @@ -14,11 +14,11 @@ import static org.junit.Assert.assertEquals; import org.junit.Test; import org.springframework.ide.vscode.bosh.models.BoshModels; -import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; 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; +import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; public class SchemaBasedSnippetGeneratorTest { diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java new file mode 100644 index 000000000..c79aa0615 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.languageserver.util; + +import java.util.Map; + +import org.springframework.ide.vscode.commons.util.CollectionUtil; +import org.springframework.ide.vscode.commons.util.text.IRegion; + +import com.google.common.collect.ImmutableMap; + +/** + * Represents a string with placeholder inside. Provides methods to retrieve + * the location of a placeholder given its 'id'. + */ +public class PlaceHolderString { + + public static class PlaceHolder { + public final Object id; // object used to identify the placeholder + public final IRegion location; + + public PlaceHolder(Object id, IRegion location) { + super(); + this.id = id; + this.location = location; + } + + public int getOffset() { + return location.getOffset(); + } + + public int getEnd() { + return location.getOffset() + location.getLength(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.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; + PlaceHolder other = (PlaceHolder) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + } + + private final ImmutableMap placeHolders; + private final String string; + + public PlaceHolderString(Map placeHolders, String string) { + super(); + this.placeHolders = ImmutableMap.copyOf(placeHolders); + this.string = string; + } + + + @Override + public String toString() { + if (placeHolders.size()==1) { + PlaceHolder placeHolder = CollectionUtil.getAny(placeHolders.values()); + if (string.length()==placeHolder.getEnd()) { + return string.substring(0, placeHolder.getOffset()); + } + } + return string; + } + + public PlaceHolder getPlaceHolder(Object id) { + return placeHolders.get(id); + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java index 26ca9aa35..5d5dc1aa7 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java @@ -10,11 +10,31 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.languageserver.util; +import java.util.HashMap; + +import org.springframework.ide.vscode.commons.languageserver.util.PlaceHolderString.PlaceHolder; +import org.springframework.ide.vscode.commons.util.text.Region; + public class SnippetBuilder { + /** + * Create a 'gimped' snippetbuilder which generates snippets without + * the '$' placeholders. This is useful to provide some snippet-like + * support in contexts that don't provide snippet support. + */ + public static SnippetBuilder gimped() { + return new SnippetBuilder() { + @Override + protected String createPlaceHolder(int id) { + return ""; + } + }; + } + private static final int FIRST_PLACE_HOLDER_ID = 1; private int nextPlaceHolderId = FIRST_PLACE_HOLDER_ID; private StringBuilder buf = new StringBuilder(); + private HashMap placeHolders = new HashMap<>(); public SnippetBuilder text(String text) { buf.append(text); @@ -25,7 +45,11 @@ public class SnippetBuilder { * Create a new `placeholder` and appends it to the snippet. */ public SnippetBuilder placeHolder() { - buf.append(createPlaceHolder(nextPlaceHolderId++)); + int offset = buf.length(); + int id = nextPlaceHolderId++; + buf.append(createPlaceHolder(id)); + int end = buf.length(); + placeHolders.put(id, new PlaceHolderString.PlaceHolder(id, new Region(offset, end-offset))); return this; } @@ -42,6 +66,10 @@ public class SnippetBuilder { return "$"+id; } + public PlaceHolderString build() { + return new PlaceHolderString(placeHolders, buf.toString()); + } + @Override public String toString() { String str = buf.toString(); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index 2b282318b..f0a801180 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -166,7 +166,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { DocumentEdits edits; if (snippetProvider!=null) { // Generate edits from snippet - Snippet snippet = snippetProvider.getSnippet(type, p); + Snippet snippet = snippetProvider.getSnippet(p); edits = createEditFromSnippet(doc, node, offset, query, indenter, snippet); } else { //Generate edits the old-fashioned way diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlPathEdits.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlPathEdits.java index 864446897..622a53bd6 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlPathEdits.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlPathEdits.java @@ -112,7 +112,7 @@ public class YamlPathEdits extends DocumentEdits { return buf.toString(); } - private int getNewPathInsertionOffset(SChildBearingNode parent) throws Exception { + public int getNewPathInsertionOffset(SChildBearingNode parent) throws Exception { int insertAfterLine = doc.getLineOfOffset(parent.getTreeEnd()); while (insertAfterLine>=0 && doc.getLineIndentation(insertAfterLine)==-1) { insertAfterLine--; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java index 9fa63297d..cb936ef0f 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java @@ -30,6 +30,7 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SChildBearingNode; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; +import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; @@ -58,11 +59,22 @@ public class YamlQuickfixes { YamlPath path = YamlPath.decode(params.getPath()); SNode _target = path.traverse(root); if (_target instanceof SChildBearingNode) { - YamlPathEdits edits = new YamlPathEdits(doc); + YamlIndentUtil indenter = new YamlIndentUtil(doc); SChildBearingNode target = (SChildBearingNode) _target; - for (String prop : params.getProps()) { - edits.createPath(target, new YamlPath(YamlPathSegment.valueAt(prop)), " "); - edits.freezeCursor(); + YamlPathEdits edits = new YamlPathEdits(doc); + int insertAt = edits.getNewPathInsertionOffset(target); + int indentBy = YamlIndentUtil.getNewChildKeyIndent(target); + boolean first = true; + String propSnippet = params.getSnippet(); + int cursorOffset = params.getCursorOffset(); + { + edits.insert(insertAt, indenter.newlineWithIndent(indentBy)); + edits.insert(insertAt, indenter.applyIndentation(propSnippet.substring(0,cursorOffset), indentBy)); + if (first) { + edits.freezeCursor(); + first = false; + } + edits.insert(insertAt, indenter.applyIndentation(propSnippet.substring(cursorOffset), indentBy)); } TextReplace replaceEdit = edits.asReplacement(_doc); if (replaceEdit!=null) { @@ -94,14 +106,13 @@ public class YamlQuickfixes { ImmutableMap.of(params.getUri(), ImmutableList.of(params.getEdit())), null ), - null //TODO: compute end of the range after applying the edit + null //TODO: compute end of the range after applying the edit ); } } catch (Exception e) { Log.log(e); } //Something went wrong. Return empty edit object. - //Something went wrong. Return empty edit object. return NULL_FIX; }); } @@ -113,7 +124,7 @@ public class YamlQuickfixes { //There is probably a more efficient way to compute the new cursor position. But its tricky... //... because we need to compute line/char coordinate, in terms of lines in the *new* document. //So we have to take into account how newlines have been inserted or shifted around by the edits. - //Doing that without actually applying the edits is... difficult. + //Doing that without actually applying the edits is... difficult. TextDocument doc = _doc.copy(); edits.apply(doc); return doc.toPosition(newSelection.getOffset()); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/MissingPropertiesData.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/MissingPropertiesData.java index d3d41ee1f..29c99780f 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/MissingPropertiesData.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/MissingPropertiesData.java @@ -12,20 +12,43 @@ package org.springframework.ide.vscode.commons.yaml.reconcile; import java.util.List; +import com.google.common.collect.ImmutableList; + public class MissingPropertiesData { private String uri; + + /** + * Yaml path (encoded segments) pointing to the node where + * the missing properties should be added (as children of that node). + */ private List path; + + /** + * The properties that are missing and should be added. + */ private List props; - public MissingPropertiesData(String uri, List path, List props) { + /** + * Snippet to insert when applying the quickfix. + */ + private String snippet; + + /** + * Offset where to place cursor relative to snippet start. + */ + private int cursorOffset; + + public MissingPropertiesData() { + } + + public MissingPropertiesData(String uri, List path, List props, String snippet, int cursorOffset) { super(); this.uri = uri; this.path = path; this.props = props; - } - - public MissingPropertiesData() { + this.snippet = snippet; + this.cursorOffset = cursorOffset; } public String getUri() { @@ -47,8 +70,24 @@ public class MissingPropertiesData { this.props = props; } + public String getSnippet() { + return snippet; + } + + public void setSnippet(String snippet) { + this.snippet = snippet; + } + @Override public String toString() { return "MissingPropertiesData [uri=" + uri + ", path=" + path + ", props=" + props + "]"; } + + public int getCursorOffset() { + return cursorOffset; + } + + public void setCursorOffset(int cursorOffset) { + this.cursorOffset = cursorOffset; + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index 910c8ff29..e78013f14 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -31,6 +31,8 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.Reconcile import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReplacementQuickfix; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; +import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.ExceptionUtil; import org.springframework.ide.vscode.commons.util.IntegerRange; import org.springframework.ide.vscode.commons.util.Log; @@ -51,6 +53,9 @@ 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.YamlSchema; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; +import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; @@ -285,21 +290,25 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { //Don't check for missing properties if some properties look like they might be spelled incorrectly. if (allPropertiesKnown) { //Check for missing required properties: - Set missingProps = beanProperties.values().stream() + List missingProps = beanProperties.values().stream() .filter(YTypedProperty::isRequired) + .filter(prop -> !foundProps.contains(prop.getName())) + .collect(CollectorUtil.toImmutableList()); + Set missingPropNames = missingProps.stream() .map(YTypedProperty::getName) - .filter((required) -> !foundProps.contains(required)) .collect(Collectors.toCollection(TreeSet::new)); - if (!missingProps.isEmpty()) { + if (!missingPropNames.isEmpty()) { String message; - if (missingProps.size()==1) { + if (missingPropNames.size()==1) { // slightly more specific message when only one missing property - String missing = missingProps.stream().findFirst().get(); + String missing = missingPropNames.stream().findFirst().get(); message = "Property '"+missing+"' is required for '"+type+"'"; } else { - message = "Properties "+missingProps+" are required for '"+type+"'"; + message = "Properties "+missingPropNames+" are required for '"+type+"'"; } - problems.accept(YamlSchemaProblems.missingProperties(message, dc, missingProps, parent, map, quickfixes.MISSING_PROP_FIX)); + SchemaBasedSnippetGenerator snippetProvider = new SchemaBasedSnippetGenerator(typeUtil, SnippetBuilder::gimped); + Snippet snippet = snippetProvider.getSnippet(missingProps); + problems.accept(YamlSchemaProblems.missingProperties(message, dc, missingPropNames, snippet.getSnippet(), snippet.getPlaceHolder(1).getOffset(), parent, map, quickfixes.MISSING_PROP_FIX)); } } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java index c46c7f2cb..5742370a6 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java @@ -23,6 +23,7 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.Reconcile import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; import org.springframework.ide.vscode.commons.util.CollectionUtil; +import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; @@ -30,6 +31,7 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; 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.YTypedProperty; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; import org.yaml.snakeyaml.error.Mark; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; @@ -138,7 +140,7 @@ public class YamlSchemaProblems { return problem(MISSING_PROPERTY, msg, underline); } - public static ReconcileProblem missingProperties(String msg, DynamicSchemaContext dc, Set missingProps, Node parent, MappingNode map, QuickfixType quickfixType) { + public static ReconcileProblem missingProperties(String msg, DynamicSchemaContext dc, Set missingProps, String snippet, int cursorOffset, Node parent, MappingNode map, QuickfixType quickfixType) { YamlPath contextPath = dc.getPath(); List segments = Stream.of(contextPath.getSegments()) .map(YamlPathSegment::encode) @@ -152,7 +154,9 @@ public class YamlSchemaProblems { new MissingPropertiesData( dc.getDocument().getUri(), segments, - ImmutableList.copyOf(missingProps) + ImmutableList.copyOf(missingProps), + snippet, + cursorOffset ), fixTitle ); diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/SchemaBasedSnippetGenerator.java similarity index 82% rename from headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java rename to headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/SchemaBasedSnippetGenerator.java index 89c99f30f..b4b00431f 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/SchemaBasedSnippetGenerator.java @@ -8,26 +8,21 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.bosh.snippets; +package org.springframework.ide.vscode.commons.yaml.snippet; 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.PlaceHolderString; 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; /** @@ -45,19 +40,11 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { this.snippetBuilderFactory = snippetBuilderFactory; } - private Cache> cache = CacheBuilder.newBuilder() - .weakKeys() - .build(); private int maxNesting = Integer.MAX_VALUE; @Override public Collection getSnippets(YType type) { - try { - return cache.get(type, () -> generateSnippets(type)); - } catch (ExecutionException e) { - Log.log(e); - return ImmutableList.of(); - } + return generateSnippets(type); } private Collection generateSnippets(YType type) { @@ -76,7 +63,7 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { generateBeanSnippet(requiredProps, builder, indent, maxNesting); } if (builder.getPlaceholderCount()>=2) { - return new Snippet(typeUtil.niceTypeName(type)+" Snippet", builder.toString(), (dc) -> + return new Snippet(typeUtil.niceTypeName(type)+" Snippet", builder.build(), (dc) -> requiredProps.stream().noneMatch(p -> dc.getDefinedProperties().contains(p.getName())) ); } @@ -103,16 +90,21 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { } @Override - public Snippet getSnippet(YType contextType, YTypedProperty p) { - //TODO: cache? + public Snippet getSnippet(List props) { SnippetBuilder builder = snippetBuilderFactory.get(); - generateBeanSnippet(ImmutableList.of(p), builder, 0, maxNesting); - String propName = p.getName(); - return new Snippet(propName, builder.toString(), (dc) -> - !dc.getDefinedProperties().contains(propName) + generateBeanSnippet(props, builder, 0, maxNesting); + String snippetName; + if (props.size()==1) { + snippetName = props.get(0).getName(); + } else { + snippetName = props.stream().map(p -> p.getName()).collect(Collectors.toList()).toString() + " Snippet"; + } + return new Snippet(snippetName, builder.build(), (dc) -> + props.stream().allMatch(p -> !dc.getDefinedProperties().contains(p.getName())) ); } + private void generateNestedSnippet(boolean parentIsSeq, YType type, SnippetBuilder builder, int indent, int nestingLimit) { if (type==null) { //Assume its some kind of pojo bean diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java index 11acd5aeb..5c65cfba2 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java @@ -12,16 +12,18 @@ package org.springframework.ide.vscode.commons.yaml.snippet; import java.util.function.Predicate; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.languageserver.util.PlaceHolderString; +import org.springframework.ide.vscode.commons.languageserver.util.PlaceHolderString.PlaceHolder; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; -import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; public class Snippet { private final String name; - private final String snippet; + private final PlaceHolderString snippet; private final Predicate applicability; - public Snippet(String name, String snippet, Predicate applicability) { + public Snippet(String name, PlaceHolderString snippet, Predicate applicability) { super(); this.name = name; this.snippet = snippet; @@ -31,7 +33,7 @@ public class Snippet { return name; } public String getSnippet() { - return snippet; + return snippet.toString(); } @Override @@ -44,4 +46,7 @@ public class Snippet { public boolean isApplicable(DynamicSchemaContext dc) { return applicability==null || applicability.test(dc); } + public PlaceHolder getPlaceHolder(Object id) { + return snippet.getPlaceHolder(id); + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java index 49acad072..7647ab643 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java @@ -11,13 +11,19 @@ package org.springframework.ide.vscode.commons.yaml.snippet; import java.util.Collection; +import java.util.List; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; +import com.google.common.collect.ImmutableList; + public interface TypeBasedSnippetProvider { Collection getSnippets(YType contextType); - Snippet getSnippet(YType contextType, YTypedProperty p); + default Snippet getSnippet(YTypedProperty p) { + return getSnippet(ImmutableList.of(p)); + } + Snippet getSnippet(List props); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java index a358489d3..0b2c8b406 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java @@ -84,6 +84,12 @@ public class YamlIndentUtil { return buf.toString(); } + public String indentString(int indent) { + StringBuilder buf = new StringBuilder(); + addIndent(indent, buf); + return buf.toString(); + } + /** * Applies a certain level of indentation to all new lines in the given text. Newlines * are expressed by '\n' characters in the text will be replaced by the appropriate diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 4c1221d9c..456c59b44 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -100,9 +100,9 @@ public class ConcourseEditorTest { " type: pool\n" + " source:\n" + " username: someone\n" + - " branch: <*>\n" + - " pool: \n" + - " uri: \n" + " uri: <*>\n" + + " branch: \n" + + " pool: \n" ); }