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 0c4f3370d..91af01c56 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 @@ -37,7 +37,7 @@ public abstract class ScoreableProposal implements ICompletionProposal { if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) { double s1 = ((ScoreableProposal)p1).getScore(); double s2 = ((ScoreableProposal)p2).getScore(); - if (Math.abs(s1-s2)<1E-5) { + if (s1==s2) { String name1 = ((ScoreableProposal)p1).getLabel(); String name2 = ((ScoreableProposal)p2).getLabel(); return name1.compareTo(name2); @@ -45,7 +45,13 @@ public abstract class ScoreableProposal implements ICompletionProposal { return Double.compare(s2, s1); } } - return 0; + if (p1 instanceof ScoreableProposal) { + return -1; + } + if (p2 instanceof ScoreableProposal) { + return +1; + } + return p1.getLabel().compareTo(p2.getLabel()); } }; public abstract double getBaseScore(); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/SimpleCompletionFactory.java similarity index 79% rename from headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java rename to headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/SimpleCompletionFactory.java index 018f68865..cced6fd95 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/SimpleCompletionFactory.java @@ -8,11 +8,10 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.handlers; +package org.springframework.ide.vscode.commons.languageserver.completion; import org.eclipse.lsp4j.CompletionItemKind; -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.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -20,13 +19,13 @@ public class SimpleCompletionFactory { public static class SimpleProposal implements ICompletionProposal{ - + private DocumentEdits edits; private CompletionItemKind kind; private Renderable info; private String detail; private String label; - + public SimpleProposal(DocumentEdits edits, CompletionItemKind kind, Renderable info, String detail, String label) { this.edits = edits; @@ -66,9 +65,17 @@ public class SimpleCompletionFactory { return detail; } + @Override + public String toString() { + return "SimpleProposal("+label+")"; + } } + public static SimpleProposal simpleProposal(DocumentRegion query, CompletionItemKind kind, String value, String detail, Renderable info) { + return simpleProposal(query.getDocument(), query.getEnd(), query.toString(), kind, value, detail, info); + } + public static SimpleProposal simpleProposal(IDocument doc, int offset, String query, CompletionItemKind kind, String value, String detail, Renderable info) { DocumentEdits edits = new DocumentEdits(doc); edits.replace(offset-query.length(), offset, value); diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java index b1899f294..915732199 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java @@ -181,6 +181,10 @@ public class DocumentRegion implements CharSequence, IRegion { return -1; } + public int indexOf(char c) { + return indexOf(c, 0); + } + public DocumentRegion[] split(char c) { List pieces = new ArrayList<>(); int start = 0; @@ -338,5 +342,4 @@ public class DocumentRegion implements CharSequence, IRegion { return getStart(); } - } \ No newline at end of file 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 a13f36166..7a3bacd2c 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 @@ -24,7 +24,7 @@ public class FuzzyMatcher { * 'score' when it does. The higher the score, the better the match is considered to * be. */ - public static double matchScore(String pattern, String data) { + public static double matchScore(CharSequence pattern, String data) { int ppos = 0; //pos of next char in pattern to look for int dpos = 0; //pos of next char in data not yet matched int gaps = 0; //number of 'gaps' in the match. A gap is any non-empty run of consecutive characters in the data that are not used by the match @@ -61,7 +61,7 @@ public class FuzzyMatcher { return score(gaps, skips, pattern); } - private static double score(int gaps, int skips, String pattern) { + private static double score(int gaps, int skips, CharSequence pattern) { if (gaps==0) { //gaps == 0 means a prefix match, ignore 'skips' at end of String and just sort // alphabetic (see STS-4049) 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 193c7d182..af771b00e 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 @@ -55,21 +55,25 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { 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); + DocumentRegion region = new DocumentRegion(doc.getDocument(), valueStart, valueEnd); + if (region.startsWith(" ")) { + region = region.subSequence(1); + } + return region; } } 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); } }; - + 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. diff --git a/headless-services/concourse-language-server/pom.xml b/headless-services/concourse-language-server/pom.xml index 0b8641f90..b5bf6ca9e 100644 --- a/headless-services/concourse-language-server/pom.xml +++ b/headless-services/concourse-language-server/pom.xml @@ -37,7 +37,19 @@ commons-yaml ${dependencies.version} + + + org.kohsuke + github-api + 1.90 + + + org.mockito + mockito-all + ${mockito-version} + test + org.springframework.ide.vscode language-server-test-harness diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java index c2bab9ad3..2cd60c74e 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java @@ -40,6 +40,7 @@ import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; +import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; import com.google.common.collect.ImmutableList; @@ -90,10 +91,10 @@ public class ConcourseLanguageServer extends SimpleLanguageServer { } } - public ConcourseLanguageServer(YamlCompletionEngineOptions completionOptions) { + public ConcourseLanguageServer(YamlCompletionEngineOptions completionOptions, GithubInfoProvider github) { super("vscode-concourse"); this.COMPLETION_OPTIONS = completionOptions; - PipelineYmlSchema pipelineSchema = new PipelineYmlSchema(models); + PipelineYmlSchema pipelineSchema = new PipelineYmlSchema(models, github); enableSnippets(pipelineSchema, true); this.yamlQuickfixes = new YamlQuickfixes(getQuickfixRegistry(), documents, structureProvider); diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/Main.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/Main.java index 7a331cbc2..7aaadc68e 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/Main.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/Main.java @@ -16,13 +16,19 @@ import java.io.IOException; import org.springframework.ide.vscode.commons.languageserver.LaunguageServerApp; import org.springframework.ide.vscode.commons.util.LogRedirect; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions; +import org.springframework.ide.vscode.concourse.github.DefaultGithubInfoProvider; +import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; + +import static org.springframework.ide.vscode.commons.languageserver.LaunguageServerApp.STANDALONE_STARTUP; public class Main { private static final YamlCompletionEngineOptions OPTIONS = YamlCompletionEngineOptions.DEFAULT; - public static void main(String[] args) throws IOException, InterruptedException { - String serverName = "concourse-language-server"; - LogRedirect.redirectToFile(serverName); - LaunguageServerApp.start(serverName, () -> new ConcourseLanguageServer(OPTIONS)); + public static void main(String[] args) throws IOException, InterruptedException { + String serverName = "concourse-language-server"; + if (!Boolean.getBoolean(STANDALONE_STARTUP)) { + LogRedirect.redirectToFile(serverName); + } + LaunguageServerApp.start(serverName, () -> new ConcourseLanguageServer(OPTIONS, new DefaultGithubInfoProvider())); } } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 6a705466e..8b722e897 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -47,6 +47,8 @@ import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; import org.springframework.ide.vscode.concourse.ConcourseModel.ResourceModel; import org.springframework.ide.vscode.concourse.ConcourseModel.StepModel; +import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; +import org.springframework.ide.vscode.concourse.github.GithubRepoContentAssistant; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; @@ -160,7 +162,10 @@ public class PipelineYmlSchema implements YamlSchema { private List definitionTypes = new ArrayList<>(); - public PipelineYmlSchema(ConcourseModel models) { + private GithubInfoProvider github; + + public PipelineYmlSchema(ConcourseModel models, GithubInfoProvider github) { + this.github = github; this.models = models; this.asts = models.getAstCache(); models.setResourceTypeRegistry(resourceTypes); @@ -417,8 +422,11 @@ public class PipelineYmlSchema implements YamlSchema { private void initializeDefaultResourceTypes() { // git : { + AbstractType t_git_repo_uri = f.yatomic("GitRepoUri"); + t_git_repo_uri.setCustomContentAssistant(new GithubRepoContentAssistant(github)); + AbstractType source = f.ybean("GitSource"); - addProp(source, "uri", t_ne_string).isPrimary(true); + addProp(source, "uri", t_git_repo_uri).isPrimary(true); addProp(source, "branch", t_ne_string); //It's more complicated than that! Its only required in 'put' step. So we'll check this as a contrain in put steps! addProp(source, "private_key", t_ne_string); addProp(source, "username", t_ne_string); diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java new file mode 100644 index 000000000..67f122405 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/DefaultGithubInfoProvider.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * 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.concourse.github; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +import org.kohsuke.github.GHPerson; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.springframework.ide.vscode.commons.util.CollectorUtil; +import org.springframework.ide.vscode.commons.util.Log; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Flux; + +public class DefaultGithubInfoProvider implements GithubInfoProvider { + + //TODO: we only try to connect to github once and cache the connection. + //This means that, if creating the connection fails we won't try again. + //It would be nice to cache connection only for a limited amount of time + //especially when connecting fails. So that user may try to address the + //issue and try again. + + private GitHub github; + private IOException connectionError; + private Collection owners; + + private Cache> reposByOwner = CacheBuilder.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(); + + { + try { + checkConfiguration(); + github = GitHub.connect(); + } catch (IOException e) { + connectionError = e; + } + } + + @Override + public Collection getOwners() throws Exception { + if (connectionError!=null) { + throw connectionError; + } + if (github!=null) { + if (owners==null) { + ImmutableSet.Builder owners = ImmutableSet.builder(); + for (GHRepository repo : github.getMyself().listRepositories()) { + owners.add(repo.getOwnerName()); + } + this.owners = owners.build(); + } + return owners; + } + return ImmutableList.of(); + } + + private void checkConfiguration() throws IOException { + File configFile = new File(System.getProperty("user.home")); + configFile = new File(configFile, ".github"); + if (!configFile.isFile()) { + throw new IOException("`~/.github` doesn't exist: You will get better content assist for github " + + "repos if you create a file at `~/.github` containing your github login and password:\n"+ + "\n" + + " login=...username...\n" + + " password=...password...\n"+ + "\n" + + "Note: Github connection data is cached indefinitely, so the editor will need to be restarted for " + + "this to take effect." + ); + } + } + + @Override + public Collection getReposForOwner(String ownerName) throws Exception { + if (connectionError!=null) { + throw connectionError; + } + try { + if (github!=null) { + return reposByOwner.get(ownerName, () -> { + GHPerson owner = getOwner(ownerName); + return Flux.fromIterable(owner.listRepositories()) + .filter(repo -> repo.getOwnerName().equals(ownerName)) + .map(GHRepository::getName) + .collect(CollectorUtil.toImmutableSet()) + .block(); + }); + } + } catch (Exception e) { + Log.log(e); + } + return ImmutableList.of(); + } + + private GHPerson getOwner(String ownerName) throws IOException { + try { + return github.getUser(ownerName); + } catch (IOException e) { + return github.getOrganization(ownerName); + } + } + +} diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java new file mode 100644 index 000000000..b23f3dac4 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubInfoProvider.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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.concourse.github; + +import java.util.Collection; + +public interface GithubInfoProvider { + + /** + * Retrieves a list of owners that can be suggested as hints in completions of github repo urls. + *

+ * Note: since github has millions of users and fetching all of them isn't really an option, the owners + * are expected to be somehow limited based on the user credentials (i.e. returning a list that is + * deemed relevant to the current logged in user, rather than an exhaustive list of every org and user name + * on github. + */ + Collection getOwners() throws Exception; + Collection getReposForOwner(String owner) throws Exception; + +} diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java new file mode 100644 index 000000000..606f30181 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/github/GithubRepoContentAssistant.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.concourse.github; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.languageserver.completion.SimpleCompletionFactory; +import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.ExceptionUtil; +import org.springframework.ide.vscode.commons.util.FuzzyMatcher; +import org.springframework.ide.vscode.commons.yaml.completion.CompletionFactory; +import org.springframework.ide.vscode.commons.yaml.schema.ISubCompletionEngine; + +import org.eclipse.lsp4j.CompletionItemKind; + +import com.google.common.collect.ImmutableList; + +public class GithubRepoContentAssistant implements ISubCompletionEngine { + + private String[] uriPrefixes = { + "git@github.com:", + "https://github.com/" + }; + private GithubInfoProvider github; + + public GithubRepoContentAssistant(GithubInfoProvider github) { + this.github = github; + } + + @Override + public List getCompletions(CompletionFactory f, DocumentRegion region, int offset) { + DocumentRegion query = region.subSequence(0, offset); + //If uri prefix is already there, we provide CA for owner / repo + for (String uriPrefix : uriPrefixes) { + if (query.startsWith(uriPrefix)) { + return getOwnerOrRepoCompletions(f, query.subSequence(uriPrefix.length())); + } + } + //If uri prefix is not yet there, maybe we can suggest it (if it matches the query) + List proposals = new ArrayList<>(uriPrefixes.length); + for (String uriPrefix : uriPrefixes) { + if (FuzzyMatcher.matchScore(query, uriPrefix)!=0.0) { + proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, uriPrefix, null, null)); + } + } + return proposals; + } + + private List getOwnerOrRepoCompletions(CompletionFactory f, DocumentRegion ownerAndRepoRegion) { + try { + int slash = ownerAndRepoRegion.indexOf('/'); + if (slash>=0) { + DocumentRegion owner = ownerAndRepoRegion.subSequence(0, slash); + return getRepoCompletions(f, owner, ownerAndRepoRegion.subSequence(slash+1)); + } else { + Collection owners = github.getOwners(); + DocumentRegion query = ownerAndRepoRegion; + if (!owners.isEmpty()) { + List proposals = new ArrayList<>(owners.size()); + for (String owner : owners) { + if (FuzzyMatcher.matchScore(query, owner)!=0.0) { + proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, owner+"/", null, null)); + } + } + return proposals; + } else { + return ImmutableList.of(); + } + } + } catch (Exception e) { + return ImmutableList.of(f.errorMessage(ownerAndRepoRegion.toString(), ExceptionUtil.getMessageNoAppendedInformation(e))); + } + } + + private List getRepoCompletions(CompletionFactory f, DocumentRegion owner, DocumentRegion query) { + try { + Collection repos = github.getReposForOwner(owner.toString()); + if (repos!=null && !repos.isEmpty()) { + List proposals = new ArrayList<>(repos.size()); + for (String repo : repos) { + if (FuzzyMatcher.matchScore(query, repo)!=0.0) { + proposals.add(SimpleCompletionFactory.simpleProposal(query, CompletionItemKind.Text, repo, null, null)); + } + } + return proposals; + } else { + return ImmutableList.of(); + } + } catch (Exception e) { + return ImmutableList.of(f.errorMessage(query.toString(), ExceptionUtil.getMessageNoAppendedInformation(e))); + } + } + +} 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 cd29cfb08..9eb954967 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 @@ -12,8 +12,12 @@ package org.springframework.ide.vscode.concourse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.INDENTED_COMPLETION; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.PLAIN_COMPLETION; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; +import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; @@ -31,12 +35,15 @@ import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; +import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; import org.springframework.ide.vscode.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; import org.springframework.ide.vscode.languageserver.testharness.SynchronizationPoint; -import static org.springframework.ide.vscode.languageserver.testharness.Editor.*; +import com.google.common.collect.ImmutableList; + +import org.mockito.Mockito; public class ConcourseEditorTest { @@ -45,9 +52,11 @@ public class ConcourseEditorTest { private static final String CURSOR = "<*>"; LanguageServerHarness harness; + private GithubInfoProvider github= Mockito.mock(GithubInfoProvider.class); + @Before public void setup() throws Exception { harness = new LanguageServerHarness(() -> { - return new ConcourseLanguageServer(OPTIONS) + return new ConcourseLanguageServer(OPTIONS, github) .setMaxCompletions(100); }, LanguageId.CONCOURSE_PIPELINE @@ -4373,7 +4382,107 @@ public class ConcourseEditorTest { "publish-snapshot|'publish-snapshot' belongs to no group", "publish-release|'publish-release' belongs to no group" ); + } + @Test public void githubCompletionsUriTypes() throws Exception { + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: <*>" + ); + editor.assertContextualCompletions("<*>", + "git@github.com:<*>", + "https://github.com/<*>" + ); + + editor.assertContextualCompletions("@<*>", + "git@github.com:<*>" + ); + } + + @Test public void githubCompletionsOwners() throws Exception { + when(github.getOwners()).thenReturn(ImmutableList.of( + "kdvolder", "spring-projects", "spring-guides" + )); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: <*>" + ); + + editor.assertContextualCompletions("git@github.com:<*>", + "git@github.com:kdvolder/<*>", + "git@github.com:spring-guides/<*>", + "git@github.com:spring-projects/<*>" + ); + + editor.assertContextualCompletions("git@github.com:vol<*>", + "git@github.com:kdvolder/<*>" + ); + editor.assertContextualCompletions("https://github.com/<*>", + "https://github.com/kdvolder/<*>", + "https://github.com/spring-guides/<*>", + "https://github.com/spring-projects/<*>" + ); + + } + + @Test public void githubCompletionsRepos() throws Exception { + when(github.getReposForOwner("the-owner")).thenReturn(ImmutableList.of( + "nice-repo", "cool-project", "good-stuff" + )); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: <*>" + ); + editor.assertContextualCompletions("https://github.com/the-owner/<*>", + "https://github.com/the-owner/cool-project<*>", + "https://github.com/the-owner/good-stuff<*>", + "https://github.com/the-owner/nice-repo<*>" + ); + + editor.assertContextualCompletions("https://github.com/the-owner/proj<*>", + "https://github.com/the-owner/cool-project<*>" + ); + + editor.assertContextualCompletions("git@github.com:the-owner/<*>", + "git@github.com:the-owner/cool-project<*>", + "git@github.com:the-owner/good-stuff<*>", + "git@github.com:the-owner/nice-repo<*>" + ); + + editor.assertContextualCompletions("git@github.com:the-owner/proj<*>", + "git@github.com:the-owner/cool-project<*>" + ); + } + + @Test public void githubCompletionErrors() throws Exception { + when(github.getReposForOwner("the-owner")).thenThrow(new IOException("Explain some stuff")); + when(github.getOwners()).thenThrow(new IOException("Explain some stuff")); + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:the-owner/<*>" + ); + editor.assertCompletionLabels("Explain some stuff"); + + editor.setText( + "resources:\n" + + "- name: my-repo\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:<*>" + ); + editor.assertCompletionLabels("Explain some stuff"); } ////////////////////////////////////////////////////////////////////////////// diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerTest.java index 26ddd0d71..c9ccf1fe3 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerTest.java @@ -20,7 +20,9 @@ import java.nio.file.Paths; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.ide.vscode.concourse.ConcourseLanguageServer; +import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; import static org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions.*; @@ -32,7 +34,8 @@ public class ConcourseLanguageServerTest { @Test public void createAndInitializeServerWithWorkspace() throws Exception { - LanguageServerHarness harness = new LanguageServerHarness(() -> new ConcourseLanguageServer(TEST_DEFAULT)); + LanguageServerHarness harness = new LanguageServerHarness(() -> + new ConcourseLanguageServer(TEST_DEFAULT, Mockito.mock(GithubInfoProvider.class))); File workspaceRoot = getTestResource("/workspace/"); assertExpectedInitResult(harness.intialize(workspaceRoot)); } @@ -40,7 +43,8 @@ public class ConcourseLanguageServerTest { @Test public void createAndInitializeServerWithoutWorkspace() throws Exception { File workspaceRoot = null; - LanguageServerHarness harness = new LanguageServerHarness(() -> new ConcourseLanguageServer(TEST_DEFAULT)); + LanguageServerHarness harness = new LanguageServerHarness(() -> + new ConcourseLanguageServer(TEST_DEFAULT, Mockito.mock(GithubInfoProvider.class))); assertExpectedInitResult(harness.intialize(workspaceRoot)); } diff --git a/vscode-extensions/vscode-concourse/lib/Main.ts b/vscode-extensions/vscode-concourse/lib/Main.ts index d027ac7b8..7f9412350 100644 --- a/vscode-extensions/vscode-concourse/lib/Main.ts +++ b/vscode-extensions/vscode-concourse/lib/Main.ts @@ -32,7 +32,7 @@ function error(msg : string) { export function activate(context: VSCode.ExtensionContext) { let options : commons.ActivatorOptions = { DEBUG : false, - CONNECT_TO_LS: false, + CONNECT_TO_LS: true, extensionId: 'vscode-concourse', launcher: (context: VSCode.ExtensionContext) => Path.resolve(context.extensionPath, 'jars/language-server.jar'), jvmHeap: "48m",