Merge branch 'master' into routes-validation
# Conflicts: # headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java # headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java
This commit is contained in:
@@ -47,6 +47,7 @@ import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngi
|
||||
import org.springframework.ide.vscode.commons.yaml.hover.YamlHoverInfoProvider;
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument;
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider;
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -119,7 +120,12 @@ public class BootPropertiesLanguageServer extends SimpleLanguageServer {
|
||||
|
||||
private ICompletionEngine getCompletionEngine() {
|
||||
ICompletionEngine propertiesCompletions = new SpringPropertiesCompletionEngine(indexProvider, typeUtilProvider, javaProjectFinder);
|
||||
ICompletionEngine yamlCompletions = new YamlCompletionEngine(yamlStructureProvider, yamlAssistContextProvider);
|
||||
ICompletionEngine yamlCompletions = new YamlCompletionEngine(yamlStructureProvider, yamlAssistContextProvider) {
|
||||
@Override
|
||||
protected boolean isLesserIndentRelaxable(SNode currentNode, SNode contextNode) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return (IDocument document, int offset) -> {
|
||||
String uri = document.getUri();
|
||||
if (uri!=null) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 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.cloudfoundry.client;
|
||||
|
||||
public interface CFStack extends CFEntity {
|
||||
|
||||
}
|
||||
@@ -18,5 +18,6 @@ public interface ClientRequests {
|
||||
List<CFBuildpack> getBuildpacks() throws Exception;
|
||||
List<CFServiceInstance> getServices() throws Exception;
|
||||
List<CFDomain> getDomains() throws Exception;
|
||||
|
||||
List<CFStack> getStacks() throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ import reactor.ipc.netty.channel.AbortedException;
|
||||
|
||||
/**
|
||||
* This is a stateful callable context that is "aware" of CF errors, and is not
|
||||
* suitable for reuse as it may cache errors
|
||||
*
|
||||
* suitable for reuse as it may cache errors.
|
||||
*/
|
||||
public class CFCallableContext {
|
||||
|
||||
private final CFParamsProviderMessages paramsProviderMessages;
|
||||
private Exception lastConnectionError;
|
||||
private long lastErrorTime = 0;
|
||||
|
||||
public CFCallableContext(CFParamsProviderMessages paramsProviderMessages) {
|
||||
this.paramsProviderMessages = paramsProviderMessages;
|
||||
@@ -38,6 +38,7 @@ public class CFCallableContext {
|
||||
try {
|
||||
return callable.call();
|
||||
} catch (Exception e) {
|
||||
lastErrorTime = System.currentTimeMillis();
|
||||
throw convertToCfVscodeError(e);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ public class CFCallableContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasConnectionError() {
|
||||
return this.lastConnectionError != null;
|
||||
public boolean hasExpiredConnectionError() {
|
||||
return this.lastConnectionError != null && System.currentTimeMillis() - lastErrorTime > CFTargetCache.ERROR_EXPIRATION.toMillis();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFBuildpack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFDomain;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInstance;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
@@ -41,8 +42,8 @@ public class CFTarget {
|
||||
private LoadingCache<String, List<CFBuildpack>> buildpacksCache;
|
||||
private LoadingCache<String, List<CFServiceInstance>> servicesCache;
|
||||
private LoadingCache<String, List<CFDomain>> domainCache;
|
||||
private LoadingCache<String, List<CFStack>> stacksCache;
|
||||
private CFCallableContext callableContext;
|
||||
|
||||
public CFTarget(String targetName, CFClientParams params, ClientRequests requests,
|
||||
CFCallableContext callableContext) {
|
||||
this.params = params;
|
||||
@@ -53,6 +54,20 @@ public class CFTarget {
|
||||
}
|
||||
|
||||
private void initCache(ClientRequests requests) {
|
||||
CacheLoader<String, List<CFStack>> stacksLoader = new CacheLoader<String, List<CFStack>>() {
|
||||
|
||||
@Override
|
||||
public List<CFStack> load(String key) throws Exception {
|
||||
/* Cache of services does not use keys, as the whole cache
|
||||
* gets wiped clean on any new call to CF.
|
||||
*/
|
||||
return runAndCheckForFailure(() -> requests.getStacks());
|
||||
}
|
||||
};
|
||||
this.stacksCache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(CFTargetCache.SERVICES_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS).build(stacksLoader);
|
||||
|
||||
|
||||
CacheLoader<String, List<CFServiceInstance>> servicesLoader = new CacheLoader<String, List<CFServiceInstance>>() {
|
||||
|
||||
@Override
|
||||
@@ -64,7 +79,7 @@ public class CFTarget {
|
||||
}
|
||||
};
|
||||
this.servicesCache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(CFTargetCache.SERVICES_EXPIRATION, TimeUnit.SECONDS).build(servicesLoader);
|
||||
.expireAfterAccess(CFTargetCache.SERVICES_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS).build(servicesLoader);
|
||||
|
||||
CacheLoader<String, List<CFBuildpack>> buildpacksLoader = new CacheLoader<String, List<CFBuildpack>>() {
|
||||
|
||||
@@ -77,7 +92,7 @@ public class CFTarget {
|
||||
}
|
||||
};
|
||||
this.buildpacksCache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(CFTargetCache.TARGET_EXPIRATION, TimeUnit.HOURS).build(buildpacksLoader);
|
||||
.expireAfterAccess(CFTargetCache.TARGET_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS).build(buildpacksLoader);
|
||||
|
||||
CacheLoader<String, List<CFDomain>> domainLoader = new CacheLoader<String, List<CFDomain>>() {
|
||||
|
||||
@@ -88,21 +103,30 @@ public class CFTarget {
|
||||
|
||||
};
|
||||
this.domainCache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(CFTargetCache.TARGET_EXPIRATION, TimeUnit.HOURS).build(domainLoader);
|
||||
.expireAfterAccess(CFTargetCache.TARGET_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS).build(domainLoader);
|
||||
}
|
||||
|
||||
protected <T> T runAndCheckForFailure(Callable<T> callable) throws Exception {
|
||||
return callableContext.checkConnection(callable);
|
||||
}
|
||||
|
||||
public boolean hasConnectionError() {
|
||||
return callableContext.hasConnectionError();
|
||||
public boolean hasExpiredConnectionError() {
|
||||
return callableContext.hasExpiredConnectionError();
|
||||
}
|
||||
|
||||
public CFClientParams getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
public List<CFStack> getStacks() throws Exception {
|
||||
// Use the target name as the "key" , since Guava cache doesn't allow null keys
|
||||
// However, the key is not really used when fetching buildpacks, as we are not caching
|
||||
// buildpacks per target here. This class only represents ONE target, so it will only
|
||||
// ever have one key
|
||||
String key = getName();
|
||||
return this.stacksCache.get(key);
|
||||
}
|
||||
|
||||
public List<CFBuildpack> getBuildpacks() throws Exception {
|
||||
// Use the target name as the "key" , since Guava cache doesn't allow null keys
|
||||
// However, the key is not really used when fetching buildpacks, as we are not caching
|
||||
@@ -143,4 +167,5 @@ public class CFTarget {
|
||||
// %o : %s - [%a]
|
||||
return params.getOrgName() + " : " + params.getSpaceName() + " ["+params.getApiUrl()+"]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -30,8 +31,9 @@ public class CFTargetCache {
|
||||
private final LoadingCache<ClientParamsCacheKey, CFTarget> cache;
|
||||
private final CFCallableContext cacheCallableContext;
|
||||
|
||||
public static final long SERVICES_EXPIRATION = 10;
|
||||
public static final long TARGET_EXPIRATION = 1;
|
||||
public static final Duration SERVICES_EXPIRATION = Duration.ofSeconds(10);
|
||||
public static final Duration TARGET_EXPIRATION = Duration.ofHours(1);
|
||||
public static final Duration ERROR_EXPIRATION = Duration.ofSeconds(10);
|
||||
|
||||
public CFTargetCache(ClientParamsProvider paramsProvider, CloudFoundryClientFactory clientFactory,
|
||||
ClientTimeouts timeouts) {
|
||||
@@ -49,7 +51,7 @@ public class CFTargetCache {
|
||||
}
|
||||
|
||||
};
|
||||
cache = CacheBuilder.newBuilder().maximumSize(1).expireAfterAccess(TARGET_EXPIRATION, TimeUnit.HOURS)
|
||||
cache = CacheBuilder.newBuilder().maximumSize(1).expireAfterAccess(TARGET_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.build(loader);
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ public class CFTargetCache {
|
||||
CFTarget target = cache.get(key);
|
||||
if (target != null) {
|
||||
// If any CF errors occurred in the target, refresh once
|
||||
if (target.hasConnectionError()) {
|
||||
if (target.hasExpiredConnectionError()) {
|
||||
cache.refresh(key);
|
||||
target = cache.get(key);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ import org.cloudfoundry.operations.buildpacks.Buildpack;
|
||||
import org.cloudfoundry.operations.domains.Domain;
|
||||
import org.cloudfoundry.operations.services.ServiceInstanceSummary;
|
||||
import org.cloudfoundry.operations.services.ServiceInstanceType;
|
||||
import org.cloudfoundry.operations.stacks.Stack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFBuildpack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFDomain;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFEntities;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInstance;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack;
|
||||
|
||||
/**
|
||||
* Various helper methods to 'wrap' objects returned by CF client into our own
|
||||
@@ -27,17 +29,16 @@ import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInsta
|
||||
*/
|
||||
public class CFWrappingV2 {
|
||||
|
||||
|
||||
public static CFBuildpack wrap(Buildpack buildpack) {
|
||||
String name = buildpack.getName();
|
||||
return CFEntities.createBuildpack(name);
|
||||
}
|
||||
|
||||
|
||||
public static CFDomain wrap(Domain domain) {
|
||||
String name = domain.getName();
|
||||
return CFEntities.createDomain(name);
|
||||
}
|
||||
|
||||
|
||||
public static CFServiceInstance wrap(ServiceInstanceSummary serviceInstance) {
|
||||
String name = serviceInstance.getName();
|
||||
String plan = serviceInstance.getPlan();
|
||||
@@ -47,4 +48,22 @@ public class CFWrappingV2 {
|
||||
return CFEntities.createServiceInstance(name, service, plan);
|
||||
}
|
||||
|
||||
public static CFStack wrap(Stack stack) {
|
||||
if (stack!=null) {
|
||||
String name = stack.getName();
|
||||
return new CFStack() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CFStack("+name+")";
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFBuildpack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFDomain;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInstance;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientTimeouts;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFClientParams;
|
||||
@@ -83,7 +84,7 @@ public class DefaultClientRequestsV2 implements ClientRequests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<CFDomain> getDomains() throws Exception {
|
||||
|
||||
@@ -98,7 +99,7 @@ public class DefaultClientRequestsV2 implements ClientRequests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<CFBuildpack> getBuildpacks() throws Exception {
|
||||
return ReactorUtils.get(timeouts.getBuildpacksTimeout(), CancelationTokens.NULL,
|
||||
@@ -112,6 +113,18 @@ public class DefaultClientRequestsV2 implements ClientRequests {
|
||||
)
|
||||
);
|
||||
}
|
||||
@Override
|
||||
public List<CFStack> getStacks() throws Exception {
|
||||
return ReactorUtils.get(
|
||||
log("operations.stacks().list()",
|
||||
_operations.stacks()
|
||||
.list()
|
||||
.map(CFWrappingV2::wrap)
|
||||
.collectList()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//// calls to client and operations with 'logging'.
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
|
||||
package org.springframework.ide.vscode.commons.languageserver;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
|
||||
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement;
|
||||
|
||||
/**
|
||||
* Some 'custom' extensions to standard LSP {@link LanguageClient}.
|
||||
@@ -24,4 +28,7 @@ public interface STS4LanguageClient extends LanguageClient {
|
||||
@JsonNotification("sts/progress")
|
||||
void progress(ProgressParams progressEvent);
|
||||
|
||||
@JsonRequest("sts/moveCursor")
|
||||
CompletableFuture<Object> moveCursor(CursorMovement cursorMovement);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -134,14 +135,15 @@ public class DocumentEdits implements ProposalApplier {
|
||||
private int offset;
|
||||
private String text;
|
||||
|
||||
public Insertion(int offset, String insert) {
|
||||
public Insertion(boolean grabCursor, int offset, String insert) {
|
||||
super(grabCursor);
|
||||
this.offset = offset;
|
||||
this.text = insert;
|
||||
}
|
||||
|
||||
@Override
|
||||
void apply(DocumentState doc) throws BadLocationException {
|
||||
doc.insert(offset, text);
|
||||
doc.insert(grabCursor, offset, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,6 +163,10 @@ public class DocumentEdits implements ProposalApplier {
|
||||
}
|
||||
|
||||
private abstract class Edit {
|
||||
protected boolean grabCursor;
|
||||
protected Edit(boolean grabCursor) {
|
||||
this.grabCursor = grabCursor;
|
||||
}
|
||||
public abstract int getStart();
|
||||
public abstract int getEnd();
|
||||
abstract void apply(DocumentState doc) throws BadLocationException;
|
||||
@@ -173,14 +179,15 @@ public class DocumentEdits implements ProposalApplier {
|
||||
private int start;
|
||||
private int end;
|
||||
|
||||
public Deletion(int start, int end) {
|
||||
public Deletion(boolean grabCursor, int start, int end) {
|
||||
super(grabCursor);
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
void apply(DocumentState doc) throws BadLocationException {
|
||||
doc.delete(start, end);
|
||||
doc.delete(grabCursor, start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -226,7 +233,7 @@ public class DocumentEdits implements ProposalApplier {
|
||||
this.doc = doc;
|
||||
}
|
||||
|
||||
public void insert(int start, final String text) throws BadLocationException {
|
||||
public void insert(boolean grabCursor, int start, final String text) throws BadLocationException {
|
||||
final int tStart = org2new.transform(start, Direction.AFTER);
|
||||
if (!text.isEmpty()) {
|
||||
if (doc!=null) {
|
||||
@@ -251,10 +258,14 @@ public class DocumentEdits implements ProposalApplier {
|
||||
}
|
||||
};
|
||||
}
|
||||
selection = tStart+text.length();
|
||||
if (grabCursor) {
|
||||
selection = tStart+text.length();
|
||||
} else if (selection > tStart) {
|
||||
selection += text.length();
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(final int start, final int end) throws BadLocationException {
|
||||
public void delete(boolean grabCursor, final int start, final int end) throws BadLocationException {
|
||||
final int tStart = org2new.transform(start, Direction.AFTER);
|
||||
if (end>start) { // skip work for 'delete nothing' op
|
||||
final int tEnd = org2new.transform(end, Direction.AFTER);
|
||||
@@ -279,7 +290,14 @@ public class DocumentEdits implements ProposalApplier {
|
||||
};
|
||||
}
|
||||
}
|
||||
selection = tStart;
|
||||
if (grabCursor) {
|
||||
selection = tStart;
|
||||
} else if (selection>tStart) {
|
||||
int len = end - start;
|
||||
if (len > 0) {
|
||||
selection = Math.max(tStart, selection-len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -295,16 +313,25 @@ public class DocumentEdits implements ProposalApplier {
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<Edit> edits = new ArrayList<Edit>();
|
||||
private List<Edit> edits = new ArrayList<Edit>();
|
||||
private IDocument doc;
|
||||
|
||||
/**
|
||||
* When this is true, the cursor is moved after each edit, to be positioned right after the
|
||||
* edit.
|
||||
* <p>
|
||||
* When it is false, the cursor only moves to remain in place relative to the surrounding text.
|
||||
* (I.e. if text is inserted/deleted before the cursor its shifted by the length of the inserted/deleted text).
|
||||
*/
|
||||
private boolean grabCursor = true;
|
||||
|
||||
public DocumentEdits(IDocument doc) {
|
||||
this.doc = doc;
|
||||
}
|
||||
|
||||
public void delete(int start, int end) {
|
||||
Assert.isLegal(start<=end);
|
||||
edits.add(new Deletion(start, end));
|
||||
edits.add(new Deletion(grabCursor, start, end));
|
||||
}
|
||||
|
||||
public void delete(int offset, String text) {
|
||||
@@ -312,7 +339,7 @@ public class DocumentEdits implements ProposalApplier {
|
||||
}
|
||||
|
||||
public void insert(int offset, String insert) {
|
||||
edits.add(new Insertion(offset, insert));
|
||||
edits.add(new Insertion(grabCursor, offset, insert));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -416,10 +443,14 @@ public class DocumentEdits implements ProposalApplier {
|
||||
if (edits.size()>0) {
|
||||
Edit firstEdit = edits.get(0);
|
||||
int offset = firstEdit.getStart();
|
||||
edits.add(0, new Insertion(offset, indentString));
|
||||
edits.add(0, new Insertion(grabCursor, offset, indentString));
|
||||
}
|
||||
}
|
||||
|
||||
public void firstDelete(int start, int end) {
|
||||
edits.add(0, new Deletion(grabCursor, start, end));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find first non-whitepace insertion edit and transform its contents.
|
||||
* @param transformFun receives the insertion text of the target edit and the offset of the first non-whitespace character
|
||||
@@ -444,4 +475,11 @@ public class DocumentEdits implements ProposalApplier {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop moving the cursor to the end of edits for successive edits. The cursor will still be
|
||||
* update to remain 'in place' relative to the surrounding text.
|
||||
*/
|
||||
public void freezeCursor() {
|
||||
this.grabCursor = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,49 +17,49 @@ import org.springframework.ide.vscode.commons.util.Assert;
|
||||
|
||||
public abstract class ScoreableProposal implements ICompletionProposal {
|
||||
|
||||
public static final double DEEMP_EXISTS = 0.1;
|
||||
public static final double DEEMP_DEPRECATION = 0.2;
|
||||
public static final double DEEMP_DASH_PROPOSAL = 0.5;
|
||||
public static final double DEEMP_INDENTED_PROPOSAL = 1.0;
|
||||
public static final double DEEMP_EXISTS = 0.1;
|
||||
public static final double DEEMP_DEPRECATION = 0.2;
|
||||
public static final double DEEMP_DASH_PROPOSAL = 0.5;
|
||||
public static final double DEEMP_INDENTED_PROPOSAL = 1.0;
|
||||
public static final double DEEMP_DEDENTED_PROPOSAL = 1.5;
|
||||
|
||||
private static final double DEEMP_VALUE = 10_000; // should be large enough to move deemphasized stuff to bottom of list.
|
||||
|
||||
private static final double DEEMP_VALUE = 10_000; // should be large enough to move deemphasized stuff to bottom of list.
|
||||
private double deemphasizedBy = 0.0;
|
||||
|
||||
private double deemphasizedBy = 0.0;
|
||||
|
||||
/**
|
||||
* A sorter suitable for sorting ScoreableProposals based on their score.
|
||||
*/
|
||||
public static final Comparator<ICompletionProposal> COMPARATOR = new Comparator<ICompletionProposal>() {
|
||||
@Override
|
||||
public int compare(ICompletionProposal p1, ICompletionProposal p2) {
|
||||
if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) {
|
||||
double s1 = ((ScoreableProposal)p1).getScore();
|
||||
double s2 = ((ScoreableProposal)p2).getScore();
|
||||
if (Math.abs(s1-s2)<1E-5) {
|
||||
String name1 = ((ScoreableProposal)p1).getLabel();
|
||||
String name2 = ((ScoreableProposal)p2).getLabel();
|
||||
return name1.compareTo(name2);
|
||||
} else {
|
||||
return Double.compare(s2, s1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
public abstract double getBaseScore();
|
||||
public final double getScore() {
|
||||
return getBaseScore() - deemphasizedBy;
|
||||
}
|
||||
/**
|
||||
* A sorter suitable for sorting ScoreableProposals based on their score.
|
||||
*/
|
||||
public static final Comparator<ICompletionProposal> COMPARATOR = new Comparator<ICompletionProposal>() {
|
||||
@Override
|
||||
public ScoreableProposal deemphasize(double howmuch) {
|
||||
Assert.isLegal(howmuch>0.0);
|
||||
deemphasizedBy+= howmuch*DEEMP_VALUE;
|
||||
return this;
|
||||
}
|
||||
public boolean isDeemphasized() {
|
||||
return deemphasizedBy > 0;
|
||||
public int compare(ICompletionProposal p1, ICompletionProposal p2) {
|
||||
if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) {
|
||||
double s1 = ((ScoreableProposal)p1).getScore();
|
||||
double s2 = ((ScoreableProposal)p2).getScore();
|
||||
if (Math.abs(s1-s2)<1E-5) {
|
||||
String name1 = ((ScoreableProposal)p1).getLabel();
|
||||
String name2 = ((ScoreableProposal)p2).getLabel();
|
||||
return name1.compareTo(name2);
|
||||
} else {
|
||||
return Double.compare(s2, s1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
public abstract double getBaseScore();
|
||||
public final double getScore() {
|
||||
return getBaseScore() - deemphasizedBy;
|
||||
}
|
||||
@Override
|
||||
public ScoreableProposal deemphasize(double howmuch) {
|
||||
Assert.isLegal(howmuch>0.0);
|
||||
deemphasizedBy+= howmuch*DEEMP_VALUE;
|
||||
return this;
|
||||
}
|
||||
public boolean isDeemphasized() {
|
||||
return deemphasizedBy > 0;
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public boolean isAutoInsertable() {
|
||||
@@ -125,9 +125,9 @@ public abstract class ScoreableProposal implements ICompletionProposal {
|
||||
// return completionOffset;
|
||||
// }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getLabel();
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return getLabel();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.springframework.ide.vscode.commons.languageserver.completion.Document
|
||||
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.SortKeys;
|
||||
import org.springframework.ide.vscode.commons.util.Futures;
|
||||
import org.springframework.ide.vscode.commons.util.Renderable;
|
||||
import org.springframework.ide.vscode.commons.util.StringUtil;
|
||||
import org.springframework.ide.vscode.commons.util.text.TextDocument;
|
||||
@@ -154,6 +153,6 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
//TODO: item is pre-resoved so we don't do anything, but we really should somehow defer some work, such as
|
||||
// for example computing docs and edits to resolve time.
|
||||
//The tricky part is that we have to probably remember infos about the unresolved elements somehow so we can resolve later.
|
||||
return Futures.of(unresolved);
|
||||
return CompletableFuture.completedFuture(unresolved);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* 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.quickfix;
|
||||
|
||||
import org.eclipse.lsp4j.Position;
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
|
||||
public class QuickfixEdit {
|
||||
|
||||
public static class CursorMovement {
|
||||
private String uri;
|
||||
private Position position;
|
||||
|
||||
public CursorMovement() {
|
||||
}
|
||||
|
||||
public CursorMovement(String uri, Position position) {
|
||||
this.uri = uri;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void setUri(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(Position position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final WorkspaceEdit workspaceEdit;
|
||||
public final CursorMovement cursorMovement;
|
||||
|
||||
public QuickfixEdit(WorkspaceEdit workspaceEdit, CursorMovement cursorMovement) {
|
||||
this.workspaceEdit = workspaceEdit;
|
||||
this.cursorMovement = cursorMovement;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,7 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.languageserver.quickfix;
|
||||
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QuickfixHandler {
|
||||
WorkspaceEdit createEdits(Object params);
|
||||
QuickfixEdit createEdits(Object params);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ package org.springframework.ide.vscode.commons.languageserver.quickfix;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
|
||||
import org.springframework.ide.vscode.commons.util.Assert;
|
||||
|
||||
@@ -40,7 +39,7 @@ public class QuickfixRegistry {
|
||||
return new QuickfixType() {
|
||||
|
||||
@Override
|
||||
public WorkspaceEdit createEdits(Object params) {
|
||||
public QuickfixEdit createEdits(Object params) {
|
||||
return handler.createEdits(params);
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ public class QuickfixRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
public Mono<WorkspaceEdit> handle(QuickfixResolveParams params) {
|
||||
public Mono<QuickfixEdit> handle(QuickfixResolveParams params) {
|
||||
QuickfixHandler handler = registry.get(params.getType());
|
||||
return Mono.fromSupplier(() -> {
|
||||
return handler.createEdits(params.getParams());
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
|
||||
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
|
||||
import org.eclipse.lsp4j.CompletionOptions;
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.eclipse.lsp4j.DiagnosticSeverity;
|
||||
@@ -31,7 +32,6 @@ import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.ServerCapabilities;
|
||||
import org.eclipse.lsp4j.TextDocumentIdentifier;
|
||||
import org.eclipse.lsp4j.TextDocumentSyncKind;
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.LanguageClientAware;
|
||||
import org.eclipse.lsp4j.services.LanguageServer;
|
||||
@@ -40,6 +40,7 @@ import org.springframework.ide.vscode.commons.languageserver.ProgressService;
|
||||
import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixResolveParams;
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
|
||||
@@ -124,15 +125,20 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl
|
||||
(String)params.getArguments().get(0), params.getArguments().get(1)
|
||||
);
|
||||
return quickfixResolve(quickfixParams)
|
||||
.then((WorkspaceEdit edit) -> Mono.fromFuture(client.applyEdit(new ApplyWorkspaceEditParams(edit))))
|
||||
.map(r -> (Object)r.getApplied())
|
||||
.toFuture();
|
||||
.then((QuickfixEdit edit) -> {
|
||||
Mono<ApplyWorkspaceEditResponse> applyEdit = Mono.fromFuture(client.applyEdit(new ApplyWorkspaceEditParams(edit.workspaceEdit)));
|
||||
Mono<Object> moveCursor = edit.cursorMovement==null
|
||||
? Mono.just(new ApplyWorkspaceEditResponse(true))
|
||||
: Mono.fromFuture(client.moveCursor(edit.cursorMovement));
|
||||
return applyEdit.then(r -> r.getApplied() ? moveCursor : Mono.just(new ApplyWorkspaceEditResponse(true)));
|
||||
})
|
||||
.toFuture();
|
||||
}
|
||||
Log.warn("Unknown command ignored: "+params.getCommand());
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
public Mono<WorkspaceEdit> quickfixResolve(QuickfixResolveParams params) {
|
||||
public Mono<QuickfixEdit> quickfixResolve(QuickfixResolveParams params) {
|
||||
QuickfixRegistry quickfixes = getQuickfixRegistry();
|
||||
return quickfixes.handle(params);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
public class Unicodes {
|
||||
|
||||
public static final char RIGHT_ARROW = '→';
|
||||
public static final char LEFT_ARROW = '←';
|
||||
|
||||
}
|
||||
@@ -44,19 +44,33 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext {
|
||||
private final YamlDocument doc;
|
||||
|
||||
|
||||
@Override
|
||||
public YamlDocument getDocument() {
|
||||
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.
|
||||
}
|
||||
|
||||
private static PrefixFinder prefixfinder = new PrefixFinder() {
|
||||
@Override
|
||||
protected boolean isPrefixChar(char c) {
|
||||
return !Character.isWhitespace(c);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public YamlDocument getDocument() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
protected final String getPrefix(YamlDocument doc, SNode node, int offset) {
|
||||
|
||||
protected 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.
|
||||
// instead we should use offset in current node / structure to determine the
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,12 +27,14 @@ public abstract class TransformedCompletion extends ScoreableProposal {
|
||||
protected final ICompletionProposal original;
|
||||
|
||||
private DocumentEdits transformedEdit = null;
|
||||
|
||||
|
||||
public TransformedCompletion(ICompletionProposal proposal) {
|
||||
this.original = proposal;
|
||||
}
|
||||
|
||||
protected abstract String tranformLabel(String originalLabel);
|
||||
protected String tranformLabel(String originalLabel) {
|
||||
return originalLabel;
|
||||
}
|
||||
protected abstract DocumentEdits transformEdit(DocumentEdits textEdit);
|
||||
|
||||
@Override
|
||||
@@ -71,4 +73,9 @@ public abstract class TransformedCompletion extends ScoreableProposal {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterText() {
|
||||
return original.getFilterText();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -29,29 +28,25 @@ import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
|
||||
import org.springframework.ide.vscode.commons.util.Log;
|
||||
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;
|
||||
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;
|
||||
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.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.util.Streams;
|
||||
import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
|
||||
|
||||
import org.springframework.ide.vscode.commons.yaml.completion.DefaultCompletionFactory.ValueProposal;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*;
|
||||
|
||||
public class YTypeAssistContext extends AbstractYamlAssistContext {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(YTypeAssistContext.class);
|
||||
@@ -87,12 +82,23 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
|
||||
|
||||
@Override
|
||||
public Collection<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
|
||||
String query = getPrefix(doc, node, offset);
|
||||
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, node, offset, query);
|
||||
if (!valueCompletions.isEmpty()) {
|
||||
return valueCompletions;
|
||||
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));
|
||||
}
|
||||
}
|
||||
return getKeyCompletions(doc, offset, query);
|
||||
String query = getPrefix(doc, node, offset);
|
||||
List<ICompletionProposal> completions = getValueCompletions(doc, node, offset, query);
|
||||
if (completions.isEmpty()) {
|
||||
completions = getKeyCompletions(doc, offset, query);
|
||||
}
|
||||
if (typeUtil.isSequencable(type)) {
|
||||
completions = new ArrayList<>(completions);
|
||||
completions.addAll(getDashedCompletions(doc, node, offset));
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
public List<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception {
|
||||
@@ -123,17 +129,18 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
|
||||
query, p, score, edits, typeUtil)
|
||||
);
|
||||
} else {
|
||||
//property already defined
|
||||
// instead of filtering, navigate to the place where its defined.
|
||||
deleteQueryAndLine(doc, query, queryOffset, edits);
|
||||
//Cast to SChildBearingNode cannot fail because otherwise definedProps would be the empty set.
|
||||
edits.createPath((SChildBearingNode) contextNode, relativePath, "");
|
||||
proposals.add(
|
||||
completionFactory().beanProperty(doc.getDocument(),
|
||||
contextPath.toPropString(), getType(),
|
||||
query, p, score, edits, typeUtil)
|
||||
.deemphasize(DEEMP_EXISTS) //deemphasize because it already exists
|
||||
);
|
||||
// This piece below deactivated becuase moving cursor like this doesn't work in vscode
|
||||
// //property already defined
|
||||
// // instead of filtering, navigate to the place where its defined.
|
||||
// deleteQueryAndLine(doc, query, queryOffset, edits);
|
||||
// //Cast to SChildBearingNode cannot fail because otherwise definedProps would be the empty set.
|
||||
// edits.createPath((SChildBearingNode) contextNode, relativePath, "");
|
||||
// proposals.add(
|
||||
// completionFactory().beanProperty(doc.getDocument(),
|
||||
// contextPath.toPropString(), getType(),
|
||||
// query, p, score, edits, typeUtil)
|
||||
// .deemphasize(DEEMP_EXISTS) //deemphasize because it already exists
|
||||
// );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,8 +302,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
|
||||
return typeUtil.getPropertiesMap(getType()).get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlAssistContext relax() {
|
||||
protected YamlAssistContext relaxForDashes() {
|
||||
try {
|
||||
if (typeUtil.isSequencable(type)) {
|
||||
YType itemType = typeUtil.getDomainType(type);
|
||||
@@ -313,7 +319,19 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return super.relax();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Collection<ICompletionProposal> getDashedCompletions(YamlDocument doc, SNode current, int offset) {
|
||||
try {
|
||||
YamlAssistContext relaxed = relaxForDashes();
|
||||
if (relaxed!=null) {
|
||||
return relaxed.getCompletions(doc, current, offset);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
private Collection<ICompletionProposal> addDashes(Collection<ICompletionProposal> basicCompletions, YamlDocument doc, SNode node) {
|
||||
|
||||
@@ -34,13 +34,4 @@ public interface YamlAssistContext extends YamlNavigable<YamlAssistContext> {
|
||||
Renderable getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion);
|
||||
|
||||
YamlDocument getDocument();
|
||||
|
||||
/**
|
||||
* Allows a context to implement a 'relaxation' transformation. A relaxed context
|
||||
* should compute the same proposals as the original context but may also add
|
||||
* additional proposals. E.g. a {@link YTypeAssistContext} uses this to
|
||||
* relax contexts for sequence types to include proposals for the elements
|
||||
* of the sequence.
|
||||
*/
|
||||
default YamlAssistContext relax() { return null; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Pivotal, Inc.
|
||||
* Copyright (c) 2016-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
|
||||
@@ -10,12 +10,15 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.yaml.completion;
|
||||
|
||||
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DEDENTED_PROPOSAL;
|
||||
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_INDENTED_PROPOSAL;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -23,11 +26,14 @@ import org.springframework.ide.vscode.commons.languageserver.completion.Document
|
||||
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.completion.ScoreableProposal;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
|
||||
import org.springframework.ide.vscode.commons.util.Assert;
|
||||
import org.springframework.ide.vscode.commons.util.Log;
|
||||
import org.springframework.ide.vscode.commons.util.Unicodes;
|
||||
import org.springframework.ide.vscode.commons.util.text.IDocument;
|
||||
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
|
||||
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.SKeyNode;
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNodeType;
|
||||
@@ -36,13 +42,18 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser
|
||||
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider;
|
||||
import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
/**
|
||||
* Implements {@link ICompletionEngine} for .yml file, based on a YamlAssistContextProvider
|
||||
* which has to to be injected into engine via its contructor.
|
||||
* which has to to be injected into engine via its constructor.
|
||||
*
|
||||
* @author Kris De Volder
|
||||
*/
|
||||
public class YamlCompletionEngine implements ICompletionEngine {
|
||||
|
||||
Pattern SPACES = Pattern.compile("[ ]+");
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(YamlCompletionEngine.class);
|
||||
|
||||
@@ -71,65 +82,154 @@ public class YamlCompletionEngine implements ICompletionEngine {
|
||||
if (!doc.isCommented(offset)) {
|
||||
SRootNode root = doc.getStructure();
|
||||
SNode current = root.find(offset);
|
||||
SNode contextNode = getContextNode(doc, current, offset);
|
||||
List<ICompletionProposal> all = new ArrayList<>(getBaseCompletions(offset, doc, current, contextNode, false));
|
||||
all.addAll(getMoreIndentedCompletions(offset, doc, contextNode, current));
|
||||
all.addAll(getDashedCompletions(offset, doc, contextNode, current));
|
||||
return all;
|
||||
List<SNode> contextNodes = getContextNodes(doc, current, offset);
|
||||
if (current.getNodeType()==SNodeType.RAW) {
|
||||
//relaxed indentation
|
||||
List<ICompletionProposal> completions = new ArrayList<>();
|
||||
int cursorIndent = doc.getColumn(offset);
|
||||
int nodeIndent = current.getIndent();
|
||||
int baseIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
|
||||
for (SNode contextNode : contextNodes) {
|
||||
completions.addAll(getRelaxedCompletions(offset, doc, current, contextNode, baseIndent));
|
||||
}
|
||||
return completions;
|
||||
} else {
|
||||
//precise indentation only
|
||||
Assert.isLegal(contextNodes.size()<=1);
|
||||
for (SNode contextNode : contextNodes) {
|
||||
return getBaseCompletions(offset, doc, current, contextNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private Collection<ICompletionProposal> addIndentations(
|
||||
Collection<ICompletionProposal> completions) {
|
||||
protected Collection<? extends ICompletionProposal> getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent) {
|
||||
try {
|
||||
return fixIndentations(getBaseCompletions(offset, doc, current, contextNode),
|
||||
current, contextNode, baseIndent);
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
protected Collection<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> completions, SNode currentNode, SNode contextNode, int baseIndent) {
|
||||
if (!completions.isEmpty()) {
|
||||
int dashyIndent = getTargetIndent(contextNode, currentNode, true);
|
||||
int plainIndent = getTargetIndent(contextNode, currentNode, false);
|
||||
List<ICompletionProposal> transformed = new ArrayList<>();
|
||||
for (ICompletionProposal p : completions) {
|
||||
transformed.add(indented(p));
|
||||
ICompletionProposal p_fixed = null;
|
||||
if (p.getLabel().startsWith("- ")) {
|
||||
p_fixed = indentFix(p, dashyIndent - baseIndent, currentNode, contextNode);
|
||||
} else {
|
||||
p_fixed = indentFix(p, plainIndent - baseIndent, currentNode, contextNode);
|
||||
}
|
||||
if (p_fixed!=null) {
|
||||
transformed.add(p_fixed);
|
||||
}
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public ICompletionProposal indented(ICompletionProposal proposal) {
|
||||
protected ICompletionProposal indentFix(ICompletionProposal p, int fixIndentBy, SNode currentNode, SNode contextNode) {
|
||||
if (fixIndentBy==0) {
|
||||
return p;
|
||||
} else if (fixIndentBy>0) {
|
||||
if (isExtraIndentRelaxable(contextNode)) {
|
||||
return indented(p, Strings.repeat(" ", fixIndentBy));
|
||||
}
|
||||
} else { // fixIndentBy < 0
|
||||
if (isLesserIndentRelaxable(currentNode, contextNode)) {
|
||||
return dedented(p, -fixIndentBy, contextNode.getDocument());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean isLesserIndentRelaxable(final SNode currentNode, final SNode contextNode) {
|
||||
SChildBearingNode parent = currentNode.getParent();
|
||||
while (parent!=null && parent!=contextNode) {
|
||||
SNode lastChild = parent.getLastRealChild();
|
||||
if (lastChild!=null && lastChild.getStart()>=currentNode.getNodeEnd()) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private int getTargetIndent(SNode contextNode, SNode currentNode, boolean dashy) {
|
||||
Optional<SNode> child = Optional.empty();
|
||||
if (contextNode instanceof SChildBearingNode) {
|
||||
child = ((SChildBearingNode)contextNode).getChildren().stream()
|
||||
.filter(c -> c!=currentNode && c.getIndent()>=0)
|
||||
.max((c1, c2) -> Integer.compare(c1.getIndent(), c2.getIndent()));
|
||||
}
|
||||
if (child.isPresent()) {
|
||||
return child.get().getIndent();
|
||||
}
|
||||
return (dashy || contextNode.getNodeType()==SNodeType.DOC)
|
||||
? contextNode.getIndent()
|
||||
: contextNode.getIndent() + YamlIndentUtil.INDENT_BY;
|
||||
}
|
||||
|
||||
public ICompletionProposal dedented(ICompletionProposal proposal, int numSpacesToRemove, IDocument doc) {
|
||||
Assert.isLegal(numSpacesToRemove>0);
|
||||
int spacesEnd = proposal.getTextEdit().getFirstEditStart();
|
||||
int spacesStart = spacesEnd-numSpacesToRemove;
|
||||
int numArrows = numSpacesToRemove / YamlIndentUtil.INDENT_BY;
|
||||
String spaces = new DocumentRegion(doc, spacesStart, spacesEnd).toString();
|
||||
if (spaces.length()==numSpacesToRemove && SPACES.matcher(spaces).matches()) {
|
||||
ScoreableProposal transformed = new TransformedCompletion(proposal) {
|
||||
@Override public String tranformLabel(String originalLabel) {
|
||||
return Strings.repeat(Unicodes.LEFT_ARROW+" ", numArrows) + originalLabel;
|
||||
}
|
||||
@Override public DocumentEdits transformEdit(DocumentEdits originalEdit) {
|
||||
originalEdit.firstDelete(spacesStart, spacesEnd);
|
||||
return originalEdit;
|
||||
}
|
||||
@Override
|
||||
public String getFilterText() {
|
||||
//If we don't add the spaces, vscode won't show the completions.
|
||||
// Presumably this is because it matches the filtter text to the text it thinks its going
|
||||
// to replace. Since we are replacing these removed spaces, they must be part of the filtertext
|
||||
return spaces + super.getFilterText();
|
||||
}
|
||||
};
|
||||
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.)
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public ICompletionProposal indented(ICompletionProposal proposal, String indentStr) {
|
||||
ScoreableProposal transformed = new TransformedCompletion(proposal) {
|
||||
@Override public String tranformLabel(String originalLabel) {
|
||||
return "➔ " + originalLabel;
|
||||
return Unicodes.RIGHT_ARROW+" " + originalLabel;
|
||||
}
|
||||
@Override public DocumentEdits transformEdit(DocumentEdits originalEdit) {
|
||||
originalEdit.indentFirstEdit(YamlIndentUtil.INDENT_STR);
|
||||
originalEdit.indentFirstEdit(indentStr);
|
||||
return originalEdit;
|
||||
}
|
||||
};
|
||||
transformed.deemphasize(DEEMP_INDENTED_PROPOSAL);
|
||||
transformed.deemphasize(DEEMP_INDENTED_PROPOSAL*indentStr.length()/2);
|
||||
return transformed;
|
||||
}
|
||||
|
||||
private Collection<? extends ICompletionProposal> getDashedCompletions(
|
||||
int offset, YamlDocument doc,
|
||||
SNode preciseContextNode, SNode currentNode
|
||||
) throws Exception {
|
||||
SNode contextNode = getContextNode(doc, currentNode, offset, "- ");
|
||||
if (preciseContextNode!=contextNode) {
|
||||
return getBaseCompletions(offset, doc, currentNode, contextNode, true);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
private Collection<ICompletionProposal> getMoreIndentedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception {
|
||||
SNode contextNode = getContextNode(doc, currentNode, offset, YamlIndentUtil.INDENT_STR);
|
||||
if (preciseContextNode!=contextNode && isIndentRelaxable(contextNode)) {
|
||||
YamlAssistContext context = getContext(doc, contextNode);
|
||||
if (context!=null) {
|
||||
return addIndentations(context.getCompletions(doc, currentNode, offset));
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private boolean isIndentRelaxable(SNode contextNode) throws Exception {
|
||||
private boolean isExtraIndentRelaxable(SNode contextNode) {
|
||||
return contextNode!=null && (
|
||||
isBarrenKey(contextNode) ||
|
||||
isBarrenSeq(contextNode)
|
||||
@@ -149,16 +249,20 @@ public class YamlCompletionEngine implements ICompletionEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isBarrenKey(SNode node) throws Exception {
|
||||
if (node.getNodeType()==SNodeType.KEY) {
|
||||
SKeyNode keyNode = (SKeyNode) node;
|
||||
String value = keyNode.getSimpleValue();
|
||||
return value.trim().isEmpty();
|
||||
private boolean isBarrenKey(SNode node) {
|
||||
try {
|
||||
if (node.getNodeType()==SNodeType.KEY) {
|
||||
SKeyNode keyNode = (SKeyNode) node;
|
||||
String value = keyNode.getSimpleValue();
|
||||
return value.trim().isEmpty();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Collection<? extends ICompletionProposal> getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, boolean onlyDashes) throws Exception {
|
||||
private Collection<ICompletionProposal> getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode) throws Exception {
|
||||
if (contextNode!=null) {
|
||||
YamlAssistContext context = getContext(doc, contextNode);
|
||||
if (context==null && isDubiousKey(contextNode, offset)) {
|
||||
@@ -167,15 +271,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
|
||||
context = getContext(doc, contextNode);
|
||||
}
|
||||
if (context!=null) {
|
||||
Collection<ICompletionProposal> all = new ArrayList<>();
|
||||
if (!onlyDashes) {
|
||||
all.addAll(context.getCompletions(doc, current, offset));
|
||||
}
|
||||
YamlAssistContext relaxedContext = context.relax();
|
||||
if (relaxedContext!=null) {
|
||||
all.addAll(relaxedContext.getCompletions(doc, current, offset));
|
||||
}
|
||||
return all;
|
||||
return context.getCompletions(doc, current, offset);
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
@@ -205,8 +301,15 @@ public class YamlCompletionEngine implements ICompletionEngine {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected SNode getContextNode(YamlDocument doc, SNode node, int offset, String adjustIndentStr) throws Exception {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* single node. (Note we may still return a singleton list for cases where relaxed indentation
|
||||
* doesn't seem desirable).
|
||||
*/
|
||||
protected List<SNode> getContextNodes(YamlDocument doc, SNode node, int offset) {
|
||||
if (node==null) {
|
||||
return null;
|
||||
} else if (node.getNodeType()==SNodeType.KEY) {
|
||||
@@ -214,62 +317,98 @@ public class YamlCompletionEngine implements ICompletionEngine {
|
||||
// contexts for content assistance
|
||||
SKeyNode keyNode = (SKeyNode)node;
|
||||
if (keyNode.isInValue(offset)) {
|
||||
return keyNode;
|
||||
return ImmutableList.of(keyNode);
|
||||
} else {
|
||||
return keyNode.getParent();
|
||||
}
|
||||
} else if (node.getNodeType()==SNodeType.RAW) {
|
||||
if (adjustIndentStr.startsWith("- ")) {
|
||||
// We are trying to determine context node for a completion that starts witjh a '- '.
|
||||
// Yaml indentation rules means we have to treat this differently because '-' doesn't
|
||||
// have to be indented to be considered as nested under a key node!
|
||||
int cursorIndent = doc.getColumn(offset);
|
||||
int nodeIndent = node.getIndent();
|
||||
int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
|
||||
while (node.getNodeType()!=SNodeType.DOC && (
|
||||
nodeIndent==-1 ||
|
||||
nodeIndent>currentIndent ||
|
||||
nodeIndent==currentIndent && (node.getNodeType()==SNodeType.SEQ || node.getNodeType() == SNodeType.RAW)
|
||||
)) {
|
||||
node = node.getParent();
|
||||
nodeIndent = node.getIndent();
|
||||
}
|
||||
return node;
|
||||
} else {
|
||||
//Treat raw node as a 'key node'. This is basically assuming that is misclasified
|
||||
// by structure parser because the ':' was not yet typed into the document.
|
||||
|
||||
//Complication: if line with cursor is empty or the cursor is inside the indentation
|
||||
// area then the structure may not reflect correctly the context. This is because
|
||||
// the correct context depends on text the user has not typed yet.(which will change the
|
||||
// indentation level of the current line. So we must use the cursorIndentation
|
||||
// rather than the structure-tree to determine the 'context' node.
|
||||
int adjustIndent = adjustIndentStr==null? 0 : adjustIndentStr.length();
|
||||
int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent);
|
||||
int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent);
|
||||
int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
|
||||
while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) {
|
||||
node = node.getParent();
|
||||
nodeIndent = node.getIndent();
|
||||
}
|
||||
return node;
|
||||
return ImmutableList.of(keyNode.getParent());
|
||||
}
|
||||
} else if (node.getNodeType()==SNodeType.SEQ) {
|
||||
SSeqNode seqNode = (SSeqNode)node;
|
||||
if (seqNode.isInValue(offset)) {
|
||||
return seqNode;
|
||||
return ImmutableList.of(seqNode);
|
||||
} else {
|
||||
return seqNode.getParent();
|
||||
return ImmutableList.of(seqNode.getParent());
|
||||
}
|
||||
} else if (node.getNodeType()==SNodeType.DOC) {
|
||||
return node;
|
||||
return ImmutableList.of(node);
|
||||
} else if (node.getNodeType()==SNodeType.RAW) {
|
||||
//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 must be kept.
|
||||
if (node.getSegment()!=null) {
|
||||
contextNodes.add(node);
|
||||
}
|
||||
node = node.getParent();
|
||||
}
|
||||
return contextNodes.build();
|
||||
}
|
||||
return null;
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
protected SNode getContextNode(YamlDocument doc, SNode node, int offset) throws Exception {
|
||||
return getContextNode(doc, node, offset, "");
|
||||
}
|
||||
// protected SNode getContextNode(YamlDocument doc, SNode node, int offset, String adjustIndentStr) throws Exception {
|
||||
// if (node==null) {
|
||||
// return null;
|
||||
// } else if (node.getNodeType()==SNodeType.KEY) {
|
||||
// //slight complication. The area in the key and value of a key node represent different
|
||||
// // contexts for content assistance
|
||||
// SKeyNode keyNode = (SKeyNode)node;
|
||||
// if (keyNode.isInValue(offset)) {
|
||||
// return keyNode;
|
||||
// } else {
|
||||
// return keyNode.getParent();
|
||||
// }
|
||||
// } else if (node.getNodeType()==SNodeType.RAW) {
|
||||
// if (adjustIndentStr.startsWith("- ")) {
|
||||
// // We are trying to determine context node for a completion that starts with a '- '.
|
||||
// // Yaml indentation rules means we have to treat this differently because '-' doesn't
|
||||
// // have to be indented to be considered as nested under a key node!
|
||||
// int cursorIndent = doc.getColumn(offset);
|
||||
// int nodeIndent = node.getIndent();
|
||||
// int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
|
||||
// while (node.getNodeType()!=SNodeType.DOC && (
|
||||
// nodeIndent==-1 ||
|
||||
// nodeIndent>currentIndent ||
|
||||
// nodeIndent==currentIndent && (node.getNodeType()==SNodeType.SEQ || node.getNodeType() == SNodeType.RAW)
|
||||
// )) {
|
||||
// node = node.getParent();
|
||||
// nodeIndent = node.getIndent();
|
||||
// }
|
||||
// return node;
|
||||
// } else {
|
||||
// //Treat raw node as a 'key node'. This is basically assuming that is misclasified
|
||||
// // by structure parser because the ':' was not yet typed into the document.
|
||||
//
|
||||
// //Complication: if line with cursor is empty or the cursor is inside the indentation
|
||||
// // area then the structure may not reflect correctly the context. This is because
|
||||
// // the correct context depends on text the user has not typed yet.(which will change the
|
||||
// // indentation level of the current line. So we must use the cursorIndentation
|
||||
// // rather than the structure-tree to determine the 'context' node.
|
||||
// int adjustIndent = adjustIndentStr==null? 0 : adjustIndentStr.length();
|
||||
// int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent);
|
||||
// int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent);
|
||||
// int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
|
||||
// while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) {
|
||||
// node = node.getParent();
|
||||
// nodeIndent = node.getIndent();
|
||||
// }
|
||||
// return node;
|
||||
// }
|
||||
// } else if (node.getNodeType()==SNodeType.SEQ) {
|
||||
// SSeqNode seqNode = (SSeqNode)node;
|
||||
// if (seqNode.isInValue(offset)) {
|
||||
// return seqNode;
|
||||
// } else {
|
||||
// return seqNode.getParent();
|
||||
// }
|
||||
// } else if (node.getNodeType()==SNodeType.DOC) {
|
||||
// return node;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// protected SNode getContextNode(YamlDocument doc, SNode node, int offset) throws Exception {
|
||||
// return getContextNode(doc, node, offset, "");
|
||||
// }
|
||||
|
||||
protected YamlPath getContextPath(YamlDocument doc, SNode node, int offset) throws Exception {
|
||||
if (node==null) {
|
||||
|
||||
@@ -43,6 +43,16 @@ public interface YamlTraversal {
|
||||
*/
|
||||
<T extends YamlNavigable<T>> T traverse(T startNode);
|
||||
|
||||
default Node traverseNode(Node root) {
|
||||
if (root!=null) {
|
||||
ASTCursor cursor = traverse(new NodeCursor(root));
|
||||
if (cursor instanceof NodeCursor) {
|
||||
return ((NodeCursor)cursor).getNode();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
default Node traverseToNode(YamlFileAST root) {
|
||||
ASTCursor cursor = traverse(new ASTRootCursor(root));
|
||||
if (cursor instanceof NodeCursor) {
|
||||
|
||||
@@ -10,13 +10,16 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.yaml.quickfix;
|
||||
|
||||
import org.eclipse.lsp4j.Position;
|
||||
import org.eclipse.lsp4j.TextEdit;
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
|
||||
import org.springframework.ide.vscode.commons.util.Log;
|
||||
import org.springframework.ide.vscode.commons.util.text.IRegion;
|
||||
import org.springframework.ide.vscode.commons.util.text.TextDocument;
|
||||
import org.springframework.ide.vscode.commons.yaml.completion.YamlPathEdits;
|
||||
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
|
||||
@@ -32,8 +35,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement;
|
||||
|
||||
public class YamlQuickfixes {
|
||||
|
||||
private static final QuickfixEdit NULL_FIX = new QuickfixEdit(
|
||||
new WorkspaceEdit(ImmutableMap.of(), null),
|
||||
null
|
||||
);
|
||||
public final QuickfixType MISSING_PROP_FIX;
|
||||
public final QuickfixType SIMPLE_TEXT_EDIT;
|
||||
|
||||
@@ -53,6 +62,7 @@ public class YamlQuickfixes {
|
||||
SChildBearingNode target = (SChildBearingNode) _target;
|
||||
for (String prop : params.getProps()) {
|
||||
edits.createPath(target, new YamlPath(YamlPathSegment.valueAt(prop)), " ");
|
||||
edits.freezeCursor();
|
||||
}
|
||||
TextReplace replaceEdit = edits.asReplacement(_doc);
|
||||
if (replaceEdit!=null) {
|
||||
@@ -61,7 +71,8 @@ public class YamlQuickfixes {
|
||||
params.getUri(),
|
||||
ImmutableList.of(new TextEdit(_doc.toRange(replaceEdit.getRegion()), replaceEdit.newText))
|
||||
));
|
||||
return wsEdits;
|
||||
Position newCursor = getCursorPostionAfter(_doc, edits);
|
||||
return new QuickfixEdit(wsEdits, newCursor==null ? null : new CursorMovement(params.getUri(), newCursor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +81,7 @@ public class YamlQuickfixes {
|
||||
Log.log(e);
|
||||
}
|
||||
//Something went wrong. Return empty edit object.
|
||||
return new WorkspaceEdit(ImmutableMap.of(), null);
|
||||
return NULL_FIX;
|
||||
});
|
||||
|
||||
SIMPLE_TEXT_EDIT = r.register("SIMPLE_TEXT_EDIT", (_params) -> {
|
||||
@@ -78,9 +89,12 @@ public class YamlQuickfixes {
|
||||
ReplaceStringData params = new ObjectMapper().convertValue(_params, ReplaceStringData.class);
|
||||
TextDocument _doc = textDocumentService.getDocument(params.getUri());
|
||||
if (_doc!=null) {
|
||||
return new WorkspaceEdit(
|
||||
return new QuickfixEdit(
|
||||
new WorkspaceEdit(
|
||||
ImmutableMap.of(params.getUri(), ImmutableList.of(params.getEdit())),
|
||||
null
|
||||
),
|
||||
null //TODO: compute end of the range after applying the edit
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -88,8 +102,26 @@ public class YamlQuickfixes {
|
||||
}
|
||||
//Something went wrong. Return empty edit object.
|
||||
//Something went wrong. Return empty edit object.
|
||||
return new WorkspaceEdit(ImmutableMap.of(), null);
|
||||
return NULL_FIX;
|
||||
});
|
||||
}
|
||||
|
||||
private Position getCursorPostionAfter(TextDocument _doc, YamlPathEdits edits) {
|
||||
try {
|
||||
IRegion newSelection = edits.getSelection();
|
||||
if (newSelection!=null) {
|
||||
//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.
|
||||
TextDocument doc = _doc.copy();
|
||||
edits.apply(doc);
|
||||
return doc.toPosition(newSelection.getOffset());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
if (nodes!=null && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), /*parent*/null, node, schema.getTopLevelType());
|
||||
reconcile(ast, new YamlPath(YamlPathSegment.valueAt(i)), /*parent*/null, node, schema.getTopLevelType());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -121,9 +121,10 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
return allOf(ast, node);
|
||||
}
|
||||
|
||||
private void reconcile(IDocument doc, YamlPath path, Node parent, Node node, YType type) {
|
||||
private void reconcile(YamlFileAST ast, YamlPath path, Node parent, Node node, YType type) {
|
||||
// IDocument doc = ast.getDocument();
|
||||
if (type!=null) {
|
||||
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(doc, path, node);
|
||||
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(ast, path, node);
|
||||
type = typeUtil.inferMoreSpecificType(type, schemaContext);
|
||||
if (typeCollector!=null) {
|
||||
typeCollector.accept(node, type);
|
||||
@@ -136,8 +137,8 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
if (typeUtil.isMap(type)) {
|
||||
for (NodeTuple entry : map.getValue()) {
|
||||
String key = NodeUtil.asScalar(entry.getKeyNode());
|
||||
reconcile(doc, keyAt(path, key), map, entry.getKeyNode(), typeUtil.getKeyType(type));
|
||||
reconcile(doc, valueAt(path, key), map, entry.getValueNode(), typeUtil.getDomainType(type));
|
||||
reconcile(ast, keyAt(path, key), map, entry.getKeyNode(), typeUtil.getKeyType(type));
|
||||
reconcile(ast, valueAt(path, key), map, entry.getValueNode(), typeUtil.getDomainType(type));
|
||||
}
|
||||
} else if (typeUtil.isBean(type)) {
|
||||
Map<String, YTypedProperty> beanProperties = typeUtil.getPropertiesMap(type);
|
||||
@@ -155,7 +156,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
if (prop.isDeprecated()) {
|
||||
problems.accept(YamlSchemaProblems.deprecatedProperty(keyNode, type, prop));
|
||||
}
|
||||
reconcile(doc, valueAt(path, key), map, entry.getValueNode(), prop.getType());
|
||||
reconcile(ast, valueAt(path, key), map, entry.getValueNode(), prop.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +169,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
if (typeUtil.isSequencable(type)) {
|
||||
for (int i = 0; i < seq.getValue().size(); i++) {
|
||||
Node el = seq.getValue().get(i);
|
||||
reconcile(doc, valueAt(path, i), seq, el, typeUtil.getDomainType(type));
|
||||
reconcile(ast, valueAt(path, i), seq, el, typeUtil.getDomainType(type));
|
||||
}
|
||||
} else {
|
||||
expectTypeButFoundSequence(type, node);
|
||||
@@ -185,7 +186,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ProblemType problemType = getProblemType(e);
|
||||
DocumentRegion region = getRegion(e, doc, node);
|
||||
DocumentRegion region = getRegion(e, ast.getDocument(), node);
|
||||
String msg = getMessage(e);
|
||||
valueParseError(type, region, msg, problemType, getValueReplacement(e));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.Set;
|
||||
|
||||
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.ast.YamlFileAST;
|
||||
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
|
||||
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
@@ -27,11 +28,11 @@ import org.yaml.snakeyaml.nodes.Node;
|
||||
public class ASTDynamicSchemaContext extends CachingSchemaContext {
|
||||
|
||||
private MappingNode mapNode;
|
||||
private IDocument doc;
|
||||
private YamlPath path;
|
||||
private YamlFileAST ast;
|
||||
|
||||
public ASTDynamicSchemaContext(IDocument doc, YamlPath path, Node node) {
|
||||
this.doc = doc;
|
||||
public ASTDynamicSchemaContext(YamlFileAST ast, YamlPath path, Node node) {
|
||||
this.ast = ast;
|
||||
this.path = path;
|
||||
this.mapNode = as(MappingNode.class, node);
|
||||
}
|
||||
@@ -51,11 +52,16 @@ public class ASTDynamicSchemaContext extends CachingSchemaContext {
|
||||
|
||||
@Override
|
||||
public IDocument getDocument() {
|
||||
return doc;
|
||||
return ast.getDocument();
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlPath getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlFileAST getAST() {
|
||||
return ast;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.yaml.schema;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ide.vscode.commons.util.text.IDocument;
|
||||
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
|
||||
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -30,6 +31,7 @@ import com.google.common.collect.ImmutableSet;
|
||||
public interface DynamicSchemaContext {
|
||||
|
||||
DynamicSchemaContext NULL = new DynamicSchemaContext() {
|
||||
|
||||
@Override
|
||||
public Set<String> getDefinedProperties() {
|
||||
return ImmutableSet.of();
|
||||
@@ -47,6 +49,12 @@ public interface DynamicSchemaContext {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the enitre AST of the current document. May be null if the AST is not
|
||||
* available (e.g. because of parsing errors)
|
||||
*/
|
||||
default YamlFileAST getAST() { return null; }
|
||||
|
||||
/**
|
||||
* Returns the set of property names that are already defined in the current context.
|
||||
* <p>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
@@ -131,7 +131,7 @@ public class YTypeFactory {
|
||||
return new YAny(name);
|
||||
}
|
||||
|
||||
public YType yseq(YType el) {
|
||||
public YSeqType yseq(YType el) {
|
||||
return new YSeqType(el);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -10,13 +10,9 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.yaml.schema.constraints;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
|
||||
import org.springframework.ide.vscode.commons.util.text.IDocument;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YType;
|
||||
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
|
||||
/**
|
||||
@@ -28,10 +24,10 @@ import org.yaml.snakeyaml.nodes.Node;
|
||||
public interface Constraint {
|
||||
|
||||
/**
|
||||
* Implemetors gain access to various bits of context information passed as parameters and
|
||||
* Implementors gain access to various bits of context information passed as parameters and
|
||||
* are supposed to use this information in whatever way they like to check if the
|
||||
* constraint is satisfied. When the constrain is not satisfied they should report any
|
||||
* violations by adding problems to the provide {@link IProblemCollector}.
|
||||
* constraint is satisfied. When the constraint is not satisfied they should report any
|
||||
* violations by adding problems to the provided {@link IProblemCollector}.
|
||||
*
|
||||
* @param node The node being validated
|
||||
* @param type The inferred type of the node.
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableList.Builder;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
|
||||
@@ -327,7 +328,7 @@ public class YamlStructureParser {
|
||||
public YamlPath getPath() throws Exception {
|
||||
List<YamlPathSegment> path = new ArrayList<>();
|
||||
for (SNode node : getPathNodes()) {
|
||||
YamlPathSegment segment = getSegment(node);
|
||||
YamlPathSegment segment = node.getSegment();
|
||||
if (segment!=null) {
|
||||
path.add(segment);
|
||||
}
|
||||
@@ -340,19 +341,24 @@ public class YamlStructureParser {
|
||||
* null because not all SNodes can be interpreted as 'step' in the yml
|
||||
* structure (e.g. raw nodes will return null, as will the 'root' node).
|
||||
*/
|
||||
private YamlPathSegment getSegment(SNode node) throws Exception {
|
||||
if (node!=null) {
|
||||
SNodeType nodeType = node.getNodeType();
|
||||
if (nodeType==SNodeType.KEY) {
|
||||
String key = ((SKeyNode)node).getKey();
|
||||
return YamlPathSegment.valueAt(key);
|
||||
} else if (nodeType==SNodeType.SEQ) {
|
||||
int index = ((SSeqNode)node).getIndex();
|
||||
return YamlPathSegment.valueAt(index);
|
||||
} else if (nodeType==SNodeType.DOC) {
|
||||
int index = ((SDocNode)node).getIndex();
|
||||
return YamlPathSegment.valueAt(index);
|
||||
public YamlPathSegment getSegment() {
|
||||
try {
|
||||
SNode node = this;
|
||||
if (node!=null) {
|
||||
SNodeType nodeType = node.getNodeType();
|
||||
if (nodeType==SNodeType.KEY) {
|
||||
String key = ((SKeyNode)node).getKey();
|
||||
return YamlPathSegment.valueAt(key);
|
||||
} else if (nodeType==SNodeType.SEQ) {
|
||||
int index = ((SSeqNode)node).getIndex();
|
||||
return YamlPathSegment.valueAt(index);
|
||||
} else if (nodeType==SNodeType.DOC) {
|
||||
int index = ((SDocNode)node).getIndex();
|
||||
return YamlPathSegment.valueAt(index);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -576,6 +582,15 @@ public class YamlStructureParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
public SNode getLastRealChild() {
|
||||
for (SNode c : Lists.reverse(getChildren())) {
|
||||
if (c.getIndent()>=0) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public abstract class SLeafNode extends SNode {
|
||||
|
||||
@@ -46,7 +46,6 @@ import org.eclipse.lsp4j.TextDocumentPositionParams;
|
||||
import org.eclipse.lsp4j.TextEdit;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.junit.Assert;
|
||||
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
|
||||
import org.springframework.ide.vscode.commons.util.text.LanguageId;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -346,6 +345,16 @@ public class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
public void assertNoCompletionsWithLabel(Predicate<String> labelPredicate) throws Exception {
|
||||
List<String> found = getCompletions().stream()
|
||||
.map(c -> c.getLabel())
|
||||
.filter(labelPredicate)
|
||||
.collect(Collectors.toList());
|
||||
if (!found.isEmpty()) {
|
||||
fail("Found but not expected: "+found);
|
||||
}
|
||||
}
|
||||
|
||||
public void assertDoesNotContainCompletions(String... notToBeFound) throws Exception {
|
||||
StringBuilder actual = new StringBuilder();
|
||||
|
||||
@@ -741,4 +750,8 @@ public class Editor {
|
||||
return harness.getDocumentSymbols(this.doc);
|
||||
}
|
||||
|
||||
public void setCursor(Position position) {
|
||||
this.selectionStart = this.selectionEnd = doc.toOffset(position);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ import org.eclipse.lsp4j.services.LanguageClientAware;
|
||||
import org.springframework.ide.vscode.commons.languageserver.ProgressParams;
|
||||
import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient;
|
||||
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
|
||||
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.LanguageServerTestListener;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
|
||||
import org.springframework.ide.vscode.commons.util.Assert;
|
||||
@@ -229,6 +230,17 @@ public class LanguageServerHarness {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Object> moveCursor(CursorMovement cursorMovement) {
|
||||
for (Editor editor : activeEditors) {
|
||||
if (editor.getUri().equals(cursorMovement.getUri())) {
|
||||
editor.setCursor(cursorMovement.getPosition());
|
||||
return CompletableFuture.completedFuture(new ApplyWorkspaceEditResponse(true));
|
||||
}
|
||||
}
|
||||
return CompletableFuture.completedFuture(new ApplyWorkspaceEditResponse(false));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ public class DocumentEditsTest {
|
||||
edits.deleteLineBackward(0);
|
||||
}
|
||||
|
||||
public void freezeCursor() {
|
||||
edits.freezeCursor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test public void testDeletes() throws Exception {
|
||||
@@ -194,4 +198,40 @@ public class DocumentEditsTest {
|
||||
|
||||
}
|
||||
|
||||
@Test public void testCursorFreeze() throws Exception {
|
||||
TestSubject it = new TestSubject(
|
||||
"something:\n" +
|
||||
"#end"
|
||||
);
|
||||
// it.insBefore("\n#end", "\n foo: ");
|
||||
// it.insBefore("\n#end", "\n zoro: ");
|
||||
// it.insBefore("\n#end", "\n banana: ");
|
||||
//
|
||||
// it.expect(
|
||||
// "something:\n" +
|
||||
// " foo: \n" +
|
||||
// " zoro: \n" +
|
||||
// " banana: <*>\n" +
|
||||
// "#end"
|
||||
// );
|
||||
//
|
||||
// it.reset();
|
||||
|
||||
it.insBefore("\n#end", "\n foo: ");
|
||||
it.freezeCursor();
|
||||
it.insBefore("\n#end", "\n zoro: ");
|
||||
it.insBefore("\n#end", "\n banana: ");
|
||||
it.del("#end");
|
||||
it.insBefore("something", "replc");
|
||||
it.del("something");
|
||||
|
||||
it.expect(
|
||||
"replc:\n" +
|
||||
" foo: <*>\n" +
|
||||
" zoro: \n" +
|
||||
" banana: \n"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.springframework.ide.vscode.commons.util.IOUtil;
|
||||
import org.springframework.ide.vscode.commons.util.Unicodes;
|
||||
import org.springframework.ide.vscode.commons.util.text.LanguageId;
|
||||
import org.springframework.ide.vscode.languageserver.testharness.CodeAction;
|
||||
import org.springframework.ide.vscode.languageserver.testharness.Editor;
|
||||
@@ -58,16 +59,41 @@ public class ConcourseEditorTest {
|
||||
assertEquals("Add property 'type'", quickfix.getLabel());
|
||||
quickfix.perform();
|
||||
|
||||
editor.assertRawText(
|
||||
editor.assertText(
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" source:\n" +
|
||||
" username: someone\n" +
|
||||
" type: \n" +
|
||||
" type: <*>\n" +
|
||||
"# Confuse"
|
||||
);
|
||||
}
|
||||
|
||||
@Test public void addMultipleRequiredPropertiesQuickfix() throws Exception {
|
||||
Editor editor = harness.newEditor(
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: pool\n" +
|
||||
" source:\n" +
|
||||
" username: someone\n"
|
||||
);
|
||||
Diagnostic problem = editor.assertProblems("source|[branch, pool, uri] are required").get(0);
|
||||
CodeAction quickfix = editor.assertCodeAction(problem);
|
||||
assertEquals("Add properties: [branch, pool, uri]", quickfix.getLabel());
|
||||
quickfix.perform();
|
||||
|
||||
editor.assertText(
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: pool\n" +
|
||||
" source:\n" +
|
||||
" username: someone\n" +
|
||||
" branch: <*>\n" +
|
||||
" pool: \n" +
|
||||
" uri: \n"
|
||||
);
|
||||
}
|
||||
|
||||
@Test public void reconcileResourceTypeType() throws Exception {
|
||||
Editor editor;
|
||||
editor = harness.newEditor(
|
||||
@@ -89,31 +115,6 @@ public class ConcourseEditorTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test public void addMultipleRequiredPropertiesQuickfix() throws Exception {
|
||||
Editor editor = harness.newEditor(
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: pool\n" +
|
||||
" source:\n" +
|
||||
" username: someone\n"
|
||||
);
|
||||
Diagnostic problem = editor.assertProblems("source|[branch, pool, uri] are required").get(0);
|
||||
CodeAction quickfix = editor.assertCodeAction(problem);
|
||||
assertEquals("Add properties: [branch, pool, uri]", quickfix.getLabel());
|
||||
quickfix.perform();
|
||||
|
||||
editor.assertRawText(
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: pool\n" +
|
||||
" source:\n" +
|
||||
" username: someone\n" +
|
||||
" branch: \n" +
|
||||
" pool: \n" +
|
||||
" uri: \n"
|
||||
);
|
||||
}
|
||||
|
||||
@Test public void testReconcileCatchesParseError() throws Exception {
|
||||
Editor editor = harness.newEditor(
|
||||
"somemap: val\n"+
|
||||
@@ -207,8 +208,6 @@ public class ConcourseEditorTest {
|
||||
editor.assertProblems(
|
||||
"a-resource|does not exist"
|
||||
);
|
||||
|
||||
//TODO: Add more test cases for structural problem?
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -840,7 +839,8 @@ public class ConcourseEditorTest {
|
||||
"- name: every5minutes\n" +
|
||||
" type: time\n" +
|
||||
" source:\n" +
|
||||
" <*>"
|
||||
" <*>\n" +
|
||||
" blah: blah"
|
||||
, // ======================
|
||||
"<*>"
|
||||
, // =>
|
||||
@@ -973,7 +973,8 @@ public class ConcourseEditorTest {
|
||||
"- name: the-repo\n" +
|
||||
" type: git\n" +
|
||||
" source:\n" +
|
||||
" <*>"
|
||||
" <*>\n" +
|
||||
" blah: blah"
|
||||
, //================
|
||||
"<*>"
|
||||
, // ==>
|
||||
@@ -1075,7 +1076,8 @@ public class ConcourseEditorTest {
|
||||
" plan:\n" +
|
||||
" - get: my-git\n" +
|
||||
" params:\n" +
|
||||
" <*>";
|
||||
" <*>\n" +
|
||||
" blah: blah";
|
||||
|
||||
assertContextualCompletions(context,
|
||||
"<*>"
|
||||
@@ -1149,7 +1151,8 @@ public class ConcourseEditorTest {
|
||||
" plan:\n" +
|
||||
" - put: my-git\n" +
|
||||
" params:\n" +
|
||||
" <*>";
|
||||
" <*>\n" +
|
||||
" blah: blah";
|
||||
|
||||
assertContextualCompletions(context,
|
||||
"<*>"
|
||||
@@ -1975,7 +1978,8 @@ public class ConcourseEditorTest {
|
||||
"- name: version\n" +
|
||||
" type: semver\n" +
|
||||
" source:\n" +
|
||||
"<*>";
|
||||
"<*>\n" +
|
||||
" blah: blah";
|
||||
assertContextualCompletions(conText,
|
||||
" driver: git\n" +
|
||||
" <*>"
|
||||
@@ -2003,8 +2007,6 @@ public class ConcourseEditorTest {
|
||||
,
|
||||
" driver: git\n" +
|
||||
" username: <*>"
|
||||
,
|
||||
" driver: git<*>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2874,24 +2876,33 @@ public class ConcourseEditorTest {
|
||||
editor.assertCompletionLabels(
|
||||
//For the 'exact' context:
|
||||
"check_every",
|
||||
"name",
|
||||
"source",
|
||||
"type",
|
||||
//"name", exists
|
||||
//"source", exists
|
||||
//"type", exists
|
||||
//For the nested context:
|
||||
"➔ branch",
|
||||
"➔ commit_verification_key_ids",
|
||||
"➔ commit_verification_keys",
|
||||
"➔ disable_ci_skip",
|
||||
"➔ git_config",
|
||||
"➔ gpg_keyserver",
|
||||
"➔ ignore_paths",
|
||||
"➔ password",
|
||||
"➔ paths",
|
||||
"➔ private_key",
|
||||
"➔ skip_ssl_verification",
|
||||
"➔ tag_filter",
|
||||
"➔ uri",
|
||||
"➔ username"
|
||||
"→ branch",
|
||||
"→ commit_verification_key_ids",
|
||||
"→ commit_verification_keys",
|
||||
"→ disable_ci_skip",
|
||||
"→ git_config",
|
||||
"→ gpg_keyserver",
|
||||
"→ ignore_paths",
|
||||
"→ password",
|
||||
"→ paths",
|
||||
"→ private_key",
|
||||
"→ skip_ssl_verification",
|
||||
"→ tag_filter",
|
||||
"→ uri",
|
||||
"→ username",
|
||||
// For the top-level context:
|
||||
"← groups",
|
||||
"← jobs",
|
||||
"← resource_types",
|
||||
// For the 'next job' context:
|
||||
"← - check_every",
|
||||
"← - name",
|
||||
"← - source",
|
||||
"← - type"
|
||||
);
|
||||
|
||||
editor.assertCompletionWithLabel("check_every",
|
||||
@@ -2902,14 +2913,14 @@ public class ConcourseEditorTest {
|
||||
" check_every: <*>"
|
||||
);
|
||||
|
||||
editor.assertCompletionWithLabel("➔ branch",
|
||||
editor.assertCompletionWithLabel("→ branch",
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: git\n" +
|
||||
" source:\n" +
|
||||
" branch: <*>"
|
||||
);
|
||||
editor.assertCompletionWithLabel("➔ commit_verification_key_ids",
|
||||
editor.assertCompletionWithLabel("→ commit_verification_key_ids",
|
||||
"resources:\n" +
|
||||
"- name: foo\n" +
|
||||
" type: git\n" +
|
||||
@@ -2965,9 +2976,9 @@ public class ConcourseEditorTest {
|
||||
"max_in_flight",
|
||||
"serial",
|
||||
"serial_groups",
|
||||
"name",
|
||||
"plan",
|
||||
"public",
|
||||
//"name", exists
|
||||
//"plan", exists
|
||||
//"public", exists
|
||||
//Completions with '-'
|
||||
"- aggregate",
|
||||
"- do",
|
||||
@@ -2976,20 +2987,31 @@ public class ConcourseEditorTest {
|
||||
"- task",
|
||||
"- try",
|
||||
//Completions for nested context (i.e. task step)
|
||||
"➔ attempts",
|
||||
"➔ config",
|
||||
"➔ ensure",
|
||||
"➔ file",
|
||||
"➔ image",
|
||||
"➔ input_mapping",
|
||||
"➔ on_failure",
|
||||
"➔ on_success",
|
||||
"➔ output_mapping",
|
||||
"➔ params",
|
||||
"➔ privileged",
|
||||
"➔ tags",
|
||||
"➔ timeout",
|
||||
"➔ task"
|
||||
"→ attempts",
|
||||
"→ config",
|
||||
"→ ensure",
|
||||
"→ file",
|
||||
"→ image",
|
||||
"→ input_mapping",
|
||||
"→ on_failure",
|
||||
"→ on_success",
|
||||
"→ output_mapping",
|
||||
"→ params",
|
||||
"→ privileged",
|
||||
"→ tags",
|
||||
"→ timeout",
|
||||
//"→ task" exists
|
||||
"← groups\n" +
|
||||
"← resource_types\n" +
|
||||
"← resources\n" +
|
||||
"← - build_logs_to_retain\n" +
|
||||
"← - disable_manual_trigger\n" +
|
||||
"← - max_in_flight\n" +
|
||||
"← - name\n" +
|
||||
"← - plan\n" +
|
||||
"← - public\n" +
|
||||
"← - serial\n" +
|
||||
"← - serial_groups"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3374,6 +3396,62 @@ public class ConcourseEditorTest {
|
||||
" - <*>"
|
||||
);
|
||||
}
|
||||
|
||||
@Test public void relaxedContentAssistLessSpaces() throws Exception {
|
||||
Editor editor;
|
||||
|
||||
editor = harness.newEditor(
|
||||
"jobs:\n" +
|
||||
"- name: build-docker-image\n" +
|
||||
" serial: true\n" +
|
||||
" plan:\n" +
|
||||
" - get: docker-git\n" +
|
||||
" trigger: true\n" +
|
||||
" <*>"
|
||||
);
|
||||
editor.assertCompletionWithLabel("← - put",
|
||||
"jobs:\n" +
|
||||
"- name: build-docker-image\n" +
|
||||
" serial: true\n" +
|
||||
" plan:\n" +
|
||||
" - get: docker-git\n" +
|
||||
" trigger: true\n" +
|
||||
" - put: <*>"
|
||||
);
|
||||
|
||||
editor = harness.newEditor(
|
||||
"jobs:\n" +
|
||||
"- name: build-docker-image\n" +
|
||||
" serial: true\n" +
|
||||
" plan:\n" +
|
||||
" - get: docker-git\n" +
|
||||
" trigger: true\n" +
|
||||
" pu<*>"
|
||||
);
|
||||
editor.assertCompletionWithLabel("← - put",
|
||||
"jobs:\n" +
|
||||
"- name: build-docker-image\n" +
|
||||
" serial: true\n" +
|
||||
" plan:\n" +
|
||||
" - get: docker-git\n" +
|
||||
" trigger: true\n" +
|
||||
" - put: <*>"
|
||||
);
|
||||
|
||||
//Should be de-indentation relaxation. These should not be
|
||||
// allowed if they cause the context node to be split. So in this example
|
||||
// de-indented completions shouldn't be suggested.
|
||||
editor = harness.newEditor(
|
||||
"jobs:\n" +
|
||||
"- name: build-docker-image\n" +
|
||||
" serial: true\n" +
|
||||
" plan:\n" +
|
||||
" - get: docker-git\n" +
|
||||
" <*>\n" +
|
||||
" trigger: true\n"
|
||||
);
|
||||
editor.assertNoCompletionsWithLabel(label -> label.startsWith(Unicodes.LEFT_ARROW+" "));;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -16,6 +16,7 @@ import java.util.concurrent.Callable;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInstance;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTarget;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTargetCache;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ConnectionException;
|
||||
@@ -78,7 +79,7 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param e
|
||||
* @return an error that requires no additional information when showing its
|
||||
* message, or null if no such error is found
|
||||
@@ -87,7 +88,7 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
|
||||
return ExceptionUtil.findThrowable(e,
|
||||
ImmutableList.of(NoTargetsException.class, ConnectionException.class));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @return non-null list of hints. Return empty if no hints available
|
||||
|
||||
@@ -29,16 +29,13 @@ public class CFServicesValueParser extends EnumValueParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String createBlankTextErrorMessage() {
|
||||
return "At least one service instance name must be specified";
|
||||
}
|
||||
|
||||
protected Exception errorOnParse(String message) {
|
||||
// Parse errors should be indicated differently than regular schema
|
||||
// problems (e.g. unknown service may be a warning)
|
||||
return new ReconcileException(message, ManifestYamlSchemaProblemsTypes.UNKNOWN_SERVICES_PROBLEM);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Exception errorOnBlank(String message) {
|
||||
// Blank errors should be regular schema problems
|
||||
return new ReconcileException(message, YamlSchemaProblems.SCHEMA_PROBLEM);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.springframework.ide.vscode.manifest.yaml;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
|
||||
@@ -8,12 +9,26 @@ import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint;
|
||||
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
import org.yaml.snakeyaml.nodes.SequenceNode;
|
||||
|
||||
public class ManifestConstraints {
|
||||
|
||||
public static Constraint mutuallyExclusive(String... propertyIds) {
|
||||
return (dc, parent, node, type, problems) -> {
|
||||
Set<String> keys = NodeUtil.getScalarKeys(parent);
|
||||
Set<String> keys = new HashSet<>();
|
||||
Node root = dc.getAST().getNodes().get(0);
|
||||
// First add keys from the root node
|
||||
keys.addAll(NodeUtil.getScalarKeys(root));
|
||||
if (root == parent) {
|
||||
// Add keys from all applications
|
||||
SequenceNode apps = NodeUtil.asSequence(NodeUtil.getProperty(root, "applications"));
|
||||
if (apps != null) {
|
||||
apps.getValue().forEach(n -> keys.addAll(NodeUtil.getScalarKeys(n)));
|
||||
}
|
||||
} else {
|
||||
// Now add keys from application node, thus application node keys would replace root node keys if they present in both nodes
|
||||
keys.addAll(NodeUtil.getScalarKeys(parent));
|
||||
}
|
||||
Arrays.stream(propertyIds).filter(id -> keys.contains(id)).findFirst().ifPresent(propertyId -> {
|
||||
// Find key node, because the node parameter is the value node
|
||||
MappingNode mapNode = (MappingNode) parent;
|
||||
@@ -24,7 +39,7 @@ public class ManifestConstraints {
|
||||
problems.accept(
|
||||
new ReconcileProblemImpl(ManifestYamlSchemaProblemsTypes.MUTUALLY_EXCLUSIVE_PROPERTY_PROBLEM,
|
||||
"Property cannot co-exist with property '" + propertyId + "'", start, end - start));
|
||||
});;
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,12 +59,12 @@ public class ManifestYamlCFServicesProvider extends AbstractCFHintsProvider {
|
||||
return hints;
|
||||
}
|
||||
|
||||
private String getServiceLabel(CFTarget cfClientTarget, CFServiceInstance service) {
|
||||
return service.getName() + " - " + service.getPlan();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTypeName() {
|
||||
return "Service";
|
||||
}
|
||||
|
||||
protected final String getServiceLabel(CFTarget cfClientTarget, CFServiceInstance service) {
|
||||
return service.getName() + " - " + service.getPlan();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,10 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
|
||||
}
|
||||
|
||||
protected ManifestYmlHintProviders getHintProviders() {
|
||||
Callable<Collection<YValueHint>> buildPacksProvider = getBuildpacksProvider();
|
||||
Callable<Collection<YValueHint>> servicesProvider = getServicesProvider();
|
||||
Callable<Collection<YValueHint>> domainsProvider = getDomainsProvider();
|
||||
Callable<Collection<YValueHint>> buildPacksProvider = new ManifestYamlCFBuildpacksProvider(getCfTargetCache());
|
||||
Callable<Collection<YValueHint>> servicesProvider = new ManifestYamlCFServicesProvider(getCfTargetCache());
|
||||
Callable<Collection<YValueHint>> domainsProvider = new ManifestYamlCFDomainsProvider(getCfTargetCache());
|
||||
Callable<Collection<YValueHint>> stacksProvider = new ManifestYamlStacksProvider(getCfTargetCache());
|
||||
|
||||
return new ManifestYmlHintProviders() {
|
||||
|
||||
@@ -115,6 +116,11 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
|
||||
public Callable<Collection<YValueHint>> getBuildpackProviders() {
|
||||
return buildPacksProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callable<Collection<YValueHint>> getStacksProvider() {
|
||||
return stacksProvider;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,16 +133,4 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
|
||||
return cfTargetCache;
|
||||
}
|
||||
|
||||
private Callable<Collection<YValueHint>> getBuildpacksProvider() {
|
||||
return new ManifestYamlCFBuildpacksProvider(getCfTargetCache());
|
||||
}
|
||||
|
||||
private Callable<Collection<YValueHint>> getServicesProvider() {
|
||||
return new ManifestYamlCFServicesProvider(getCfTargetCache());
|
||||
}
|
||||
|
||||
private Callable<Collection<YValueHint>> getDomainsProvider() {
|
||||
return new ManifestYamlCFDomainsProvider(getCfTargetCache());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,12 +20,10 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy
|
||||
*/
|
||||
public class ManifestYamlSchemaProblemsTypes {
|
||||
|
||||
public static final ProblemType UNKNOWN_SERVICES_PROBLEM = problemType("UnknownServicesProblem",
|
||||
ProblemSeverity.WARNING);
|
||||
|
||||
public static final ProblemType UNKNOWN_DOMAIN_PROBLEM = problemType("UnknownDomainProblem",
|
||||
ProblemSeverity.WARNING);
|
||||
|
||||
public static final ProblemType UNKNOWN_SERVICES_PROBLEM = problemType("UnknownServicesProblem", ProblemSeverity.WARNING);
|
||||
public static final ProblemType UNKNOWN_DOMAIN_PROBLEM = problemType("UnknownDomainProblem", ProblemSeverity.WARNING);
|
||||
public static final ProblemType UNKNOWN_STACK_PROBLEM = problemType("UnknownStackProblem", ProblemSeverity.WARNING);
|
||||
public static final ProblemType IGNORED_PROPERTY = problemType("IgnoredProperty", ProblemSeverity.WARNING);
|
||||
public static final ProblemType MUTUALLY_EXCLUSIVE_PROPERTY_PROBLEM = problemType("MutuallyExclusiveProperty",
|
||||
ProblemSeverity.ERROR);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*******************************************************************************
|
||||
* 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 org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTarget;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTargetCache;
|
||||
import org.springframework.ide.vscode.commons.util.Renderable;
|
||||
import org.springframework.ide.vscode.commons.util.Renderables;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
|
||||
|
||||
public class ManifestYamlStacksProvider extends AbstractCFHintsProvider {
|
||||
|
||||
public ManifestYamlStacksProvider(CFTargetCache targetCache) {
|
||||
super(targetCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTypeName() {
|
||||
return "Stack";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<YValueHint> getHints(List<CFTarget> targets) throws Exception {
|
||||
// NOTE: empty list of services is a VALID result. A CF target may have
|
||||
// no service instances
|
||||
// created, so if empty list is returned from the client, then RETURN empty list. don't
|
||||
// return null
|
||||
// for empty services cases
|
||||
List<YValueHint> hints = new ArrayList<>();
|
||||
|
||||
for (CFTarget cfTarget : targets) {
|
||||
List<CFStack> stacks = cfTarget.getStacks();
|
||||
Renderable targetLabel = Renderables.text(cfTarget.getLabel());
|
||||
if (stacks != null && !stacks.isEmpty()) {
|
||||
for (CFStack s : stacks) {
|
||||
String name = s.getName();
|
||||
String label = name;
|
||||
YValueHint hint = new BasicYValueHint(name, label)
|
||||
.setDocumentation(targetLabel);
|
||||
if (!hints.contains(hint)) {
|
||||
hints.add(hint);
|
||||
}
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -23,4 +23,6 @@ public interface ManifestYmlHintProviders {
|
||||
|
||||
Callable<Collection<YValueHint>> getDomainsProvider();
|
||||
|
||||
Callable<Collection<YValueHint>> getStacksProvider();
|
||||
|
||||
}
|
||||
|
||||
@@ -14,19 +14,28 @@ import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
|
||||
import org.springframework.ide.vscode.commons.util.IntegerRange;
|
||||
import org.springframework.ide.vscode.commons.util.Renderable;
|
||||
import org.springframework.ide.vscode.commons.util.Renderables;
|
||||
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.YamlFileAST;
|
||||
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.reconcile.YamlSchemaProblems;
|
||||
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.YTypeFactory;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YSeqType;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YTypedPropertyImpl;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
@@ -35,9 +44,13 @@ import com.google.common.collect.ImmutableSet;
|
||||
*/
|
||||
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;
|
||||
private final Callable<Collection<YValueHint>> buildpackProvider;
|
||||
|
||||
public final AbstractType t_route_string;
|
||||
|
||||
private static final Set<String> TOPLEVEL_EXCLUDED = ImmutableSet.of(
|
||||
"name", "host", "hosts"
|
||||
@@ -48,11 +61,46 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
return IntegerRange.exactly(1);
|
||||
}
|
||||
|
||||
private void verify_heatth_check_http_end_point_constraint(DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) {
|
||||
YamlFileAST ast = dc.getAST();
|
||||
if (ast!=null) {
|
||||
Node markerNode = YamlPathSegment.keyAt(HEALTH_CHECK_HTTP_ENDPOINT_PROP).traverseNode(node);
|
||||
if (markerNode != null) {
|
||||
String healthCheckType = getEffectiveHealthCheckType(ast, dc.getPath(), node);
|
||||
if (!"http".equals(healthCheckType)) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the actual health-check-type that applies to a given node, taking into account
|
||||
* inheritance from parent node, and default value.
|
||||
*/
|
||||
private String getEffectiveHealthCheckType(YamlFileAST ast, YamlPath path, Node node) {
|
||||
String explicit = NodeUtil.getScalarProperty(node, HEALTH_CHECK_TYPE_PROP);
|
||||
if (explicit!=null) {
|
||||
return explicit;
|
||||
}
|
||||
if (path.size()>2) {
|
||||
//Must consider inherited props!
|
||||
YamlPath parentPath = path.dropLast(2);
|
||||
Node parent = parentPath.traverseToNode(ast);
|
||||
String inherited = NodeUtil.getScalarProperty(parent, HEALTH_CHECK_TYPE_PROP);
|
||||
if (inherited!=null) {
|
||||
return inherited;
|
||||
}
|
||||
}
|
||||
return "port";
|
||||
}
|
||||
|
||||
public ManifestYmlSchema(ManifestYmlHintProviders providers) {
|
||||
this.buildpackProvider = providers.getBuildpackProviders();
|
||||
Callable<Collection<YValueHint>> buildpackProvider = providers.getBuildpackProviders();
|
||||
Callable<Collection<YValueHint>> servicesProvider = providers.getServicesProvider();
|
||||
Callable<Collection<YValueHint>> domainsProvider = providers.getDomainsProvider();
|
||||
Callable<Collection<YValueHint>> stacksProvider = providers.getStacksProvider();
|
||||
|
||||
|
||||
YTypeFactory f = new YTypeFactory();
|
||||
@@ -60,20 +108,29 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
|
||||
// define schema types
|
||||
TOPLEVEL_TYPE = f.ybean("Cloudfoundry Manifest");
|
||||
TOPLEVEL_TYPE.require(this::verify_heatth_check_http_end_point_constraint);
|
||||
|
||||
AbstractType application = f.ybean("Application");
|
||||
application.require(this::verify_heatth_check_http_end_point_constraint);
|
||||
YAtomicType t_path = f.yatomic("Path");
|
||||
|
||||
YAtomicType t_buildpack = f.yatomic("Buildpack");
|
||||
if (this.buildpackProvider != null) {
|
||||
t_buildpack.addHintProvider(this.buildpackProvider);
|
||||
if (buildpackProvider != null) {
|
||||
t_buildpack.addHintProvider(buildpackProvider);
|
||||
// t_buildpack.parseWith(ManifestYmlValueParsers.fromHints(t_buildpack.toString(), buildpackProvider));
|
||||
}
|
||||
|
||||
YAtomicType t_stack = f.yatomic("Stack");
|
||||
if (stacksProvider!=null) {
|
||||
t_stack.addHintProvider(stacksProvider);
|
||||
t_stack.parseWith(ManifestYmlValueParsers.fromValueHints(stacksProvider, t_stack, ManifestYamlSchemaProblemsTypes.UNKNOWN_STACK_PROBLEM));
|
||||
}
|
||||
|
||||
YAtomicType t_domain = f.yatomic("Domain");
|
||||
t_domain.require(ManifestConstraints.mutuallyExclusive("routes"));
|
||||
if (domainsProvider != null) {
|
||||
t_domain.addHintProvider(domainsProvider);
|
||||
t_domain.parseWith(ManifestYmlValueParsers.fromValueHints(domainsProvider, t_domain, ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM));
|
||||
}
|
||||
|
||||
YAtomicType t_service = f.yatomic("Service");
|
||||
@@ -87,16 +144,13 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
YAtomicType t_ne_string = f.yatomic("String");
|
||||
t_ne_string.parseWith(ValueParsers.NE_STRING);
|
||||
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");
|
||||
@@ -118,28 +172,43 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
TOPLEVEL_TYPE.addProperty(f.yprop("applications", f.yseq(application)));
|
||||
TOPLEVEL_TYPE.addProperty("inherit", t_string, descriptionFor("inherit"));
|
||||
|
||||
YSeqType routesType = f.yseq(route);
|
||||
routesType.require(ManifestConstraints.mutuallyExclusive("domain", "domains", "host", "hosts", "no-hostname"));
|
||||
|
||||
YSeqType domainsType = f.yseq(t_domain);
|
||||
domainsType.require(ManifestConstraints.mutuallyExclusive("routes"));
|
||||
|
||||
YSeqType hostsType = f.yseq(t_string);
|
||||
hostsType.require(ManifestConstraints.mutuallyExclusive("routes"));
|
||||
|
||||
YAtomicType hostType = f.yatomic("String");
|
||||
hostType.require(ManifestConstraints.mutuallyExclusive("routes"));
|
||||
|
||||
YAtomicType noHostType = f.yenum("boolean", "true", "false");
|
||||
noHostType.require(ManifestConstraints.mutuallyExclusive("routes"));
|
||||
|
||||
YTypedPropertyImpl[] props = {
|
||||
f.yprop("buildpack", t_buildpack),
|
||||
f.yprop("command", t_string),
|
||||
f.yprop("disk_quota", t_memory),
|
||||
f.yprop("domain", t_domain),
|
||||
f.yprop("domains", f.yseq(t_domain)),
|
||||
f.yprop("domains", domainsType),
|
||||
f.yprop("env", t_env),
|
||||
f.yprop("host", t_string),
|
||||
f.yprop("hosts", t_strings),
|
||||
f.yprop("host", hostType),
|
||||
f.yprop("hosts", hostsType),
|
||||
f.yprop("instances", t_strictly_pos_integer),
|
||||
f.yprop("memory", t_memory),
|
||||
f.yprop("name", t_ne_string).isRequired(true),
|
||||
f.yprop("no-hostname", t_boolean),
|
||||
f.yprop("no-hostname", noHostType),
|
||||
f.yprop("no-route", t_boolean),
|
||||
f.yprop("path", t_path),
|
||||
f.yprop("random-route", t_boolean),
|
||||
f.yprop("routes", f.yseq(route)),
|
||||
f.yprop("routes", routesType),
|
||||
f.yprop("services", f.yseq(t_service)),
|
||||
f.yprop("stack", t_string),
|
||||
f.yprop("stack", t_stack),
|
||||
f.yprop("timeout", t_pos_integer),
|
||||
f.yprop("health-check-type", t_health_check_type),
|
||||
f.yprop("health-check-http-endpoint", t_ne_string)
|
||||
f.yprop(HEALTH_CHECK_TYPE_PROP, t_health_check_type),
|
||||
f.yprop(HEALTH_CHECK_HTTP_ENDPOINT_PROP, t_ne_string)
|
||||
};
|
||||
|
||||
for (YTypedPropertyImpl prop : props) {
|
||||
|
||||
@@ -10,10 +10,18 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.manifest.yaml;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException;
|
||||
import org.springframework.ide.vscode.commons.util.Assert;
|
||||
import org.springframework.ide.vscode.commons.util.EnumValueParser;
|
||||
import org.springframework.ide.vscode.commons.util.ValueParser;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType;
|
||||
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
@@ -87,4 +95,13 @@ public class ManifestYmlValueParsers {
|
||||
};
|
||||
}
|
||||
|
||||
public static EnumValueParser fromValueHints(Callable<Collection<YValueHint>> hintProvider, YAtomicType type, ProblemType problemType) {
|
||||
return new EnumValueParser(type.toString(), YTypeFactory.valuesFromHintProvider(hintProvider)) {
|
||||
@Override
|
||||
protected Exception errorOnParse(String message) {
|
||||
return new ReconcileException(message, problemType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class RouteValueParser extends RegexpParser {
|
||||
}
|
||||
if (!cloudDomains.contains(route.getDomain())) {
|
||||
String hostDomain = matcher.group(1);
|
||||
throw new ReconcileException("Unknown domain", ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM, hostDomain.lastIndexOf(route.getDomain()), hostDomain.length());
|
||||
throw new ReconcileException("Unknown 'Domain'. Valid domains are: "+cloudDomains, ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM, hostDomain.lastIndexOf(route.getDomain()), hostDomain.length());
|
||||
}
|
||||
return route;
|
||||
} catch (ConnectionException | NoTargetsException e) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.mockito.Mockito;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFBuildpack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFDomain;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInstance;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.NoTargetsException;
|
||||
import org.springframework.ide.vscode.languageserver.testharness.CodeAction;
|
||||
@@ -428,8 +429,7 @@ public class ManifestYamlEditorTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconcileHealthCheckType() throws Exception {
|
||||
@Test public void reconcileHealthCheckType() throws Exception {
|
||||
Editor editor;
|
||||
Diagnostic problem;
|
||||
|
||||
@@ -464,6 +464,109 @@ public class ManifestYamlEditorTest {
|
||||
editor.assertProblems(/*NONE*/);
|
||||
}
|
||||
|
||||
@Test public void reconcileHealthHttpEndPointIgnoredWarning() throws Exception {
|
||||
Editor editor;
|
||||
Diagnostic problem;
|
||||
|
||||
editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" health-check-type: process\n" +
|
||||
" 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-type: http\n" +
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" health-check-http-endpoint: /health"
|
||||
);
|
||||
editor.assertProblems(/*NONE*/);
|
||||
|
||||
editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" health-check-http-endpoint: /health"
|
||||
);
|
||||
problem = editor.assertProblems("health-check-http-endpoint|This has no effect unless `health-check-type` is `http` (but it is currently set to `port`)").get(0);
|
||||
assertEquals(DiagnosticSeverity.Warning, problem.getSeverity());
|
||||
|
||||
editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" health-check-type: http\n" +
|
||||
" health-check-http-endpoint: /health"
|
||||
);
|
||||
editor.assertProblems(/*NONE*/);
|
||||
|
||||
editor = harness.newEditor(
|
||||
"health-check-type: http\n" +
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" health-check-type: process\n" +
|
||||
" 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"
|
||||
);
|
||||
editor.assertProblems("health-check-http-endpoint|This has no effect unless `health-check-type` is `http` (but it is currently set to `process`)");
|
||||
}
|
||||
|
||||
@Test public void reconcileRoutesWithNoHost() throws Exception {
|
||||
Editor editor;
|
||||
|
||||
editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" no-hostname: true\n" +
|
||||
" routes:\n" +
|
||||
" - route: myapp.org"
|
||||
);
|
||||
editor.ignoreProblem("UnknownDomainProblem");
|
||||
|
||||
editor.assertProblems(
|
||||
"no-hostname|Property cannot co-exist with property 'routes'",
|
||||
"routes|Property cannot co-exist with property 'no-hostname'"
|
||||
);
|
||||
|
||||
editor = harness.newEditor(
|
||||
"no-hostname: true\n" +
|
||||
"applications:\n" +
|
||||
"- name: my-app\n" +
|
||||
" routes:\n" +
|
||||
" - route: myapp.org"
|
||||
);
|
||||
editor.ignoreProblem("UnknownDomainProblem");
|
||||
|
||||
editor.assertProblems(
|
||||
"no-hostname|Property cannot co-exist with property 'routes'",
|
||||
"routes|Property cannot co-exist with property 'no-hostname'"
|
||||
);
|
||||
|
||||
// editor = harness.newEditor(
|
||||
// "no-hostname: true\n" +
|
||||
// "routes:\n" +
|
||||
// "- route: myapp.org" +
|
||||
// "applications:\n" +
|
||||
// "- name: my-app\n"
|
||||
// );
|
||||
// editor.ignoreProblem("UnknownDomainProblem");
|
||||
//
|
||||
// editor.assertProblems(
|
||||
// "no-hostname|Property cannot co-exist with property 'routes'",
|
||||
// "routes|Property cannot co-exist with property 'no-hostname'"
|
||||
// );
|
||||
}
|
||||
|
||||
@Test public void deprecatedHealthCheckTypeQuickfix() throws Exception {
|
||||
Editor editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
@@ -589,8 +692,77 @@ public class ManifestYamlEditorTest {
|
||||
editor.assertNoHover("otherdomain.org");
|
||||
}
|
||||
|
||||
@Test public void stacksCompletion() throws Exception {
|
||||
List<CFStack> stacks = ImmutableList.of(
|
||||
mockStack("linux"), mockStack("windows")
|
||||
);
|
||||
when(cloudfoundry.client.getStacks()).thenReturn(stacks);
|
||||
Editor editor = harness.newEditor(
|
||||
"stack: <*>"
|
||||
);
|
||||
CompletionItem c = editor.assertCompletions(
|
||||
"stack: linux<*>",
|
||||
"stack: windows<*>"
|
||||
).get(0);
|
||||
|
||||
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" +
|
||||
"- bad.com\n" +
|
||||
"- two.com"
|
||||
);
|
||||
editor.assertProblems("bad.com|unknown 'Domain'. Valid values are: [one.com, two.com]");
|
||||
}
|
||||
|
||||
@Test public void stacksReconcile() throws Exception {
|
||||
List<CFStack> stacks = ImmutableList.of(
|
||||
mockStack("linux"), mockStack("windows")
|
||||
);
|
||||
when(cloudfoundry.client.getStacks()).thenReturn(stacks);
|
||||
{
|
||||
Editor editor = harness.newEditor(
|
||||
"stack: android<*>"
|
||||
);
|
||||
Diagnostic p = editor.assertProblems("android|'android' is an unknown 'Stack'. Valid values are: [linux, windows]").get(0);
|
||||
assertEquals(DiagnosticSeverity.Warning, p.getSeverity());
|
||||
}
|
||||
|
||||
{
|
||||
Editor editor = harness.newEditor(
|
||||
"stack: <*>"
|
||||
);
|
||||
Diagnostic p = editor.assertProblems("|'Stack' cannot be blank").get(0);
|
||||
assertEquals(DiagnosticSeverity.Error, p.getSeverity());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private CFStack mockStack(String name) {
|
||||
CFStack stack = Mockito.mock(CFStack.class);
|
||||
when(stack.getName()).thenReturn(name);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconcileDuplicateKeys() throws Exception {
|
||||
ImmutableList<CFDomain> domains = ImmutableList.of(mockDomain("pivotal.io"), mockDomain("otherdomain.org"));
|
||||
when(cloudfoundry.client.getDomains()).thenReturn(domains);
|
||||
Editor editor = harness.newEditor(
|
||||
"#comment\n" +
|
||||
"applications:\n" +
|
||||
@@ -882,10 +1054,12 @@ public class ManifestYamlEditorTest {
|
||||
ClientRequests cfClient = cloudfoundry.client;
|
||||
when(cfClient.getBuildpacks()).thenThrow(new IOException("Can't get buildpacks"));
|
||||
when(cfClient.getServices()).thenThrow(new IOException("Can't get services"));
|
||||
when(cfClient.getStacks()).thenThrow(new IOException("Can't get stacks"));
|
||||
Editor editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: foo\n" +
|
||||
" buildpack: bad-buildpack\n" +
|
||||
" stack: bad-stack\n" +
|
||||
" services:\n" +
|
||||
" - bad-service\n" +
|
||||
" bogus: bad" //a token error to make sure reconciler is actually running!
|
||||
@@ -1105,6 +1279,8 @@ public class ManifestYamlEditorTest {
|
||||
|
||||
@Test
|
||||
public void reconcileRoute_Advanced() throws Exception {
|
||||
ImmutableList<CFDomain> domains = ImmutableList.of(mockDomain("somedomain.com"));
|
||||
when(cloudfoundry.client.getDomains()).thenReturn(domains);
|
||||
Editor editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: foo\n" +
|
||||
@@ -1128,11 +1304,17 @@ public class ManifestYamlEditorTest {
|
||||
"- name: foo\n" +
|
||||
" routes:\n" +
|
||||
" - route: host.springsource.org\n");
|
||||
editor.assertProblems("springsource.org|Unknown domain");
|
||||
editor.assertProblems("springsource.org|Unknown 'Domain'. Valid domains are: [somedomain.com]");
|
||||
problem = editor.assertProblem("springsource.org");
|
||||
assertEquals(DiagnosticSeverity.Warning, problem.getSeverity());
|
||||
}
|
||||
|
||||
private CFDomain mockDomain(String name) {
|
||||
CFDomain domain = mock(CFDomain.class);
|
||||
when(domain.getName()).thenReturn(name);
|
||||
return domain;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconcileRouteValidDomain() throws Exception {
|
||||
ClientRequests cfClient = cloudfoundry.client;
|
||||
@@ -1257,7 +1439,7 @@ public class ManifestYamlEditorTest {
|
||||
"- name: foo\n" +
|
||||
" ro<*>"
|
||||
);
|
||||
editor.assertCompletions(c -> c.getLabel().contains("routes"),
|
||||
editor.assertCompletions(c -> c.getLabel().startsWith("routes"),
|
||||
"applications:\n" +
|
||||
"- name: foo\n" +
|
||||
" routes:\n"+
|
||||
@@ -1265,6 +1447,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 {
|
||||
|
||||
@@ -174,5 +174,10 @@ public class ManifestYmlSchemaTest {
|
||||
public Callable<Collection<YValueHint>> getBuildpackProviders() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callable<Collection<YValueHint>> getStacksProvider() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { RequestType, LanguageClient, LanguageClientOptions, SettingMonitor, Ser
|
||||
import { TextDocument, OutputChannel, Disposable, window } from 'vscode';
|
||||
import { Trace, NotificationType } from 'vscode-jsonrpc';
|
||||
import * as P2C from 'vscode-languageclient/lib/protocolConverter';
|
||||
import {WorkspaceEdit} from 'vscode-languageserver-types';
|
||||
import {WorkspaceEdit, Position} from 'vscode-languageserver-types';
|
||||
|
||||
let p2c = P2C.createConverter();
|
||||
|
||||
@@ -145,6 +145,7 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser
|
||||
}
|
||||
|
||||
let progressNotification = new NotificationType<ProgressParams,void>("sts/progress");
|
||||
let moveCursorRequest = new RequestType<MoveCursorParams,MoveCursorResponse,void,void>("sts/moveCursor");
|
||||
|
||||
let disposable = client.start();
|
||||
|
||||
@@ -155,6 +156,17 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser
|
||||
client.onNotification(progressNotification, (params: ProgressParams) => {
|
||||
progressService.handle(params);
|
||||
});
|
||||
client.onRequest(moveCursorRequest, (params: MoveCursorParams) => {
|
||||
let editors = VSCode.window.visibleTextEditors;
|
||||
for (let editor of editors) {
|
||||
if (editor.document.uri.toString() == params.uri) {
|
||||
let cursor = p2c.asPosition(params.position);
|
||||
let selection : VSCode.Selection = new VSCode.Selection(cursor, cursor);
|
||||
editor.selections = [ selection ];
|
||||
}
|
||||
}
|
||||
return { applied: true};
|
||||
});
|
||||
return client;
|
||||
});
|
||||
}
|
||||
@@ -204,6 +216,15 @@ function correctBinname(binname: string) {
|
||||
return binname;
|
||||
}
|
||||
|
||||
interface MoveCursorParams {
|
||||
uri: string
|
||||
position: Position
|
||||
}
|
||||
|
||||
interface MoveCursorResponse {
|
||||
applied: boolean
|
||||
}
|
||||
|
||||
interface ProgressParams {
|
||||
id: string
|
||||
statusMsg?: string
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||
"stopOnEntry": true,
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/out/lib",
|
||||
"preLaunchTask": "npm"
|
||||
|
||||
Reference in New Issue
Block a user