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:
BoykoAlex
2017-05-03 14:55:06 -04:00
55 changed files with 1780 additions and 409 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.commons.languageserver.completion;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = '←';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.List;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.yaml.completion.CompletionFactory;
/**
* Interface that can be used by a {@link ICompletionEngine} to delegate to a
* 'helper' completion engine that computes completions for some sub-region of
* the document.
*
* @author Kris De Volder
*/
@FunctionalInterface
public interface ISubCompletionEngine {
/**
* @param region The relevant subregion of the document (typically, the sub-engine only cares about the text
* within this particular region.)
* @param offset The cursor position relative to the region.
*/
List<ICompletionProposal> getCompletions(CompletionFactory f, DocumentRegion region, int offset);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,4 +23,6 @@ public interface ManifestYmlHintProviders {
Callable<Collection<YValueHint>> getDomainsProvider();
Callable<Collection<YValueHint>> getStacksProvider();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -174,5 +174,10 @@ public class ManifestYmlSchemaTest {
public Callable<Collection<YValueHint>> getBuildpackProviders() {
return null;
}
@Override
public Callable<Collection<YValueHint>> getStacksProvider() {
return null;
}
};
}

View File

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

View File

@@ -9,7 +9,7 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": true,
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/out/lib",
"preLaunchTask": "npm"