Content assist for 'domains' inside of route-strings

This commit is contained in:
Kris De Volder
2017-04-28 18:20:34 -07:00
parent 1966affe05
commit 5ccb3e5f22
13 changed files with 473 additions and 26 deletions

View File

@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.commons.languageserver.completion;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -295,7 +296,7 @@ public class DocumentEdits implements ProposalApplier {
}
}
private ArrayList<Edit> edits = new ArrayList<Edit>();
private List<Edit> edits = new ArrayList<Edit>();
private IDocument doc;
public DocumentEdits(IDocument doc) {

View File

@@ -58,18 +58,17 @@ public class FuzzyMatcher {
//tend to favor matches at the end of the string over matches in the middle.
skips+=dlen-dpos; //but do count the extra chars at end => more extra = worse score
}
return score(gaps, skips);
return score(gaps, skips, pattern);
}
private static double score(int gaps, int skips) {
private static double score(int gaps, int skips, String pattern) {
if (gaps==0) {
//gaps == 0 means a prefix match, ignore 'skips' at end of String and just sort
// alphabetic (see STS-4049)
double badness = 0.1; // all scored equally, assumes using a 'stable' sorter.
return -badness; //higher is better
return 0.5+pattern.length(); //all scored equally, assumes using a 'stable' sorter.
} else {
double badness = 1+gaps + skips/10000.0; // higher is worse
return -badness; //higher is better
double badness = 1+gaps + skips/1000.0; // higher is worse
return 1.0/badness + pattern.length(); //higher is better
}
}

View File

@@ -56,6 +56,20 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext {
return doc;
}
protected DocumentRegion getCustomAssistRegion(YamlDocument doc, SNode node, int offset) {
if (node.getNodeType()==SNodeType.KEY) {
SKeyNode keyNode = (SKeyNode) node;
if (keyNode.isInValue(offset)) {
int valueStart = keyNode.getColonOffset()+1;
int valueEnd = keyNode.getNodeEnd(); // assumes we only look at the current line, good enough for now
return new DocumentRegion(doc.getDocument(), valueStart, valueEnd);
}
}
return null; // TODO Reaching here might mean support for calling the custom assistant isn't
// implemented for this kind of context yet. It will have to be expanded upon
// as the need for it arises in real use-cases.
}
protected final String getPrefix(YamlDocument doc, SNode node, int offset) {
//For value completions... in general we would like to determine the whole text
// corresponding to the value, so a simplistic backwards scan isn't good enough.

View File

@@ -84,7 +84,6 @@ public class DefaultCompletionFactory implements CompletionFactory {
public class ValueProposal extends ScoreableProposal {
private String value;
//private String query;
private String label;
private YType type;
private double baseScore;
@@ -94,7 +93,6 @@ public class DefaultCompletionFactory implements CompletionFactory {
public ValueProposal(String value, String query, String label, YType type, Renderable docs, double score, DocumentEdits edits, YTypeUtil typeUtil) {
this.value = value;
//this.query = query;
this.label = label;
this.type = type;
this.docs = docs;

View File

@@ -32,7 +32,9 @@ public abstract class TransformedCompletion extends ScoreableProposal {
this.original = proposal;
}
protected abstract String tranformLabel(String originalLabel);
protected String tranformLabel(String originalLabel) {
return originalLabel;
}
protected abstract DocumentEdits transformEdit(DocumentEdits textEdit);
@Override

View File

@@ -36,6 +36,7 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.YamlPathSegmentType;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.ISubCompletionEngine;
import org.springframework.ide.vscode.commons.yaml.schema.SNodeDynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil;
@@ -84,6 +85,13 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
@Override
public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
ISubCompletionEngine customContentAssistant = typeUtil.getCustomContentAssistant(type);
if (customContentAssistant!=null) {
DocumentRegion region = getCustomAssistRegion(doc, node, offset);
if (region!=null) {
return customContentAssistant.getCompletions(completionFactory(), region, region.toRelative(offset));
}
}
String query = getPrefix(doc, node, offset);
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, node, offset, query);
if (!valueCompletions.isEmpty()) {

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
* 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.schema;
import java.util.List;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.yaml.completion.CompletionFactory;
/**
* Interface that can be used by a {@link ICompletionEngine} to delegate to a
* 'helper' completion engine that computes completions for some sub-region of
* the document.
*
* @author Kris De Volder
*/
@FunctionalInterface
public interface ISubCompletionEngine {
/**
* @param region The relevant subregion of the document (typically, the sub-engine only cares about the text
* within this particular region.)
* @param offset The cursor position relative to the region.
*/
List<ICompletionProposal> getCompletions(CompletionFactory f, DocumentRegion region, int offset);
}

View File

@@ -224,6 +224,11 @@ public class YTypeFactory {
public List<Constraint> getConstraints(YType type) {
return ((AbstractType)type).getConstraints();
}
@Override
public ISubCompletionEngine getCustomContentAssistant(YType type) {
return ((AbstractType)type).getCustomContentAssistant();
}
};
/////////////////////////////////////////////////////////////////////////////////////
@@ -239,11 +244,21 @@ public class YTypeFactory {
private Map<String, YTypedProperty> cachedPropertyMap;
private SchemaContextAware<Callable<Collection<YValueHint>>> hintProvider;
private List<Constraint> constraints = new ArrayList<>(2);
private ISubCompletionEngine customContentAssistant = null;
public boolean isSequenceable() {
return false;
}
public ISubCompletionEngine getCustomContentAssistant() {
return customContentAssistant;
}
public AbstractType setCustomContentAssistant(ISubCompletionEngine customContentAssistant) {
this.customContentAssistant = customContentAssistant;
return this;
}
public YType inferMoreSpecificType(DynamicSchemaContext dc) {
return this;
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.List;
import java.util.Map;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint;
@@ -47,4 +48,6 @@ public interface YTypeUtil {
*/
YType inferMoreSpecificType(YType type, DynamicSchemaContext dc);
List<Constraint> getConstraints(YType type);
ISubCompletionEngine getCustomContentAssistant(YType type);
}

View File

@@ -0,0 +1,59 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_functional_interfaces=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=false
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.insert_inferred_type_arguments=false
sp_cleanup.make_local_variable_final=true
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=false
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=false
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_redundant_type_arguments=false
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=false
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_anonymous_class_creation=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_lambda=true
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

View File

@@ -45,10 +45,12 @@ public class ManifestYmlSchema implements YamlSchema {
private static final String HEALTH_CHECK_HTTP_ENDPOINT_PROP = "health-check-http-endpoint";
private static final String HEALTH_CHECK_TYPE_PROP = "health-check-type";
private final AbstractType TOPLEVEL_TYPE;
private final YTypeUtil TYPE_UTIL;
public final AbstractType t_route_string;
private static final Set<String> TOPLEVEL_EXCLUDED = ImmutableSet.of(
"name", "host", "hosts"
);
@@ -65,7 +67,7 @@ public class ManifestYmlSchema implements YamlSchema {
if (markerNode != null) {
String healthCheckType = getEffectiveHealthCheckType(ast, dc.getPath(), node);
if (!"http".equals(healthCheckType)) {
problems.accept(YamlSchemaProblems.problem(ManifestYamlSchemaProblemsTypes.IGNORED_PROPERTY,
problems.accept(YamlSchemaProblems.problem(ManifestYamlSchemaProblemsTypes.IGNORED_PROPERTY,
"This has no effect unless `"+HEALTH_CHECK_TYPE_PROP+"` is `http` (but it is currently set to `"+healthCheckType+"`)", markerNode));
}
}
@@ -142,14 +144,12 @@ public class ManifestYmlSchema implements YamlSchema {
YType t_string = f.yatomic("String");
YType t_strings = f.yseq(t_string);
// "routes" has nested required property "route":
// routes:
// - route: someroute.io
t_route_string = f.yatomic("RouteUri")
.parseWith(new RouteValueParser(YTypeFactory.valuesFromHintProvider(domainsProvider)))
.setCustomContentAssistant(new RouteContentAssistant(domainsProvider, this));
YBeanType route = f.ybean("Route");
YAtomicType t_route_string = f.yatomic("route");
route.addProperty(f.yprop("route", t_route_string).isRequired(true));
t_route_string.parseWith(new RouteValueParser(YTypeFactory.valuesFromHintProvider(domainsProvider)));
YAtomicType t_memory = f.yatomic("Memory");
t_memory.addHints("256M", "512M", "1024M");

View File

@@ -0,0 +1,133 @@
/*******************************************************************************
* 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.manifest.yaml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.yaml.completion.CompletionFactory;
import org.springframework.ide.vscode.commons.yaml.schema.ISubCompletionEngine;
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
import com.google.common.collect.ImmutableList;
/**
* Custom content assistant for making 'domains' suggestions inside of a RouteUri.
*
* @author Kris De Volder
*/
public class RouteContentAssistant implements ISubCompletionEngine {
private static final Pattern STOP_AP = Pattern.compile("[#:/]");
private Callable<Collection<YValueHint>> domainsProvider;
private ManifestYmlSchema schema;
public RouteContentAssistant(Callable<Collection<YValueHint>> domainsProvider, ManifestYmlSchema manifestYmlSchema) {
this.domainsProvider = domainsProvider;
this.schema = manifestYmlSchema;
}
@Override
public List<ICompletionProposal> getCompletions(CompletionFactory f, DocumentRegion region, int offset) {
// offset = 0 means: "<*>abc"
// offset = 1 means: "a<*>bc"
// offset = 2 means: "ab<*>c"
try {
region = chopEnd(region, offset);
// region = "abc"
// offset = 3 means: "abc<*>"
// So offset > 3 means we are 'our of bounds for this content assistant
if (offset<=region.length()) {
String[] queries = getQueries(region.subSequence(0, offset));
Collection<YValueHint> domains = domainsProvider.call();
List<ICompletionProposal> proposals = new ArrayList<>();
for (YValueHint domain : domains) {
for (String query : queries) {
double score = FuzzyMatcher.matchScore(query, domain.getValue());
if (score!=0.0) {
proposals.add(createProposal(f, region, offset, query, score, domain));
break; //break here so we select the first (i.e. longest) query that matches
}
}
}
return proposals;
}
} catch (Exception e) {
Log.log(e);
//Ignore. This is somewhat expected. Stuff can go wrong resolving the domains
// and CA engine just doesn't provide CA in that case.
}
return ImmutableList.of();
}
private String[] getQueries(DocumentRegion region) {
// Example of what this should do
// region = "foo.bar.com"
// We have to make a guess which part of this is the host-name and which part is domain-name
// The split could be either at the very start, or at any one of the '.' chars.
// So the possible queries are as follows:
// - "foo.bar.com"
// - "bar.com"
// - "com"
region = region.trimStart();
DocumentRegion[] pieces = region.split('.');
String[] queries = new String[pieces.length];
for (int i = pieces.length-1; i >= 0; i--) {
if (i==pieces.length-1) {
queries[i] = pieces[i].toString();
} else {
queries[i] = pieces[i] + "." + queries[i+1];
}
}
return queries;
}
private ICompletionProposal createProposal(CompletionFactory f, DocumentRegion region, int offset, String query, double score, YValueHint domain) {
DocumentEdits edits = new DocumentEdits(region.getDocument());
region = region.subSequence(offset - query.length());
boolean needSpace = region.textBefore(1).charAt(0)==':'; //Add extra space after ':' if needed!
edits.replace(region.getStart(), region.getEnd(), needSpace ? " "+domain.getValue() : domain.getValue());
return f.valueProposal(domain.getValue(), query, domain.getLabel(), schema.t_route_string,
domain.getDocumentation(), score, edits, schema.getTypeUtil());
}
/**
* Chop-off the uninteresting parts of region (whitespace, comments, anything after the first ':' or '/'
*/
private DocumentRegion chopEnd(DocumentRegion region, int offset) {
Matcher matcher = STOP_AP.matcher(region);
if (matcher.find()) {
region = region.subSequence(0, matcher.start());
}
if (offset<=region.getLength()) {
//We want to trim whitespace of the end, but must be careful not to trim past the offset
DocumentRegion trimmed = region.trimEnd();
if (offset<=trimmed.length()) {
return trimmed;
} else {
return region.subSequence(0, offset);
}
}
return region;
}
}

View File

@@ -463,7 +463,7 @@ public class ManifestYamlEditorTest {
);
editor.assertProblems(/*NONE*/);
}
@Test public void reconcileHealthHttpEndPointIgnoredWarning() throws Exception {
Editor editor;
Diagnostic problem;
@@ -483,7 +483,7 @@ public class ManifestYamlEditorTest {
" health-check-http-endpoint: /health"
);
editor.assertProblems(/*NONE*/);
editor = harness.newEditor(
"applications:\n" +
"- name: my-app\n" +
@@ -499,7 +499,7 @@ public class ManifestYamlEditorTest {
" health-check-http-endpoint: /health"
);
editor.assertProblems(/*NONE*/);
editor = harness.newEditor(
"health-check-type: http\n" +
"applications:\n" +
@@ -508,12 +508,12 @@ public class ManifestYamlEditorTest {
" health-check-http-endpoint: /health"
);
editor.assertProblems("health-check-http-endpoint|This has no effect unless `health-check-type` is `http` (but it is currently set to `process`)");
editor = harness.newEditor(
"health-check-http-endpoint: /health"
);
editor.assertProblems("health-check-http-endpoint|This has no effect unless `health-check-type` is `http` (but it is currently set to `port`)");
editor = harness.newEditor(
"health-check-type: process\n" +
"health-check-http-endpoint: /health"
@@ -662,19 +662,19 @@ public class ManifestYamlEditorTest {
assertEquals("an-org : a-space [test.io]", c.getDocumentation());
}
@Test public void domainReconcile() throws Exception {
List<CFDomain> domains = ImmutableList.of(mockDomain("one.com"), mockDomain("two.com"));
when(cloudfoundry.client.getDomains()).thenReturn(domains);
Editor editor;
Diagnostic p;
editor = harness.newEditor(
"domain: bad.com"
);
p = editor.assertProblems("bad.com|unknown 'Domain'. Valid values are: [one.com, two.com]").get(0);
assertEquals(DiagnosticSeverity.Warning, p.getSeverity());
editor= harness.newEditor(
"domains:\n" +
"- one.com\n" +
@@ -1282,7 +1282,7 @@ public class ManifestYamlEditorTest {
" - route: host.springsource.org\n");
editor.assertProblems();
}
@Test public void dashedContentAssistForServices() throws Exception {
Editor editor;
CFServiceInstance service = mock(CFServiceInstance.class);
@@ -1401,6 +1401,183 @@ public class ManifestYamlEditorTest {
);
}
@Test public void contentAssistInsideRouteDomain() throws Exception {
Editor editor;
ImmutableList<CFDomain> domains = ImmutableList.of(
mockDomain("cfapps.io"),
mockDomain("dsyer.com")
);
when(cloudfoundry.client.getDomains()).thenReturn(domains);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route:<*>"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: cfapps.io<*>"
, // ==============
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: dsyer.com<*>"
);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: <*>"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: cfapps.io<*>"
, // ==============
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: dsyer.com<*>"
);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: dsyer.<*>"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: dsyer.com<*>"
, // ==============
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: dsyer.cfapps.io<*>"
);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.<*>"
);
CompletionItem c = editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.cfapps.io<*>"
, // ==============
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.dsyer.com<*>"
).get(0);
assertEquals("cfapps.io", c.getLabel());
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.<*>"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.cfapps.io<*>"
, // ==============
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.dsyer.com<*>"
);
/// no content assist inside of path or port section of route:
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.com/<*>"
);
editor.assertCompletions(/*NONE*/);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.com:<*>"
);
editor.assertCompletions(/*NONE*/);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: foo.bar.com:7777/blah<*>"
);
editor.assertCompletions(/*NONE*/);
// Martin's most fancy example:
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.cf<*>pps.io/superpath\n" +
" memory: 1024M\n"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.cfapps.io<*>/superpath\n" +
" memory: 1024M\n"
);
//Kris's variants of Martin's example:
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.ds<*>pps.io/superpath\n" +
" memory: 1024M\n"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.dsyer.com<*>/superpath\n" +
" memory: 1024M\n"
);
editor = harness.newEditor(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.ds<*>pps.io:8888/superpath\n" +
" memory: 1024M\n"
);
editor.assertCompletions(
"applications:\n" +
"- name: test\n" +
" routes:\n" +
" - route: test.dsyer.com<*>:8888/superpath\n" +
" memory: 1024M\n"
);
}
//////////////////////////////////////////////////////////////////////////////
private List<CompletionItem> assertCompletions(String textBefore, String... textAfter) throws Exception {