diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java index 47d19606a..746326fc7 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java @@ -16,6 +16,7 @@ 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; @@ -84,17 +85,13 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { SnippetBuilder builder = snippetBuilderFactory.get(); List requiredProps = typeUtil.getProperties(type).stream() .filter(p -> p.isPrimary() || p.isRequired()) - .collect(Collectors.toList()); + .collect(CollectorUtil.toImmutableList()); if (!requiredProps.isEmpty()) { generateBeanSnippet(requiredProps, builder, indent, maxNesting); } if (builder.getPlaceholderCount()>=2) { - //place holder count is a good indicator of snippet complexity and allows us to - // avoid creating trivial snippets (which aren't very useful). - ImmutableSet vetoProps = ImmutableSet.copyOf(requiredProps); - // Do not suggest snippet if it contains properties that are already defined. return new Snippet(typeUtil.niceTypeName(type)+" Snippet", builder.toString(), (dc) -> - dc.getDefinedProperties().stream().noneMatch(vetoProps::contains) + requiredProps.stream().noneMatch(p -> dc.getDefinedProperties().contains(p.getName())) ); } } 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 acbe32b55..ac8c1e902 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java @@ -14,7 +14,7 @@ import static org.junit.Assert.assertEquals; 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.PLAIN_COMPLETION; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.*; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; import java.io.IOException; @@ -25,6 +25,7 @@ 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.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandReleasesProvider; @@ -1609,4 +1610,159 @@ public class BoshEditorTest { ); } + @Test public void snippet_toplevel() throws Exception { + Editor editor = harness.newEditor("<*>"); + editor.assertCompletions(SNIPPET_COMPLETION, + "name: $1\n" + + "releases:\n" + + "- name: $2\n" + + " version: $3\n" + + "stemcells:\n" + + "- alias: $4\n" + + " version: $5\n" + + "update:\n" + + " canaries: $6\n" + + " max_in_flight: $7\n" + + " canary_watch_time: $8\n" + + " update_watch_time: $9\n" + + "instance_groups:\n" + + "- name: $10\n" + + " azs:\n" + + " - $11\n" + + " instances: $12\n" + + " jobs:\n" + + " - name: $13\n" + + " release: $14\n" + + " vm_type: $15\n" + + " stemcell: $16\n" + + " networks:\n" + + " - name: $17<*>" + , // ------------------ + "instance_groups:\n" + + "- name: $1\n" + + " azs:\n" + + " - $2\n" + + " instances: $3\n" + + " jobs:\n" + + " - name: $4\n" + + " release: $5\n" + + " vm_type: $6\n" + + " stemcell: $7\n" + + " networks:\n" + + " - name: $8<*>" + , // ---------------- + "releases:\n" + + "- name: $1\n" + + " version: $2<*>" + , // ---------------- + "stemcells:\n" + + "- alias: $1\n" + + " version: $2<*>" + , // ---------------- + "update:\n" + + " canaries: $1\n" + + " max_in_flight: $2\n" + + " canary_watch_time: $3\n" + + " update_watch_time: $4<*>" + , // ----------------- + "variables:\n" + + "- name: $1\n" + + " type: $2<*>" + ); + } + + @Test public void snippet_disabledWhenPropertiesAlreadyDefined() throws Exception { + Editor editor = harness.newEditor( + "name:\n" + + "releases:\n" + + "stemcells:\n" + + "<*>" + ); + + editor.assertCompletionLabels(SNIPPET_COMPLETION, + //"BoshDeploymentManifest Snippet", + "instance_groups Snippet", +// "releases Snippet", +// "stemcells Snippet" + "update Snippet", + "variables Snippet", + "- Stemcell Snippet" + ); + } + + @Test public void snippet_nested_plain() throws Exception { + Editor editor; + //Plain exact completion + editor = harness.newEditor( + "instance_groups:\n" + + "- name: blah\n" + + " <*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("jobs Snippet"), + "jo<*>" + , // ------ + "jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + editor.assertCompletionWithLabel("jobs Snippet", + "instance_groups:\n" + + "- name: blah\n" + + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + } + + @Test public void snippet_nested_indenting() throws Exception { + Editor editor; + //With extra indent: + editor = harness.newEditor( + "instance_groups:\n" + + "- name: blah\n" + + "<*>" + ); + editor.assertCompletionWithLabel("→ jobs Snippet", + "instance_groups:\n" + + "- name: blah\n" + + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs Snippet"), + "jo<*>" + , // ------ + " jobs:\n" + + " - name: $1\n" + + " release: $2<*>" + ); + } + + @Test public void relaxedCAmoreSpaces() throws Exception { + Editor editor = harness.newEditor( + "name: foo\n" + + "instance_groups:\n" + + "- name: \n" + + "<*>" + ); + editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs"), + "jo<*>" + , // ==> + " jobs:\n" + + " - <*>" + ); + } + + @Test @Ignore public void keyCompletionThatNeedANewline() throws Exception { + Editor editor = harness.newEditor( + "name: foo\n" + + "update: canwa<*>" + ); + editor.assertCompletions( + "name: foo\n" + + "update: \n" + + " canary_watch_time: <*>" + ); + } + } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 8a3a1b544..7b8f6acea 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -47,6 +47,8 @@ public class DocumentEdits implements ProposalApplier { private static final Pattern NON_WS_CHAR = Pattern.compile("\\S"); + private boolean isRelativeIndent = false; + // Note: for small number of edits this implementation is okay. // for large number of edits it is potentially slow because of the // way it transforms edit coordinates (a growing chain of diff --git a/headless-services/commons/commons-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 433eed681..8361a1157 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 @@ -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; @@ -33,7 +34,6 @@ 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; @@ -107,13 +107,12 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { for (Snippet snippet : snippets) { String snippetName = snippet.getName(); double score = FuzzyMatcher.matchScore(query, snippetName); - if (score!=0.0) { - String textBeforeQuery = doc.getLineTextBefore(offset); + if (score!=0.0 && snippet.isApplicable(getSchemaContext())) { DocumentEdits edits = new DocumentEdits(doc.getDocument()); int start = offset - query.length(); edits.delete(start, query); - int referenceIndent = textBeforeQuery.length(); - boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(offset-1)); + int referenceIndent = doc.getColumn(start); + boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(start-1)); if (needsSpace) { referenceIndent++; edits.insert(start, " "); @@ -133,7 +132,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { public List getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception { int queryOffset = offset - query.length(); - SNode contextNode = getContextNode(); DynamicSchemaContext dynamicCtxt = getSchemaContext(); List allProperties = typeUtil.getProperties(type); if (CollectionUtil.hasElements(allProperties)) { @@ -141,6 +139,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { Set definedProps = dynamicCtxt.getDefinedProperties(); List proposals = new ArrayList<>(); boolean suggestDeprecated = typeUtil.suggestDeprecatedProperties(); + YamlIndentUtil indenter = new YamlIndentUtil(doc); for (List thisTier : tieredProperties) { List undefinedProps = thisTier.stream() .filter(p -> !definedProps.contains(p.getName()) && (suggestDeprecated || !p.isDeprecated())) @@ -150,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); @@ -405,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; } @@ -425,11 +428,9 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { //value proposals which are inserted right after a key will not automatically include a newline, as // its not required for them. So we should add it along with the dash. try { - if (original instanceof ValueProposal) { - Integer insertAt = textEdit.getFirstEditStart(); - if (insertAt!=null) { - return !"".equals(doc.getLineTextBefore(insertAt).trim()); - } + Integer insertAt = textEdit.getFirstEditStart(); + if (insertAt!=null) { + return !"".equals(doc.getLineTextBefore(insertAt).trim()); } } catch (Exception e) { Log.log(e); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index e5bce3e1d..6cb30cc96 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -52,7 +52,7 @@ import com.google.common.collect.ImmutableList; * @author Kris De Volder */ public class YamlCompletionEngine implements ICompletionEngine { - + Pattern SPACES = Pattern.compile("[ ]+"); final static Logger logger = LoggerFactory.getLogger(YamlCompletionEngine.class); @@ -110,7 +110,7 @@ public class YamlCompletionEngine implements ICompletionEngine { protected Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent, double deempasizeBy) { try { - return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), + return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), current, contextNode, baseIndent, deempasizeBy); } catch (Exception e) { Log.log(e); @@ -118,7 +118,7 @@ public class YamlCompletionEngine implements ICompletionEngine { return ImmutableList.of(); } - protected Collection fixIndentations(Collection completions, SNode currentNode, + protected Collection fixIndentations(Collection completions, SNode currentNode, SNode contextNode, int baseIndent, double deempasizeBy) { if (!completions.isEmpty()) { int dashyIndent = getTargetIndent(contextNode, currentNode, true); @@ -144,7 +144,7 @@ public class YamlCompletionEngine implements ICompletionEngine { if (isExtraIndentRelaxable(contextNode, fixIndentBy)) { return indented(p, Strings.repeat(" ", fixIndentBy)); } - } else { // fixIndentBy < 0 + } else { // fixIndentBy < 0 if (isLesserIndentRelaxable(currentNode, contextNode)) { return dedented(p, -fixIndentBy, contextNode.getDocument()); } @@ -168,7 +168,7 @@ public class YamlCompletionEngine implements ICompletionEngine { } /** - * Determine the indentation level needed to line up with other contextNode children. + * Determine the indentation level needed to line up with other contextNode children. * If the contextNode has no children, then compute a proper default indentation where * a new child could be added. */ @@ -182,8 +182,8 @@ public class YamlCompletionEngine implements ICompletionEngine { if (child.isPresent()) { return child.get().getIndent(); } - return (dashy || contextNode.getNodeType()==SNodeType.DOC) - ? contextNode.getIndent() + return (dashy || contextNode.getNodeType()==SNodeType.DOC) + ? contextNode.getIndent() : contextNode.getIndent() + YamlIndentUtil.INDENT_BY; } @@ -212,7 +212,7 @@ public class YamlCompletionEngine implements ICompletionEngine { }; transformed.deemphasize(DEEMP_DEDENTED_PROPOSAL*numArrows); return transformed; - } + } // we can't dedent the proposal by the requested amount of space. So err on the safe // side and ignore the proposal. (Otherwise me might end up deleting non-space chars // in our attempt to de-dent.) @@ -226,7 +226,13 @@ public class YamlCompletionEngine implements ICompletionEngine { return Strings.repeat(Unicodes.RIGHT_ARROW+" ", numArrows) + originalLabel; } @Override public DocumentEdits transformEdit(DocumentEdits originalEdit) { - originalEdit.indentFirstEdit(indentStr); +// originalEdit.indentFirstEdit(indentStr); + YamlIndentUtil indenter = new YamlIndentUtil("\n"); + originalEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> { + String prefix = insertText.substring(0, offset); + String target = insertText.substring(offset); + return prefix + indentStr + indenter.applyIndentation(target, indentStr); + }); return originalEdit; } }; @@ -306,14 +312,14 @@ public class YamlCompletionEngine implements ICompletionEngine { } return null; } - + /** * Get context node candidates taking into account that we want to have a 'relaxed' interpretation * of the context node with respect to the current indentation where we ask for a completion. - * To allow for the ambiguity in indentation a list of context nodes is returned instead of a + * To allow for the ambiguity in indentation a list of context nodes is returned instead of a * single node. (Note we may still return a singleton list for cases where relaxed indentation * doesn't seem desirable). - * @param baseIndent + * @param baseIndent */ protected List getContextNodes(YamlDocument doc, SNode node, int offset, int baseIndent) { if (node==null) { @@ -340,7 +346,7 @@ public class YamlCompletionEngine implements ICompletionEngine { //This node has flexibility around indentation. So this is where me need to build a list of candidates! ImmutableList.Builder contextNodes = ImmutableList.builder(); while (node!=null ) { - //Any node that represents a 'step' between contexts and is not too deeply nested is kept. + //Any node that represents a 'step' between contexts and is not too deeply nested is kept. if (node.getSegment()!=null && node.getIndent()<=baseIndent) { contextNodes.add(node); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java index 5946114ca..e5a074434 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 @@ -37,4 +37,10 @@ public class Snippet { public String toString() { return "Snippet [ name="+name+",\n" +snippet +"\n]"; } + public Predicate getApplicability() { + return applicability; + } + public boolean isApplicable(DynamicSchemaContext dc) { + return applicability==null || applicability.test(dc); + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java index 4952c4b05..8fe013109 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.yaml.util; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import com.google.common.base.Strings; @@ -42,7 +43,11 @@ public class YamlIndentUtil { } public YamlIndentUtil(YamlDocument doc) { - this(doc.getDocument().getDefaultLineDelimiter()); + this(doc.getDocument()); + } + + public YamlIndentUtil(IDocument doc) { + this(doc.getDefaultLineDelimiter()); } /** @@ -89,6 +94,10 @@ public class YamlIndentUtil { return text.replaceAll("\\n", newlineWithIndent(indentBy)); } + public String applyIndentation(String text, String indentStr) { + return text.replaceAll("\\n", "\n"+indentStr); + } + /** * Increase offset by indentation. Take care when 'indent' is -1 (unkownn) to * just return offset unmodified. diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 7274cad87..4e81b51a8 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -63,6 +63,7 @@ public class Editor { public static final Predicate PLAIN_COMPLETION = c -> !RELAXED_COMPLETION.test(c); public static final Predicate DEDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.LEFT_ARROW+" "); public static final Predicate INDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" "); + public static final Predicate SNIPPET_COMPLETION = c -> c.getLabel().endsWith("Snippet"); static class EditorState { String documentContents; @@ -327,6 +328,10 @@ public class Editor { } public List assertCompletionLabels(String... expectedLabels) throws Exception { + return assertCompletionLabels(c -> true, expectedLabels); + } + + public List assertCompletionLabels(Predicate isInteresting, String... expectedLabels) throws Exception { StringBuilder expect = new StringBuilder(); StringBuilder actual = new StringBuilder(); for (String label : expectedLabels) { @@ -336,8 +341,10 @@ public class Editor { List completions; for (CompletionItem completion : completions = getCompletions()) { - actual.append(completion.getLabel()); - actual.append("\n"); + if (isInteresting.test(completion)) { + actual.append(completion.getLabel()); + actual.append("\n"); + } } assertEquals(expect.toString(), actual.toString()); return completions;