diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java index 55caced85..726eaa7b2 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java @@ -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) { diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFStack.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFStack.java new file mode 100644 index 000000000..8cb8b34d2 --- /dev/null +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFStack.java @@ -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 { + +} diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/ClientRequests.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/ClientRequests.java index 74e797579..7a4365a66 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/ClientRequests.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/ClientRequests.java @@ -18,5 +18,6 @@ public interface ClientRequests { List getBuildpacks() throws Exception; List getServices() throws Exception; List getDomains() throws Exception; - + List getStacks() throws Exception; + } diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFCallableContext.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFCallableContext.java index 977d921e8..078fd5891 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFCallableContext.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFCallableContext.java @@ -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(); } } \ No newline at end of file diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTarget.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTarget.java index c53a2feee..e86e7bde2 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTarget.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTarget.java @@ -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> buildpacksCache; private LoadingCache> servicesCache; private LoadingCache> domainCache; + private LoadingCache> 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> stacksLoader = new CacheLoader>() { + + @Override + public List 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> servicesLoader = new CacheLoader>() { @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> buildpacksLoader = new CacheLoader>() { @@ -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> domainLoader = new CacheLoader>() { @@ -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 runAndCheckForFailure(Callable 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 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 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()+"]"; } + } diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTargetCache.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTargetCache.java index 109495ddd..e0280f76d 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTargetCache.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/cftarget/CFTargetCache.java @@ -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 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); } diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/CFWrappingV2.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/CFWrappingV2.java index b16829bb4..9461b7c45 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/CFWrappingV2.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/CFWrappingV2.java @@ -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; + } + } diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/DefaultClientRequestsV2.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/DefaultClientRequestsV2.java index 30646d1d4..3461d8225 100644 --- a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/DefaultClientRequestsV2.java +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/v2/DefaultClientRequestsV2.java @@ -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 getDomains() throws Exception { @@ -98,7 +99,7 @@ public class DefaultClientRequestsV2 implements ClientRequests { ) ); } - + @Override public List getBuildpacks() throws Exception { return ReactorUtils.get(timeouts.getBuildpacksTimeout(), CancelationTokens.NULL, @@ -112,6 +113,18 @@ public class DefaultClientRequestsV2 implements ClientRequests { ) ); } + @Override + public List getStacks() throws Exception { + return ReactorUtils.get( + log("operations.stacks().list()", + _operations.stacks() + .list() + .map(CFWrappingV2::wrap) + .collectList() + ) + ); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////// //// calls to client and operations with 'logging'. diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java index 5c9bec553..4830dff83 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/STS4LanguageClient.java @@ -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 moveCursor(CursorMovement cursorMovement); + } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 54e19550c..8a3a1b544 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -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 edits = new ArrayList(); + private List edits = new ArrayList(); private IDocument doc; + /** + * When this is true, the cursor is moved after each edit, to be positioned right after the + * edit. + *

+ * 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; + } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java index 3fdb5bc4b..f510c4303 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java @@ -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 COMPARATOR = new Comparator() { - @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 COMPARATOR = new Comparator() { @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(); + } - } \ No newline at end of file +} \ No newline at end of file diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java index 56e6d3453..d5bf7c76c 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java @@ -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); } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java new file mode 100644 index 000000000..85e3ed852 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixEdit.java @@ -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; + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java index 634df1fc1..f0b534ffe 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixHandler.java @@ -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); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java index c879ea66a..52bd9a2ad 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixRegistry.java @@ -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 handle(QuickfixResolveParams params) { + public Mono handle(QuickfixResolveParams params) { QuickfixHandler handler = registry.get(params.getType()); return Mono.fromSupplier(() -> { return handler.createEdits(params.getParams()); diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index bb0d81781..ec037a7cb 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -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 applyEdit = Mono.fromFuture(client.applyEdit(new ApplyWorkspaceEditParams(edit.workspaceEdit))); + Mono 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 quickfixResolve(QuickfixResolveParams params) { + public Mono quickfixResolve(QuickfixResolveParams params) { QuickfixRegistry quickfixes = getQuickfixRegistry(); return quickfixes.handle(params); } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuzzyMatcher.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuzzyMatcher.java index 5fb77b6e8..a13f36166 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuzzyMatcher.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FuzzyMatcher.java @@ -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 } } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Unicodes.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Unicodes.java new file mode 100644 index 000000000..e4b478319 --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Unicodes.java @@ -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 = '←'; + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java index a0e05e9b7..193c7d182 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java @@ -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 diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/DefaultCompletionFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/DefaultCompletionFactory.java index aa494b63e..94832dd65 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/DefaultCompletionFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/DefaultCompletionFactory.java @@ -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; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java index 601066b4b..3e5887e1c 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java @@ -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(); + } } \ No newline at end of file diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index e3518afc2..69fe322ed 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -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 getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { - String query = getPrefix(doc, node, offset); - List 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 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 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 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 addDashes(Collection basicCompletions, YamlDocument doc, SNode node) { diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java index 184101ea0..68e306059 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java @@ -34,13 +34,4 @@ public interface YamlAssistContext extends YamlNavigable { 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; } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index 3d2b859c6..4a91634cd 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -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 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 contextNodes = getContextNodes(doc, current, offset); + if (current.getNodeType()==SNodeType.RAW) { + //relaxed indentation + List 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 addIndentations( - Collection completions) { + protected Collection 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 fixIndentations(Collection completions, SNode currentNode, SNode contextNode, int baseIndent) { if (!completions.isEmpty()) { + int dashyIndent = getTargetIndent(contextNode, currentNode, true); + int plainIndent = getTargetIndent(contextNode, currentNode, false); List 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 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 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 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 getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, boolean onlyDashes) throws Exception { + private Collection 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 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 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 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) { diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java index b806d0222..e9e357c4a 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java @@ -43,6 +43,16 @@ public interface YamlTraversal { */ > 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) { diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java index b4d15c6fc..9fa63297d 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/quickfix/YamlQuickfixes.java @@ -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; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index 7097c365b..9a0056140 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -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 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)); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ASTDynamicSchemaContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ASTDynamicSchemaContext.java index aa1af750f..86b19137d 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ASTDynamicSchemaContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ASTDynamicSchemaContext.java @@ -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; + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/DynamicSchemaContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/DynamicSchemaContext.java index 211e93f41..67e271f6c 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/DynamicSchemaContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/DynamicSchemaContext.java @@ -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 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. *

diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ISubCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ISubCompletionEngine.java new file mode 100644 index 000000000..e9c59fb79 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/ISubCompletionEngine.java @@ -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 getCompletions(CompletionFactory f, DocumentRegion region, int offset); + + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index a33d7da92..dc5493bc4 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -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 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 cachedPropertyMap; private SchemaContextAware>> hintProvider; private List 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; } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java index a78845a29..0f3bb1b4e 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java @@ -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 getConstraints(YType type); + + ISubCompletionEngine getCustomContentAssistant(YType type); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraint.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraint.java index 3d768b06d..dd5fb666a 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraint.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraint.java @@ -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. diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java index d9847d2e5..f1e098c2b 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java @@ -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 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 { diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 3c712d013..5ca2ce00d 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -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 labelPredicate) throws Exception { + List 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); + } + } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 65038befc..1c219cf38 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -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 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)); + } }); } diff --git a/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java b/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java index c07cce229..70d8f17ba 100644 --- a/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java +++ b/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java @@ -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" + ); + + } + } diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index ee0967cec..4c2b0ac5d 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -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+" "));; + } ////////////////////////////////////////////////////////////////////////////// diff --git a/headless-services/manifest-yaml-language-server/.settings/org.eclipse.jdt.ui.prefs b/headless-services/manifest-yaml-language-server/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..68f26e829 --- /dev/null +++ b/headless-services/manifest-yaml-language-server/.settings/org.eclipse.jdt.ui.prefs @@ -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 diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/AbstractCFHintsProvider.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/AbstractCFHintsProvider.java index 82f3a9a64..0f27a07ae 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/AbstractCFHintsProvider.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/AbstractCFHintsProvider.java @@ -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 { - Set keys = NodeUtil.getScalarKeys(parent); + Set 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)); - });; + }); }); }; } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlCFServicesProvider.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlCFServicesProvider.java index 3468e082e..0eccd95c8 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlCFServicesProvider.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlCFServicesProvider.java @@ -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(); + } } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlLanguageServer.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlLanguageServer.java index a149f00bd..d965d87c9 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlLanguageServer.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlLanguageServer.java @@ -95,9 +95,10 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { } protected ManifestYmlHintProviders getHintProviders() { - Callable> buildPacksProvider = getBuildpacksProvider(); - Callable> servicesProvider = getServicesProvider(); - Callable> domainsProvider = getDomainsProvider(); + Callable> buildPacksProvider = new ManifestYamlCFBuildpacksProvider(getCfTargetCache()); + Callable> servicesProvider = new ManifestYamlCFServicesProvider(getCfTargetCache()); + Callable> domainsProvider = new ManifestYamlCFDomainsProvider(getCfTargetCache()); + Callable> stacksProvider = new ManifestYamlStacksProvider(getCfTargetCache()); return new ManifestYmlHintProviders() { @@ -115,6 +116,11 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { public Callable> getBuildpackProviders() { return buildPacksProvider; } + + @Override + public Callable> getStacksProvider() { + return stacksProvider; + } }; } @@ -127,16 +133,4 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { return cfTargetCache; } - private Callable> getBuildpacksProvider() { - return new ManifestYamlCFBuildpacksProvider(getCfTargetCache()); - } - - private Callable> getServicesProvider() { - return new ManifestYamlCFServicesProvider(getCfTargetCache()); - } - - private Callable> getDomainsProvider() { - return new ManifestYamlCFDomainsProvider(getCfTargetCache()); - } - } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java index 64f9b1585..156397fc0 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java @@ -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); diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlStacksProvider.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlStacksProvider.java new file mode 100644 index 000000000..c89c3b8d1 --- /dev/null +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlStacksProvider.java @@ -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 getHints(List 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 hints = new ArrayList<>(); + + for (CFTarget cfTarget : targets) { + List 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; + } + + +} diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlHintProviders.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlHintProviders.java index 09558ba5b..3a89b5f45 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlHintProviders.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlHintProviders.java @@ -23,4 +23,6 @@ public interface ManifestYmlHintProviders { Callable> getDomainsProvider(); + Callable> getStacksProvider(); + } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index 32accaa0a..90353793c 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -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> buildpackProvider; + + public final AbstractType t_route_string; private static final Set 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> buildpackProvider = providers.getBuildpackProviders(); Callable> servicesProvider = providers.getServicesProvider(); Callable> domainsProvider = providers.getDomainsProvider(); + Callable> 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) { diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlValueParsers.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlValueParsers.java index b04a47124..6849f02df 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlValueParsers.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlValueParsers.java @@ -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> hintProvider, YAtomicType type, ProblemType problemType) { + return new EnumValueParser(type.toString(), YTypeFactory.valuesFromHintProvider(hintProvider)) { + @Override + protected Exception errorOnParse(String message) { + return new ReconcileException(message, problemType); + } + }; + } + } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteContentAssistant.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteContentAssistant.java new file mode 100644 index 000000000..5422d892d --- /dev/null +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteContentAssistant.java @@ -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> domainsProvider; + + private ManifestYmlSchema schema; + + public RouteContentAssistant(Callable> domainsProvider, ManifestYmlSchema manifestYmlSchema) { + this.domainsProvider = domainsProvider; + this.schema = manifestYmlSchema; + } + + @Override + public List 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 domains = domainsProvider.call(); + List 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; + } + +} diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java index 67e329516..440957a57 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java @@ -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) { diff --git a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java index 7f2e44941..f56e5d805 100644 --- a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java +++ b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java @@ -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 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 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 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 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 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 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 assertCompletions(String textBefore, String... textAfter) throws Exception { diff --git a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java index 7d0364a02..31ca4e5ff 100644 --- a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java +++ b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java @@ -174,5 +174,10 @@ public class ManifestYmlSchemaTest { public Callable> getBuildpackProviders() { return null; } + + @Override + public Callable> getStacksProvider() { + return null; + } }; } diff --git a/vscode-extensions/commons-vscode/src/launch-util.ts b/vscode-extensions/commons-vscode/src/launch-util.ts index 0463ab120..a465ad859 100644 --- a/vscode-extensions/commons-vscode/src/launch-util.ts +++ b/vscode-extensions/commons-vscode/src/launch-util.ts @@ -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("sts/progress"); + let moveCursorRequest = new RequestType("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 diff --git a/vscode-extensions/vscode-concourse/.vscode/launch.json b/vscode-extensions/vscode-concourse/.vscode/launch.json index 140324f98..51d437ae0 100644 --- a/vscode-extensions/vscode-concourse/.vscode/launch.json +++ b/vscode-extensions/vscode-concourse/.vscode/launch.json @@ -9,7 +9,7 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], - "stopOnEntry": true, + "stopOnEntry": false, "sourceMaps": true, "outDir": "${workspaceRoot}/out/lib", "preLaunchTask": "npm"