From 0b1b5cdf2a9f4784a5747f2fd6f04d5db9d3c1f5 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 1 Aug 2018 15:23:57 -0700 Subject: [PATCH] Fix https://www.pivotaltracker.com/story/show/153107266 Completions inside of @Value(...) context now also suggest 'ad-hoc' properties that are defined in application.properties or application.yml (in addition to regular properties defined via @ConfigurationProperties). --- .../util/SimpleServerFileObserver.java | 21 +- .../commons/util/BasicFileObserver.java | 14 - .../ide/vscode/commons/util/FileObserver.java | 14 +- .../spring-boot-language-server/.classpath | 6 +- .../org.eclipse.core.resources.prefs | 1 + .../vscode/boot/BootLanguageServerParams.java | 9 + .../BootJavaLanguageServerComponents.java | 13 +- .../java/value/ValueCompletionProcessor.java | 52 ++-- .../java/value/ValuePropertyKeyProposal.java | 20 +- .../AdHocSpringPropertyIndexProvider.java | 173 +++++++++++ .../DefaultSpringPropertyIndexProvider.java | 4 +- .../vscode/boot/metadata/PropertyInfo.java | 5 +- .../SpringPropertiesIndexManager.java | 38 +-- .../boot/metadata/SpringPropertyIndex.java | 6 +- .../AdHocSpringPropertyIndexProviderTest.java | 135 +++++++++ .../boot/java/value/test/MockProjects.java | 279 ++++++++++++++++++ .../java/value/test/ValueCompletionTest.java | 48 ++- .../test/ApplicationPropertiesEditorTest.java | 2 + .../boot/test/ApplicationYamlEditorTest.java | 2 + .../BootJavaLanguageServerHarness.java | 3 + .../project/harness/ProjectsHarness.java | 4 +- .../project/harness/PropertyIndexHarness.java | 16 + .../src/main/resources/application.properties | 3 - .../src/main/resources/application.yml | 4 - .../test-projects/test-annotations/pom.xml | 2 +- 25 files changed, 783 insertions(+), 91 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/AdHocSpringPropertyIndexProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/MockProjects.java delete mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.yml diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleServerFileObserver.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleServerFileObserver.java index 5785947f5..ca87b358b 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleServerFileObserver.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleServerFileObserver.java @@ -22,6 +22,7 @@ import org.eclipse.lsp4j.Unregistration; import org.eclipse.lsp4j.UnregistrationParams; import org.springframework.ide.vscode.commons.languageserver.json.DidChangeWatchedFilesRegistrationOptions; import org.springframework.ide.vscode.commons.languageserver.json.FileSystemWatcher; +import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.BasicFileObserver; /** @@ -62,18 +63,22 @@ public class SimpleServerFileObserver extends BasicFileObserver { } private void subscribe(String subscriptionId, List globPattern, int kind) { - if (server.canRegisterFileWatchersDynamically()) { - List watchers = globPattern.stream().map(pattern -> new FileSystemWatcher(pattern, kind)).collect(Collectors.toList()); - Registration registration = new Registration(subscriptionId, WORKSPACE_DID_CHANGE_WATCHED_FILES, new DidChangeWatchedFilesRegistrationOptions(watchers)); - server.getClient().registerCapability(new RegistrationParams(Arrays.asList(registration))); - } + server.onInitialized(() -> { + if (server.canRegisterFileWatchersDynamically()) { + List watchers = globPattern.stream().map(pattern -> new FileSystemWatcher(pattern, kind)).collect(Collectors.toList()); + Registration registration = new Registration(subscriptionId, WORKSPACE_DID_CHANGE_WATCHED_FILES, new DidChangeWatchedFilesRegistrationOptions(watchers)); + server.getClient().registerCapability(new RegistrationParams(Arrays.asList(registration))); + } + }); } @Override public boolean unsubscribe(String subscriptionId) { - if (server.canRegisterFileWatchersDynamically()) { - server.getClient().unregisterCapability(new UnregistrationParams(Arrays.asList(new Unregistration(subscriptionId, WORKSPACE_DID_CHANGE_WATCHED_FILES)))); - } + server.onInitialized(() -> { + if (server.canRegisterFileWatchersDynamically()) { + server.getClient().unregisterCapability(new UnregistrationParams(Arrays.asList(new Unregistration(subscriptionId, WORKSPACE_DID_CHANGE_WATCHED_FILES)))); + } + }); return super.unsubscribe(subscriptionId); } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java index 0d7303cf9..cf4ca070f 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/BasicFileObserver.java @@ -97,18 +97,4 @@ public class BasicFileObserver implements FileObserver { .forEach(pair -> pair.right.accept(uri)); } - @Override - public Disposable onAnyChange(List globPattern, Consumer handler) { - String[] ids = { - onFileChanged(globPattern, handler), - onFileCreated(globPattern, handler), - onFileDeleted(globPattern, handler) - }; - return () -> { - for (String id : ids) { - unsubscribe(id); - } - }; - } - } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java index 0f47ecec1..e3e441684 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/FileObserver.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.util; +import java.io.File; import java.util.List; import java.util.function.Consumer; @@ -31,6 +32,17 @@ public interface FileObserver { boolean unsubscribe(String subscriptionId); - Disposable onAnyChange(List globPattern, Consumer handler); + default Disposable onAnyChange(List globPattern, Consumer handler) { + String[] ids = { + onFileChanged(globPattern, handler), + onFileCreated(globPattern, handler), + onFileDeleted(globPattern, handler) + }; + return () -> { + for (String id : ids) { + unsubscribe(id); + } + }; + } } diff --git a/headless-services/spring-boot-language-server/.classpath b/headless-services/spring-boot-language-server/.classpath index ae371e919..66b5a171e 100644 --- a/headless-services/spring-boot-language-server/.classpath +++ b/headless-services/spring-boot-language-server/.classpath @@ -13,7 +13,11 @@ - + + + + + diff --git a/headless-services/spring-boot-language-server/.settings/org.eclipse.core.resources.prefs b/headless-services/spring-boot-language-server/.settings/org.eclipse.core.resources.prefs index cdfe4f1b6..29abf9995 100644 --- a/headless-services/spring-boot-language-server/.settings/org.eclipse.core.resources.prefs +++ b/headless-services/spring-boot-language-server/.settings/org.eclipse.core.resources.prefs @@ -1,5 +1,6 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 encoding//src/test/java=UTF-8 encoding//src/test/resources=UTF-8 encoding/=UTF-8 diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/BootLanguageServerParams.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/BootLanguageServerParams.java index 3c3aba010..9c7db4040 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/BootLanguageServerParams.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/BootLanguageServerParams.java @@ -21,7 +21,9 @@ import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService; import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsServiceWithFallback; import org.springframework.ide.vscode.boot.jdt.ls.JdtLsProjectCache; +import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.DefaultSpringPropertyIndexProvider; +import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndex; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; import org.springframework.ide.vscode.boot.metadata.types.TypeUtilProvider; @@ -58,6 +60,7 @@ public class BootLanguageServerParams { public final JavaProjectFinder projectFinder; public final ProjectObserver projectObserver; public final SpringPropertyIndexProvider indexProvider; + public final SpringPropertyIndexProvider adHocIndexProvider; //Boot Properies public final TypeUtilProvider typeUtilProvider; @@ -70,6 +73,7 @@ public class BootLanguageServerParams { JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringPropertyIndexProvider indexProvider, + SpringPropertyIndexProvider adHocIndexProvider, TypeUtilProvider typeUtilProvider, RunningAppProvider runningAppProvider, Duration watchDogInterval @@ -79,6 +83,7 @@ public class BootLanguageServerParams { this.projectFinder = projectFinder; this.projectObserver = projectObserver; this.indexProvider = indexProvider; + this.adHocIndexProvider = adHocIndexProvider; this.typeUtilProvider = typeUtilProvider; this.runningAppProvider = runningAppProvider; this.watchDogInterval = watchDogInterval; @@ -93,12 +98,14 @@ public class BootLanguageServerParams { () -> createFallbackProjectCache(server) ); DefaultSpringPropertyIndexProvider indexProvider = new DefaultSpringPropertyIndexProvider(jdtProjectCache, jdtProjectCache); + SpringPropertyIndexProvider adHocProvider = new AdHocSpringPropertyIndexProvider(jdtProjectCache, jdtProjectCache, server.getWorkspaceService().getFileObserver()); indexProvider.setProgressService(server.getProgressService()); return new BootLanguageServerParams( jdtProjectCache.filter(BootProjectUtil::isBootProject), jdtProjectCache, indexProvider, + adHocProvider, (IDocument doc) -> new TypeUtil(jdtProjectCache.find(new TextDocumentIdentifier(doc.getUri()))), RunningAppProvider.createDefault(server), SpringLiveHoverWatchdog.DEFAULT_INTERVAL @@ -161,6 +168,7 @@ public class BootLanguageServerParams { javaProjectFinder.filter(BootProjectUtil::isBootProject), projectObserver, indexProvider, + (doc) -> SpringPropertyIndex.EMPTY_INDEX, typeUtilProvider, RunningAppProvider.NULL, SpringLiveHoverWatchdog.DEFAULT_INTERVAL @@ -189,6 +197,7 @@ public class BootLanguageServerParams { javaProjectFinder.filter(BootProjectUtil::isBootProject), projectObserver, indexProvider, + (doc) -> SpringPropertyIndex.EMPTY_INDEX, (IDocument doc) -> new TypeUtil(javaProjectFinder.find(new TextDocumentIdentifier(doc.getUri()))), RunningAppProvider.NULL, SpringLiveHoverWatchdog.DEFAULT_INTERVAL diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index fb1ba06a9..301e8ad9c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -95,6 +95,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private final BootLanguageServerParams serverParams; private final SpringIndexer indexer; private final SpringPropertyIndexProvider propertyIndexProvider; + private final SpringPropertyIndexProvider adHocPropertyIndexProvider; private final SpringLiveHoverWatchdog liveHoverWatchdog; private final SpringLiveChangeDetectionWatchdog liveChangeDetectionWatchdog; private final ProjectObserver projectObserver; @@ -117,6 +118,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent cuCache = new CompilationUnitCache(projectFinder, server.getTextDocumentService(), projectObserver); propertyIndexProvider = serverParams.indexProvider; + adHocPropertyIndexProvider = serverParams.adHocIndexProvider; SimpleWorkspaceService workspaceService = server.getWorkspaceService(); SimpleTextDocumentService documents = server.getTextDocumentService(); @@ -205,7 +207,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent @Override public ICompletionEngine getCompletionEngine() { - return createCompletionEngine(projectFinder, propertyIndexProvider); + return createCompletionEngine(projectFinder, propertyIndexProvider, adHocPropertyIndexProvider); } @Override @@ -244,13 +246,16 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent this.cuCache.dispose(); } - protected ICompletionEngine createCompletionEngine(JavaProjectFinder javaProjectFinder, - SpringPropertyIndexProvider indexProvider) { + protected ICompletionEngine createCompletionEngine( + JavaProjectFinder javaProjectFinder, + SpringPropertyIndexProvider indexProvider, + SpringPropertyIndexProvider adHocIndexProvider + ) { Map providers = new HashMap<>(); providers.put(org.springframework.ide.vscode.boot.java.scope.Constants.SPRING_SCOPE, new ScopeCompletionProcessor()); providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, - new ValueCompletionProcessor(indexProvider)); + new ValueCompletionProcessor(indexProvider, adHocIndexProvider)); JavaSnippetManager snippetManager = new JavaSnippetManager(server::createSnippetBuilder); snippetManager.add( diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java index 92f9d72ae..e9e0eeac6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java @@ -14,7 +14,9 @@ import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseTo import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; @@ -22,7 +24,6 @@ import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.StringLiteral; -import org.springframework.ide.vscode.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; import org.springframework.ide.vscode.boot.metadata.PropertyInfo; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; @@ -39,9 +40,11 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; public class ValueCompletionProcessor implements CompletionProvider { private final SpringPropertyIndexProvider indexProvider; + private SpringPropertyIndexProvider adHocIndexProvider; - public ValueCompletionProcessor(SpringPropertyIndexProvider indexProvider) { + public ValueCompletionProcessor(SpringPropertyIndexProvider indexProvider, SpringPropertyIndexProvider adHocIndexProvider) { this.indexProvider = indexProvider; + this.adHocIndexProvider = adHocIndexProvider; } @Override @@ -51,41 +54,39 @@ public class ValueCompletionProcessor implements CompletionProvider { List result = new ArrayList<>(); try { - FuzzyMap index = indexProvider.getIndex(doc); - // case: @Value(<*>) if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { - List> matches = findMatches("", index); + List> matches = findMatches("", doc); for (Match match : matches) { DocumentEdits edits = new DocumentEdits(doc); edits.replace(offset, offset, "\"${" + match.data.getId() + "}\""); - ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match.data.getId(), match.data.getName(), null); + ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match); result.add(proposal); } } // case: @Value(prefix<*>) else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { - computeProposalsForSimpleName(node, result, offset, doc, index); + computeProposalsForSimpleName(node, result, offset, doc); } // case: @Value(value=<*>) else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { - computeProposalsForSimpleName(node, result, offset, doc, index); + computeProposalsForSimpleName(node, result, offset, doc); } // case: @Value("prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(node, result, offset, doc, index); + computeProposalsForStringLiteral(node, result, offset, doc); } } // case: @Value(value="prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(node, result, offset, doc, index); + computeProposalsForStringLiteral(node, result, offset, doc); } } } @@ -97,7 +98,7 @@ public class ValueCompletionProcessor implements CompletionProvider { } private void computeProposalsForSimpleName(ASTNode node, List completions, int offset, - IDocument doc, FuzzyMap index) { + IDocument doc) { String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); int startOffset = node.getStartPosition(); @@ -106,20 +107,20 @@ public class ValueCompletionProcessor implements CompletionProvider { String proposalPrefix = "\""; String proposalPostfix = "\""; - List> matches = findMatches(prefix, index); + List> matches = findMatches(prefix, doc); for (Match match : matches) { DocumentEdits edits = new DocumentEdits(doc); edits.replace(startOffset, endOffset, proposalPrefix + "${" + match.data.getId() + "}" + proposalPostfix); - ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match.data.getId(), match.data.getName(), null); + ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match); completions.add(proposal); } } private void computeProposalsForStringLiteral(ASTNode node, List completions, int offset, - IDocument doc, FuzzyMap index) throws BadLocationException { + IDocument doc) throws BadLocationException { String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1)); int startOffset = offset - prefix.length(); @@ -141,14 +142,14 @@ public class ValueCompletionProcessor implements CompletionProvider { String fullNodeContent = doc.get(node.getStartPosition(), node.getLength()); String postCompletion = isClosingBracketMissing(fullNodeContent + preCompletion) ? "}" : ""; - List> matches = findMatches(prefix, index); + List> matches = findMatches(prefix, doc); for (Match match : matches) { DocumentEdits edits = new DocumentEdits(doc); edits.replace(startOffset, endOffset, preCompletion + match.data.getId() + postCompletion); - ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match.data.getId(), match.data.getName(), null); + ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match); completions.add(proposal); } } @@ -184,8 +185,23 @@ public class ValueCompletionProcessor implements CompletionProvider { return result; } - private List> findMatches(String prefix, FuzzyMap index) { - List> matches = index.find(camelCaseToHyphens(prefix)); + private List> findMatches(String prefix, IDocument doc) { + FuzzyMap index = indexProvider.getIndex(doc); + List> matches =index.find(camelCaseToHyphens(prefix)); + + //First the 'real' properties. + Set suggestedKeys = new HashSet<>(); + for (Match m : matches) { + suggestedKeys.add(m.data.getId()); + } + + //Then also add 'ad-hoc' properties (see https://www.pivotaltracker.com/story/show/153107266). + index = adHocIndexProvider.getIndex(doc); + for (Match m : index.find(prefix)) { + if (suggestedKeys.add(m.data.getId())) { + matches.add(m); + } + } return matches; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java index e07878d89..c7dfb8272 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java @@ -11,25 +11,36 @@ package org.springframework.ide.vscode.boot.java.value; import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.boot.common.InformationTemplates; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; 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.completion.ScoreableProposal; +import org.springframework.ide.vscode.commons.util.FuzzyMap.Match; import org.springframework.ide.vscode.commons.util.Renderable; +import org.springframework.ide.vscode.commons.util.Renderables; /** * @author Martin Lippert */ -public class ValuePropertyKeyProposal implements ICompletionProposal { +public class ValuePropertyKeyProposal extends ScoreableProposal { private DocumentEdits edits; private String label; private String detail; private Renderable documentation; + private double score; - public ValuePropertyKeyProposal(DocumentEdits edits, String label, String detail, Renderable documentation) { + private ValuePropertyKeyProposal(DocumentEdits edits, String label, String detail, double score, Renderable documentation) { this.edits = edits; this.label = label; this.detail = detail; this.documentation = documentation; + this.score = score; + } + + public ValuePropertyKeyProposal(DocumentEdits edits, Match match) { + this(edits, match.data.getId(), match.data.getType(), match.score, InformationTemplates.createCompletionDocumentation(match.data)); } @Override @@ -57,4 +68,9 @@ public class ValuePropertyKeyProposal implements ICompletionProposal { return this.documentation; } + @Override + public double getBaseScore() { + return score; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java new file mode 100644 index 000000000..b34bdb744 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java @@ -0,0 +1,173 @@ +package org.springframework.ide.vscode.boot.metadata; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ExecutionException; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.util.FileObserver; +import org.springframework.ide.vscode.commons.util.FuzzyMap; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; + +import com.google.common.base.Function; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; + +/******************************************************************************* + * Copyright (c) 2018 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 + *******************************************************************************/ +public class AdHocSpringPropertyIndexProvider implements SpringPropertyIndexProvider { + + private static final Logger log = LoggerFactory.getLogger(AdHocSpringPropertyIndexProvider.class); + + private static class SimplePropertyIndex extends FuzzyMap { + @Override + protected String getKey(PropertyInfo entry) { + return entry.getId(); + } + } + + private Cache indexes; + final private JavaProjectFinder projectFinder; + + public AdHocSpringPropertyIndexProvider(JavaProjectFinder projectFinder, ProjectObserver projectObserver, FileObserver fileObserver) { + this.projectFinder = projectFinder; + this.indexes = CacheBuilder.newBuilder().build(); + if (projectObserver != null) { + projectObserver.addListener(ProjectObserver.onAny(project -> indexes.invalidate(project))); + } + if (fileObserver!=null) { + fileObserver.onAnyChange(ImmutableList.of( + "**/application.properties", + "**/application.yml" + ), changed -> { + log.info("File changed: "+changed); + projectFinder.find(new TextDocumentIdentifier(changed)).ifPresent(project -> { + log.info("=> Project changed: "+project.getElementName()); + indexes.invalidate(project); + }); + }); + } + } + + + @Override + public FuzzyMap getIndex(IDocument doc) { + Optional jp = projectFinder.find(new TextDocumentIdentifier(doc.getUri())); + if (jp.isPresent()) { + return getIndex(jp.get()); + } + return SpringPropertyIndex.EMPTY_INDEX; + } + + + private FuzzyMap getIndex(IJavaProject jp) { + try { + return indexes.get(jp, () -> { + SimplePropertyIndex index = new SimplePropertyIndex(); + IClasspathUtil.getSourceFolders(jp.getClasspath()).forEach(sourceFolder -> { + processFile(this::parseProperties, new File(sourceFolder, "application.properties"), index); + processFile(this::parseYaml, new File(sourceFolder, "application.yml"), index); + }); + return index; + }); + } catch (ExecutionException e) { + log.error("", e); + } + return null; + } + + private void processFile(Function parserFunction, File file, SimplePropertyIndex index) { + Properties props = parserFunction.apply(file); + if (props!=null) { + for (Object p : props.keySet()) { + if (p instanceof String) { + index.add(new PropertyInfo((String)p)); + } + } + } + } + + private Properties parseProperties(File propsFile) { + try { + if (propsFile.isFile()) { + Properties props = new Properties(); + try (InputStream reader = new FileInputStream(propsFile)) { + props.load(reader); + } + return props; + } + } catch (Exception e) { + //ignore failed attempt to read bad file + } + return null; + } + + private Properties parseYaml(File yamlFile) { + if (yamlFile.isFile()) { + Yaml yaml = new Yaml(); + try (Reader reader = new InputStreamReader(new FileInputStream(yamlFile), "UTF8")) { + Properties props = new Properties(); + for (Node node : yaml.composeAll(reader)) { + flattenProperties("", node, props); + } + return props; + } catch (Exception e ) { + //ignore failed attempt to read bad file + } + } + return null; + } + + + private void flattenProperties(String prefix, Node node, Properties props) { + switch (node.getNodeId()) { + case mapping: + if (!prefix.isEmpty()) { + prefix = prefix +"."; + } + MappingNode mapping = (MappingNode)node; + for (NodeTuple tup : mapping.getValue()) { + String key = NodeUtil.asScalar(tup.getKeyNode()); + if (key!=null) { + flattenProperties(prefix+key, tup.getValueNode(), props); + } + } + break; + case scalar: + //End of the line. + props.put(prefix, NodeUtil.asScalar(node)); + break; + default: + //Ignore other cases, might implement later if it makes sense. + break; + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/DefaultSpringPropertyIndexProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/DefaultSpringPropertyIndexProvider.java index 2f1dea806..279f262b5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/DefaultSpringPropertyIndexProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/DefaultSpringPropertyIndexProvider.java @@ -23,8 +23,6 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; public class DefaultSpringPropertyIndexProvider implements SpringPropertyIndexProvider { - private static final FuzzyMap EMPTY_INDEX = new SpringPropertyIndex(null, null); - private JavaProjectFinder javaProjectFinder; private SpringPropertiesIndexManager indexManager; @@ -41,7 +39,7 @@ public class DefaultSpringPropertyIndexProvider implements SpringPropertyIndexPr if (jp.isPresent()) { return indexManager.get(jp.get(), progressService); } - return EMPTY_INDEX; + return SpringPropertyIndex.EMPTY_INDEX; } public void setProgressService(ProgressService progressService) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertyInfo.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertyInfo.java index 3a49aafc7..5fe63f516 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertyInfo.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertyInfo.java @@ -117,6 +117,9 @@ public class PropertyInfo { } } } + public PropertyInfo(String p) { + this(p, null, null, null, null, null, null, null, null, null, null); + } private void handleAs(Object targetObject) { // debug("handle-as "+this.getId()+" -> "+targetObject); if (targetObject instanceof String) { @@ -165,7 +168,7 @@ public class PropertyInfo { private HintProvider valueHints(TypeUtil typeUtil) { return HintProviders.basic(typeUtil.getJavaProject(), valueHints, valueProvider); } - + public List getSources() { if (sources!=null) { return sources; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java index c1614e2b8..4725e0195 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java @@ -11,16 +11,19 @@ package org.springframework.ide.vscode.boot.metadata; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import org.springframework.ide.vscode.boot.metadata.util.Listener; import org.springframework.ide.vscode.boot.metadata.util.ListenerManager; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.ProgressService; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.util.FileObserver; 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; /** * Support for Reconciling, Content Assist and Hover Text in spring properties @@ -38,28 +41,11 @@ public class SpringPropertiesIndexManager extends ListenerManager indexes.invalidate(project))); } - } public synchronized SpringPropertyIndex get(IJavaProject project, ProgressService progressService) { @@ -70,7 +56,7 @@ public class SpringPropertiesIndexManager extends ListenerManager { - + + public static final FuzzyMap EMPTY_INDEX = new SpringPropertyIndex(null, null); + private ValueProviderRegistry valueProviders; public SpringPropertyIndex(ValueProviderRegistry valueProviders, IClasspath projectPath) { @@ -126,7 +128,7 @@ public class SpringPropertyIndex extends FuzzyMap { protected String getKey(PropertyInfo entry) { return entry.getId(); } - + /** * Find the longest known property that is a prefix of the given name. Here prefix does not mean * 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/AdHocSpringPropertyIndexProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/AdHocSpringPropertyIndexProviderTest.java new file mode 100644 index 000000000..202ad8ff9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/AdHocSpringPropertyIndexProviderTest.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2018 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.boot.java.value.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.value.test.MockProjects.MockProject; +import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.commons.util.FuzzyMap; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCustomizer; + +public class AdHocSpringPropertyIndexProviderTest { + + private MockProjects projects = new MockProjects(); + + private static final ProjectCustomizer WITH_PROPERTIES = (projectContent) -> { + projectContent.createFile("src/main/resources/application.properties", + "some-adhoc-foo=somefoo\n" + + "some-adhoc-bar=somebar\n" + ); + }; + + @Test + public void parseProperties() throws Exception { + MockProject project = projects.create("test-project"); + project.ensureFile("src/main/resources/application.properties", + "some-adhoc-foo=somefoo\n" + + "some-adhoc-bar=somebar\n" + ); + AdHocSpringPropertyIndexProvider indexer = new AdHocSpringPropertyIndexProvider(projects.finder, projects.observer, null); + + TextDocument doc = new TextDocument(project.uri("src/main/java/SomeClass.java"), LanguageId.JAVA); + assertProperties(indexer.getIndex(doc), + //alphabetic order + "some-adhoc-bar", + "some-adhoc-foo" + ); + } + + @Test + public void parseYaml() throws Exception { + MockProject project = projects.create("test-project"); + project.ensureFile("src/main/resources/application.yml", + "from-yaml:\n" + + " adhoc:\n" + + " foo: somefoo\n" + + " bar: somebar\n" + ); + AdHocSpringPropertyIndexProvider indexer = new AdHocSpringPropertyIndexProvider(projects.finder, projects.observer, null); + + TextDocument doc = new TextDocument(project.uri("src/main/java/SomeClass.java"), LanguageId.JAVA); + assertProperties(indexer.getIndex(doc), + //alphabetic order + "from-yaml.adhoc.bar", + "from-yaml.adhoc.foo" + ); + } + + @Test + public void respondsToClasspathChanges() throws Exception { + MockProject project = projects.create("test-project"); + project.ensureFile("src/main/resources/application.properties", + "initial-property=somefoo\n" + ); + + AdHocSpringPropertyIndexProvider indexer = new AdHocSpringPropertyIndexProvider(projects.finder, projects.observer, null); + TextDocument doc = new TextDocument(project.uri("src/main/java/SomeClass.java"), LanguageId.JAVA); + + assertProperties(indexer.getIndex(doc), + "initial-property" + ); + + project.ensureFile("new-sourcefolder/application.properties", "new-property=whatever"); + assertProperties(indexer.getIndex(doc), + "initial-property" + ); + + project.createSourceFolder("new-sourcefolder"); + assertProperties(indexer.getIndex(doc), + "initial-property", + "new-property" + ); + } + + @Test + public void respondsToFileChanges() throws Exception { + MockProject project = projects.create("test-project"); + project.ensureFile("src/main/resources/application.properties", + "initial-property=somefoo\n" + ); + + AdHocSpringPropertyIndexProvider indexer = new AdHocSpringPropertyIndexProvider(projects.finder, projects.observer, projects.fileObserver); + TextDocument doc = new TextDocument(project.uri("src/main/java/SomeClass.java"), LanguageId.JAVA); + + assertProperties(indexer.getIndex(doc), + "initial-property" + ); + + project.ensureFile("src/main/resources/application.properties", "from-properties=whatever"); + assertProperties(indexer.getIndex(doc), + "from-properties" + ); + + project.ensureFile("src/main/resources/application.yml", "from-yaml: whatever"); + assertProperties(indexer.getIndex(doc), + "from-properties", + "from-yaml" + ); + } + + private void assertProperties(FuzzyMap index, String... expectedProps) { + StringBuilder foundProps = new StringBuilder(); + for (PropertyInfo p : index) { + foundProps.append(p.getId()+"\n"); + } + StringBuilder expecteds = new StringBuilder(); + for (String string : expectedProps) { + expecteds.append(string+"\n"); + } + assertEquals(expecteds.toString(), foundProps.toString()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/MockProjects.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/MockProjects.java new file mode 100644 index 000000000..1726a7555 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/MockProjects.java @@ -0,0 +1,279 @@ +/******************************************************************************* + * Copyright (c) 2018 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.boot.java.value.test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.Assert; +import org.springframework.ide.vscode.commons.java.ClasspathIndex; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; +import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath; +import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath.CPE; +import org.springframework.ide.vscode.commons.util.FileObserver; +import org.springframework.ide.vscode.commons.util.IOUtil; + +import com.google.common.io.Files; + +public class MockProjects { + + private final Map projectsByName = new HashMap(); + + public final MockProjectObserver observer = new MockProjectObserver(); + public final JavaProjectFinder finder = new JavaProjectFinder() { + @Override + public Optional find(TextDocumentIdentifier doc) { + synchronized (projectsByName) { + try { + File file = new File(new URI(doc.getUri())); + for (MockProject project : projectsByName.values()) { + if (project.contains(file)) { + return Optional.of(project); + } + } + } catch (Exception e) { + } + + return Optional.empty(); + } + } + }; + + public MockFileObserver fileObserver = new MockFileObserver(); + + public class MockProject implements IJavaProject { + + final private File root; + final private String name; + final private List sourceFolders = new ArrayList(); + + final private IClasspath classpath = new IClasspath() { + + @Override + public String getName() { + return name; + } + + @Override + public Collection getClasspathEntries() throws Exception { + List cp = new ArrayList<>(); + for (File sf : sourceFolders) { + cp.add(new CPE(Classpath.ENTRY_KIND_SOURCE, sf.getAbsolutePath())); + } + return cp; + } + }; + + public MockProject(String name) { + synchronized (projectsByName) { + Assert.assertFalse(projectsByName.containsKey(name)); + this.name = name; + this.root = Files.createTempDir(); + createSourceFolder("src/main/java"); + createSourceFolder("src/main/resources"); + projectsByName.put(name, this); + } + synchronized (observer.listeners) { + for (Listener l : observer.listeners) { + l.created(this); + } + } + } + + public boolean contains(File file) { + return file.toPath().startsWith(root.toPath()); + } + + public void createSourceFolder(String projectRelativePath) { + File sourceFolder = new File(root, projectRelativePath); + sourceFolder.mkdirs(); + sourceFolders.add(sourceFolder); + synchronized (observer.listeners) { + for (Listener l : observer.listeners) { + l.changed(this); + } + } + } + + @Override + public IClasspath getClasspath() { + return classpath; + } + + @Override + public ClasspathIndex getIndex() { + //TODO: the fact we have to implement this probably means something is a bit off with the + // framework api, because this info should not really depend on anything but a project's classpath. + // So why should every type of project need to implement its own mechanic for indexing classpath? + throw new IllegalStateException("Not implemented"); + } + + @Override + public URI getLocationUri() { + return root.toURI(); + } + + @Override + public boolean exists() { + return root.isDirectory(); + } + + public void ensureFile(String projectRelativePath, String contents) throws Exception { + File target = new File(root, projectRelativePath); + boolean existed = target.exists(); + IOUtil.pipe(new ByteArrayInputStream(contents.getBytes("UTF8")), target); + if (existed) { + fileObserver.fileChanged(target); + } else { + fileObserver.fileCreated(target); + } + } + + public String uri(String projectRelativePath) { + return new File(root, projectRelativePath).toURI().toString(); + } + } + + public class MockProjectObserver implements ProjectObserver { + + public final LinkedHashSet listeners = new LinkedHashSet<>(); + + @Override + public void addListener(Listener l) { + listeners.add(l); + } + + @Override + public void removeListener(Listener l) { + listeners.remove(l); + } + + } + + private static class FileListener { + final PathMatcher matcher; + final Consumer handler; + FileListener(List globPatterns, Consumer listener) { + super(); + this.matcher = buildPathMatcher(globPatterns); + this.handler = listener; + } + private PathMatcher buildPathMatcher(List globPatterns) { + if (globPatterns.size()==0) { + return path -> true; + } else if (globPatterns.size()==1) { + return FileSystems.getDefault().getPathMatcher("glob:"+globPatterns.get(0)); + } else { + PathMatcher[] matchers = new PathMatcher[globPatterns.size()]; + for (int i = 0; i < matchers.length; i++) { + matchers[i] = FileSystems.getDefault().getPathMatcher("glob:"+globPatterns.get(i)); + } + return (path) -> { + for (int i = 0; i < matchers.length; i++) { + if (matchers[i].matches(path)) { + return true; + } + } + return false; + }; + } + } + } + + public class MockFileObserver implements FileObserver { + + final AtomicLong idGen = new AtomicLong(); + + final Map create_listeners = new HashMap<>(); + final Map change_listeners = new HashMap<>(); + final Map delete_listeners = new HashMap<>(); + + private String add(Map listeners, List globPatterns, Consumer handler) { + String id = ""+idGen.incrementAndGet(); + synchronized (listeners) { + listeners.put(id, new FileListener(globPatterns, handler)); + } + return id; + } + + public void fileChanged(File target) { + notify(change_listeners, target); + } + + public void fileCreated(File target) { + notify(create_listeners, target); + } + + private void notify(Map listeners, File target) { + Path path = target.toPath(); + synchronized (listeners) { + for (FileListener l : listeners.values()) { + if (l.matcher.matches(path)) { + l.handler.accept(target.toURI().toString()); + } + } + } + } + + @Override + public String onFileCreated(List globPattern, Consumer handler) { + return add(create_listeners, globPattern, handler); + } + + + @Override + public String onFileChanged(List globPattern, Consumer handler) { + return add(change_listeners, globPattern, handler); + } + + @Override + public String onFileDeleted(List globPattern, Consumer handler) { + return add(delete_listeners, globPattern, handler); + } + + @Override + public boolean unsubscribe(String subscriptionId) { + return + remove(create_listeners, subscriptionId) || + remove(change_listeners, subscriptionId) || + remove(delete_listeners, subscriptionId); + } + + private boolean remove(Map listeners, String subscriptionId) { + synchronized (listeners) { + return listeners.remove(subscriptionId) != null; + } + } + } + + public MockProject create(String name) { + return new MockProject(name); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java index 2bbce2989..6ec464658 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java @@ -62,7 +62,7 @@ public class ValueCompletionTest { @Test public void testPrefixIdentification() { - ValueCompletionProcessor processor = new ValueCompletionProcessor(null); + ValueCompletionProcessor processor = new ValueCompletionProcessor(null, null); assertEquals("pre", processor.identifyPropertyPrefix("pre", 3)); assertEquals("pre", processor.identifyPropertyPrefix("prefix", 3)); @@ -262,6 +262,52 @@ public class ValueCompletionTest { "@Value(\"#{345${spring.prop1<*>}}\")"); } + @Test + public void adHoc() throws Exception { + prepareDefaultIndexData(); + Editor editor = harness.newEditor(LanguageId.JAVA, + "package org.test;\n" + + "\n" + + "import org.springframework.beans.factory.annotation.Value;\n" + + "\n" + + "public class TestValueCompletion {\n" + + " \n" + + " @Value(\"<*>\")\n" + + " private String value1;\n" + + "}" + ); + + //There are no 'ad-hoc' properties yet. So should only suggest the default ones. + editor.assertContextualCompletions( + "<*>" + , //==> + "${data.prop2}<*>", + "${else.prop3}<*>", + "${spring.prop1}<*>" + ); + + indexHarness.adHoc("spring.ad-hoc.thingy"); + indexHarness.adHoc("spring.ad-hoc.other-thingy"); + indexHarness.adHoc("spring.prop1"); //should not suggest this twice! + editor.assertContextualCompletions( + "<*>" + , //==> + "${data.prop2}<*>", + "${else.prop3}<*>", + "${spring.ad-hoc.other-thingy}<*>", + "${spring.ad-hoc.thingy}<*>", + "${spring.prop1}<*>" + ); + + editor.assertContextualCompletions( + "adhoc<*>" + , //==> + "${spring.ad-hoc.other-thingy}<*>", + "${spring.ad-hoc.thingy}<*>" + ); + } + + private void prepareDefaultIndexData() { indexHarness.data("spring.prop1", "java.lang.String", null, null); indexHarness.data("data.prop2", "java.lang.String", null, null); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java index 93bd5b283..1f3ead792 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java @@ -33,6 +33,7 @@ import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.metadata.CachingValueProvider; import org.springframework.ide.vscode.boot.metadata.PropertiesLoader; +import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndex; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IType; import org.springframework.ide.vscode.commons.languageserver.composable.ComposableLanguageServer; @@ -1627,6 +1628,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { javaProjectFinder, ProjectObserver.NULL, md.getIndexProvider(), + (doc) -> SpringPropertyIndex.EMPTY_INDEX, typeUtilProvider, RunningAppProvider.NULL, SpringLiveHoverWatchdog.DEFAULT_INTERVAL diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java index 142faab77..80826454c 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java @@ -30,6 +30,7 @@ import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.metadata.CachingValueProvider; import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndex; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.composable.ComposableLanguageServer; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; @@ -3866,6 +3867,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { javaProjectFinder, ProjectObserver.NULL, md.getIndexProvider(), + (doc) -> SpringPropertyIndex.EMPTY_INDEX, typeUtilProvider, RunningAppProvider.NULL, SpringLiveHoverWatchdog.DEFAULT_INTERVAL diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootJavaLanguageServerHarness.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootJavaLanguageServerHarness.java index de2008157..6c7b565c6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootJavaLanguageServerHarness.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootJavaLanguageServerHarness.java @@ -54,6 +54,7 @@ public class BootJavaLanguageServerHarness extends LanguageServerHarness null, null), (uri, cpe) -> JavaDocProviders.createFor(cpe)); @@ -143,5 +143,5 @@ public class ProjectsHarness { public MavenJavaProject mavenProject(String name) throws Exception { return (MavenJavaProject) project(ProjectType.MAVEN, name); } - + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/PropertyIndexHarness.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/PropertyIndexHarness.java index b3b011417..62bfb1641 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/PropertyIndexHarness.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/PropertyIndexHarness.java @@ -36,6 +36,12 @@ public class PropertyIndexHarness { private Map datas = new LinkedHashMap<>(); private SpringPropertyIndex index = null; + private FuzzyMap adHocProperties = new FuzzyMap() { + @Override + protected String getKey(PropertyInfo entry) { + return entry.getId(); + } + }; private IJavaProject testProject = null; protected final SpringPropertyIndexProvider indexProvider = new SpringPropertyIndexProvider() { @@ -54,6 +60,8 @@ public class PropertyIndexHarness { } }; + protected final SpringPropertyIndexProvider adHocIndexProvider = doc -> adHocProperties; + public synchronized void useProject(IJavaProject p) throws Exception { index = null; this.testProject = p; @@ -563,8 +571,16 @@ public class PropertyIndexHarness { return indexProvider; } + public SpringPropertyIndexProvider getAdHocIndexProvider() { + return adHocIndexProvider; + } + public JavaProjectFinder getProjectFinder() { return (doc) -> Optional.ofNullable(testProject); } + public void adHoc(String adHocPropertyId) { + adHocProperties.add(new PropertyInfo(adHocPropertyId, null, null, null, null, null, null, null, null, null, null)); + } + } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.properties index 02bef719e..e69de29bb 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.properties +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.properties @@ -1,3 +0,0 @@ -logging.pattern.level= - - diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.yml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.yml deleted file mode 100644 index ac4763779..000000000 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/empty-boot-1.3.0-app/src/main/resources/application.yml +++ /dev/null @@ -1,4 +0,0 @@ -logging: - level: - auto - \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/pom.xml index 7fe1c3c40..31b08ffce 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.example - test-scope-annotation + test-annotation 0.0.1-SNAPSHOT jar