From 41378193827e3b580bb4fd49e58a1e5b9a3faf47 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 7 Nov 2018 16:51:38 -0500 Subject: [PATCH] PT #155474396: Ctrl-Click for Boot properties YAML --- .../languageserver/util/LanguageSpecific.java | 21 ++ .../yaml/completion/YamlAssistContext.java | 12 + .../starter/LanguageServerAutoConf.java | 38 ++- .../boot/app/BootLanguagServerBootApp.java | 36 +++ .../app/BootLanguageServerInitializer.java | 10 +- .../boot/app/BootLanguageServerParams.java | 1 - .../app/PropertiesJavaDefinitionHandler.java | 39 ++- .../YamlPropertiesJavaDefinitionHandler.java | 131 ++++++++ .../common/PropertyCompletionFactory.java | 22 +- .../vscode/boot/metadata/types/TypeUtil.java | 6 +- ...ootPropertiesLanguageServerComponents.java | 48 +-- .../SpringPropertiesCompletionEngine.java | 2 +- .../hover/PropertiesDefinitionCalculator.java | 214 ++++++------- .../boot/properties/hover/PropertyFinder.java | 10 +- .../ApplicationYamlAssistContext.java | 87 +++++- .../boot/bootiful/PropertyEditorTestConf.java | 16 + .../test/ApplicationPropertiesEditorTest.java | 294 +----------------- .../boot/test/ApplicationYamlEditorTest.java | 89 ++++-- .../boot/test/DefinitionLinkAsserts.java | 293 +++++++++++++++++ 19 files changed, 880 insertions(+), 489 deletions(-) create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/LanguageSpecific.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/YamlPropertiesJavaDefinitionHandler.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/DefinitionLinkAsserts.java diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/LanguageSpecific.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/LanguageSpecific.java new file mode 100644 index 000000000..39f9dd7dc --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/LanguageSpecific.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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.commons.languageserver.util; + +import java.util.Collection; + +import org.springframework.ide.vscode.commons.util.text.LanguageId; + +public interface LanguageSpecific { + + Collection supportedLanguages(); + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java index f0f194893..bbf275cf6 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java @@ -11,7 +11,9 @@ package org.springframework.ide.vscode.commons.yaml.completion; import java.util.Collection; +import java.util.List; +import org.eclipse.lsp4j.Location; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.DocumentRegion; @@ -20,6 +22,8 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode; +import com.google.common.collect.ImmutableList; + /** * @author Kris De Volder */ @@ -34,4 +38,12 @@ public interface YamlAssistContext extends YamlNavigable { Renderable getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion); YamlDocument getDocument(); + + default List getDefinitionsForPropertyKey() { + return ImmutableList.of(); + } + + default List getDefinitionsForPropertyValue(DocumentRegion valueRegion) { + return ImmutableList.of(); + } } diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java index 06cf28bef..ac3b640a4 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java @@ -10,6 +10,9 @@ *******************************************************************************/ package org.springframework.ide.vscode.languageserver.starter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.beans.factory.InitializingBean; @@ -22,8 +25,15 @@ import org.springframework.ide.vscode.commons.languageserver.config.LanguageServ import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; import org.springframework.ide.vscode.commons.languageserver.reconcile.DiagnosticSeverityProvider; import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.util.Assert; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; @Configuration @EnableConfigurationProperties(LanguageServerProperties.class) @@ -54,7 +64,31 @@ public class LanguageServerAutoConf { @ConditionalOnBean(DefinitionHandler.class) @Bean InitializingBean registerDefintionHandler(SimpleTextDocumentService documents, - DefinitionHandler definitionHandler) { - return () -> documents.onDefinition(definitionHandler); + List definitionHandlers) { + if (definitionHandlers.size() == 1) { + return () -> documents.onDefinition(definitionHandlers.get(0)); + } else { + Map handlers = new HashMap<>(definitionHandlers.size()); + for (DefinitionHandler h : definitionHandlers) { + Assert.isInstanceOf(LanguageSpecific.class, h, "Only language specific defintion handlers supported!"); + for (LanguageId l : ((LanguageSpecific)h).supportedLanguages()) { + Assert.isTrue(!handlers.containsKey(l), "Multiple definition handlers for the same language not supported!"); + handlers.put(l, h); + } + } + ImmutableMap immutableMap = ImmutableMap.copyOf(handlers); + return () -> documents.onDefinition((position) -> { + TextDocument doc = documents.get(position.getTextDocument().getUri()); + if (doc != null) { + LanguageId language = doc.getLanguageId(); + DefinitionHandler handler = immutableMap.get(language); + if (handler != null) { + return handler.handle(position); + } + } + return ImmutableList.of(); + }); + } } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java index d8d7d7edc..5e30a7b38 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java @@ -14,6 +14,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.context.annotation.Bean; +import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; +import org.springframework.ide.vscode.boot.common.RelaxedNameConfig; import org.springframework.ide.vscode.boot.java.links.DefaultJavaElementLocationProvider; import org.springframework.ide.vscode.boot.java.links.EclipseJavaDocumentUriProvider; import org.springframework.ide.vscode.boot.java.links.EclipseJavaElementLocationProvider; @@ -23,10 +25,21 @@ import org.springframework.ide.vscode.boot.java.links.JdtJavaDocumentUriProvider import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.yaml.completions.ApplicationYamlAssistContext; import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.FuzzyMap; import org.springframework.ide.vscode.commons.util.LogRedirect; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; +import org.springframework.ide.vscode.commons.yaml.ast.YamlParser; +import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContext; +import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContextProvider; +import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; +import org.yaml.snakeyaml.Yaml; @SpringBootApplication public class BootLanguagServerBootApp { @@ -75,4 +88,27 @@ public class BootLanguagServerBootApp { } } + @Bean Yaml yaml() { + return new Yaml(); + } + + @Bean YamlASTProvider yamlAstProvider(Yaml yaml) { + return new YamlParser(yaml); + } + + @Bean YamlStructureProvider yamlStructureProvider() { + return YamlStructureProvider.DEFAULT; + } + + @Bean YamlAssistContextProvider yamlAssistContextProvider(BootLanguageServerParams params, JavaElementLocationProvider javaElementLocationProvider) { + return new YamlAssistContextProvider() { + @Override + public YamlAssistContext getGlobalAssistContext(YamlDocument ydoc) { + IDocument doc = ydoc.getDocument(); + FuzzyMap index = params.indexProvider.getIndex(doc); + return ApplicationYamlAssistContext.global(ydoc, index, new PropertyCompletionFactory(), params.typeUtilProvider.getTypeUtil(doc), RelaxedNameConfig.COMPLETION_DEFAULTS, javaElementLocationProvider); + } + }; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index 017b06f8c..cefb3fa90 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; @@ -26,6 +27,9 @@ import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; +import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContextProvider; +import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -36,6 +40,10 @@ public class BootLanguageServerInitializer implements InitializingBean { @Autowired BootLanguageServerParams params; @Autowired SourceLinks sourceLinks; @Autowired CompilationUnitCache cuCache; + @Autowired JavaElementLocationProvider javaElementLocationProvider; + @Autowired YamlASTProvider parser; + @Autowired YamlStructureProvider yamlStructureProvider; + @Autowired YamlAssistContextProvider yamlAssistContextProvider; private CompositeLanguageServerComponents components; private VscodeCompletionEngineAdapter completionEngineAdapter; @@ -58,7 +66,7 @@ public class BootLanguageServerInitializer implements InitializingBean { //TODO: ComposableLanguageServer object instance serves no purpose anymore. The constructor really just contains // some server intialization code. Migrate that code and get rid of the ComposableLanguageServer class CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder(); - builder.add(new BootPropertiesLanguageServerComponents(server, (ignore) -> params)); + builder.add(new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider)); builder.add(new BootJavaLanguageServerComponents(server, params, sourceLinks, cuCache)); components = builder.build(server); params.projectObserver.addListener(reconcileOpenDocuments(server, components)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java index 141d55428..3eaf4454c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java @@ -40,7 +40,6 @@ import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFin import org.springframework.ide.vscode.commons.languageserver.java.JavadocService; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath.CPE; -import org.springframework.ide.vscode.commons.languageserver.util.LSFactory; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.maven.MavenCore; import org.springframework.ide.vscode.commons.maven.java.MavenProjectCache; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java index 209422ad0..47b923fb2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.app; +import java.util.Collection; import java.util.List; import org.eclipse.lsp4j.Location; @@ -18,23 +19,32 @@ import org.gradle.internal.impldep.com.google.common.collect.ImmutableList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.metadata.types.Type; import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; import org.springframework.ide.vscode.boot.properties.hover.PropertiesDefinitionCalculator; +import org.springframework.ide.vscode.boot.properties.hover.PropertyFinder; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.IMember; import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.BadLocationException; 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.java.properties.parser.PropertiesAst.Key; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value; import org.springframework.stereotype.Component; @Component -public class PropertiesJavaDefinitionHandler implements DefinitionHandler { +public class PropertiesJavaDefinitionHandler implements DefinitionHandler, LanguageSpecific { @Autowired private SimpleTextDocumentService documents; @Autowired - private JavaElementLocationProvider javaDocumentLocationProvider; + private JavaElementLocationProvider javaElementLocationProvider; @Autowired private BootLanguageServerParams params; @@ -47,10 +57,33 @@ public class PropertiesJavaDefinitionHandler implements DefinitionHandler { FuzzyMap index = params.indexProvider.getIndex(doc); int offset; offset = doc.toOffset(position.getPosition()); - return new PropertiesDefinitionCalculator(javaDocumentLocationProvider, index, typeUtil, doc, offset).calculate(); + return getDefinitions(index, typeUtil, doc, offset); } catch (BadLocationException e) { return ImmutableList.of(); } } + private List getDefinitions(FuzzyMap index, TypeUtil typeUtil, TextDocument doc, int offset) { + IJavaProject project = typeUtil.getJavaProject(); + PropertyFinder propertyFinder = new PropertyFinder(index, typeUtil, doc, offset); + Node node = propertyFinder.findNode(); + if (node instanceof Key) { + Collection propertyJavaElements = PropertiesDefinitionCalculator.getPropertyJavaElements(propertyFinder, project, ((Key) node).decode()); + return PropertiesDefinitionCalculator.getLocations(javaElementLocationProvider, project, propertyJavaElements); + } else if (node instanceof Value) { + Value value = (Value) node; + Key key = value.getParent().getKey(); + Type type = PropertiesDefinitionCalculator.getPropertyType(propertyFinder, key.decode()); + if (type != null) { + return PropertiesDefinitionCalculator.getValueDefinitionLocations(javaElementLocationProvider, typeUtil, type, value.decode()); + } + } + return ImmutableList.of(); + } + + @Override + public Collection supportedLanguages() { + return ImmutableList.of(LanguageId.BOOT_PROPERTIES); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/YamlPropertiesJavaDefinitionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/YamlPropertiesJavaDefinitionHandler.java new file mode 100644 index 000000000..a151eac54 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/YamlPropertiesJavaDefinitionHandler.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * 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.app; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.text.DocumentRegion; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.commons.yaml.ast.NodeRef; +import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; +import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; +import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContext; +import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContextProvider; +import org.springframework.ide.vscode.commons.yaml.path.YamlPath; +import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; +import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; +import org.springframework.stereotype.Component; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.parser.ParserException; +import org.yaml.snakeyaml.scanner.ScannerException; + +import com.google.common.collect.ImmutableList; + +@Component +public class YamlPropertiesJavaDefinitionHandler implements DefinitionHandler, LanguageSpecific { + + private static final Logger log = LoggerFactory.getLogger(YamlPropertiesJavaDefinitionHandler.class); + + @Autowired + private SimpleTextDocumentService documents; + + @Autowired + private YamlASTProvider astProvider; + + @Autowired + private YamlStructureProvider structureProvider; + + @Autowired + private YamlAssistContextProvider assistContextProvider; + + @Override + public Collection supportedLanguages() { + return ImmutableList.of(LanguageId.BOOT_PROPERTIES_YAML); + } + + @Override + public List handle(TextDocumentPositionParams position) { + try { + TextDocument doc = documents.get(position); + int offset = doc.toOffset(position.getPosition()); + YamlFileAST ast = getAst(doc); + if (ast != null) { + YamlDocument ymlDoc = new YamlDocument(doc, structureProvider); + YamlAssistContext assistContext = assistContextProvider.getGlobalAssistContext(ymlDoc); + if (assistContext != null) { + List> astPath = ast.findPath(offset); + final YamlPath path = YamlPath.fromASTPath(astPath); + if (path != null) { + YamlPath assistPath = path; + if (assistPath.pointsAtKey()) { + // When a path points at a key we must tramsform it to a + // 'value-terminating path' + // to be able to reuse the 'getHoverInfo' method on + // YamlAssistContext (as navigation + // into 'key' is not defined for YamlAssistContext. + String key = path.getLastSegment().toPropString(); + assistPath = path.dropLast().append(YamlPathSegment.valueAt(key)); + } + assistContext = assistPath.traverse(assistContext); + + if (assistContext != null) { + if (path.pointsAtValue()) { + return assistContext.getDefinitionsForPropertyValue(getNodeRegion(ast, offset)); + } else { + return assistContext.getDefinitionsForPropertyKey(); + } + } + } + } + } + } catch (Exception e) { + log.error("", e); + } + return ImmutableList.of(); + } + + private DocumentRegion getNodeRegion(YamlFileAST ast, int offset) { + if (ast != null) { + Node n = ast.findNode(offset); + if (n != null && n.getNodeId() == NodeId.scalar) { + int start = n.getStartMark().getIndex(); + int end = n.getEndMark().getIndex(); + return new DocumentRegion(ast.getDocument(), start, end); + } + } + return null; + } + + + private YamlFileAST getAst(IDocument doc) throws Exception { + try { + return astProvider.getAST(doc); + } catch (ParserException | ScannerException e) { + // ignore, the user just typed some crap + } + return null; + } + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/PropertyCompletionFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/PropertyCompletionFactory.java index 4557ee4c9..5e1ecc1bc 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/PropertyCompletionFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/PropertyCompletionFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2014-2017 Pivotal, Inc. + * Copyright (c) 2014, 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 @@ -19,7 +19,6 @@ import org.springframework.ide.vscode.boot.metadata.types.TypedProperty; 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.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.util.FuzzyMap.Match; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -27,25 +26,25 @@ import org.springframework.ide.vscode.commons.yaml.hover.YPropertyInfoTemplates; import org.springframework.ide.vscode.commons.yaml.schema.YType; public class PropertyCompletionFactory { - + public ICompletionProposal valueProposal(String value, String query, String niceTypeName, double score, DocumentEdits edits, Renderable info) { return new ScoreableProposal() { - + @Override public DocumentEdits getTextEdit() { return edits; } - + @Override public String getLabel() { return value; } - + @Override public CompletionItemKind getKind() { return CompletionItemKind.Value; } - + @Override public double getBaseScore() { return score; @@ -105,7 +104,7 @@ public class PropertyCompletionFactory { return getBaseDisplayString(); } - + }; if (property.isDeprecated()) { proposal.deprecate(); @@ -113,10 +112,7 @@ public class PropertyCompletionFactory { return proposal; } - private JavaProjectFinder documentContextFinder; - - public PropertyCompletionFactory(JavaProjectFinder documentContextFinder) { - this.documentContextFinder = documentContextFinder; + public PropertyCompletionFactory() { } private class PropertyProposal extends AbstractPropertyProposal { @@ -171,7 +167,7 @@ public class PropertyCompletionFactory { public Renderable getDocumentation() { return InformationTemplates.createCompletionDocumentation(match.data); } - + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java index 57b7bd094..30e32ca01 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016-2017 Pivotal, Inc. + * Copyright (c) 2016, 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 @@ -546,6 +546,10 @@ public class TypeUtil { } } + public Type getMapKeyType(Type mapType) { + return isMap(mapType) ? getKeyType(mapType) : null; + } + public boolean isAssignableType(Type type) { return ASSIGNABLE_TYPES.contains(type.getErasure()) || isEnum(type) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/BootPropertiesLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/BootPropertiesLanguageServerComponents.java index fc18f1943..b33072c19 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/BootPropertiesLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/BootPropertiesLanguageServerComponents.java @@ -14,15 +14,12 @@ import java.util.Optional; import java.util.Set; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; -import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; -import org.springframework.ide.vscode.boot.common.RelaxedNameConfig; -import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.types.TypeUtilProvider; import org.springframework.ide.vscode.boot.properties.completions.SpringPropertiesCompletionEngine; import org.springframework.ide.vscode.boot.properties.hover.PropertiesHoverInfoProvider; import org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertiesReconcileEngine; -import org.springframework.ide.vscode.boot.yaml.completions.ApplicationYamlAssistContext; import org.springframework.ide.vscode.boot.yaml.reconcile.ApplicationYamlReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents; @@ -32,22 +29,16 @@ import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFin import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; -import org.springframework.ide.vscode.commons.languageserver.util.LSFactory; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; -import org.springframework.ide.vscode.commons.util.FuzzyMap; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; -import org.springframework.ide.vscode.commons.yaml.ast.YamlParser; -import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContext; import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContextProvider; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngine; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions; import org.springframework.ide.vscode.commons.yaml.hover.YamlHoverInfoProvider; -import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; -import org.yaml.snakeyaml.Yaml; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -62,10 +53,10 @@ public class BootPropertiesLanguageServerComponents implements LanguageServerCom private static final String YML = ".yml"; private static final String PROPERTIES = ".properties"; - + private static final Set LANGUAGES = ImmutableSet.of( - LanguageId.BOOT_PROPERTIES, - LanguageId.BOOT_PROPERTIES_YAML + LanguageId.BOOT_PROPERTIES, + LanguageId.BOOT_PROPERTIES_YAML ); private static final YamlCompletionEngineOptions COMPLETION_OPTIONS = new YamlCompletionEngineOptions() { @@ -77,35 +68,28 @@ public class BootPropertiesLanguageServerComponents implements LanguageServerCom private final JavaProjectFinder javaProjectFinder; private final SpringPropertyIndexProvider indexProvider; private final TypeUtilProvider typeUtilProvider; - private final RelaxedNameConfig relaxedNameConfig = RelaxedNameConfig.COMPLETION_DEFAULTS; - - private final PropertyCompletionFactory completionFactory; // For yaml - private final Yaml yaml = new Yaml(); - private final YamlASTProvider parser = new YamlParser(yaml); - private final YamlStructureProvider yamlStructureProvider= YamlStructureProvider.DEFAULT; + private final YamlStructureProvider yamlStructureProvider; private YamlAssistContextProvider yamlAssistContextProvider; private final SimpleLanguageServer server; + private YamlASTProvider parser; - public BootPropertiesLanguageServerComponents(SimpleLanguageServer server, LSFactory _params) { + public BootPropertiesLanguageServerComponents( + SimpleLanguageServer server, + BootLanguageServerParams serverParams, + JavaElementLocationProvider javaElementLocationProvider, + YamlASTProvider parser, + YamlStructureProvider yamlStructureProvider, + YamlAssistContextProvider yamlAssistContextProvider) { this.server = server; - BootLanguageServerParams serverParams = _params.create(server); - + this.parser = parser; this.indexProvider = serverParams.indexProvider; this.typeUtilProvider = serverParams.typeUtilProvider; this.javaProjectFinder = serverParams.projectFinder; this.projectObserver = serverParams.projectObserver; - - this.completionFactory = new PropertyCompletionFactory(javaProjectFinder); - this.yamlAssistContextProvider = new YamlAssistContextProvider() { - @Override - public YamlAssistContext getGlobalAssistContext(YamlDocument ydoc) { - IDocument doc = ydoc.getDocument(); - FuzzyMap index = indexProvider.getIndex(doc); - return ApplicationYamlAssistContext.global(ydoc, index, completionFactory, typeUtilProvider.getTypeUtil(doc), relaxedNameConfig); - } - }; + this.yamlStructureProvider = yamlStructureProvider; + this.yamlAssistContextProvider = yamlAssistContextProvider; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/completions/SpringPropertiesCompletionEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/completions/SpringPropertiesCompletionEngine.java index f778fced6..47bd33375 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/completions/SpringPropertiesCompletionEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/completions/SpringPropertiesCompletionEngine.java @@ -40,7 +40,7 @@ public class SpringPropertiesCompletionEngine implements ICompletionEngine { public SpringPropertiesCompletionEngine(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder projectFinder) { this.indexProvider = indexProvider; this.typeUtilProvider = typeUtilProvider; - this.completionFactory = new PropertyCompletionFactory(projectFinder); + this.completionFactory = new PropertyCompletionFactory(); } /** diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java index 6338d7cac..814c021ef 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java @@ -22,17 +22,15 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.metadata.PropertyInfo; import org.springframework.ide.vscode.boot.metadata.PropertyInfo.PropertySource; +import org.springframework.ide.vscode.boot.metadata.types.Type; +import org.springframework.ide.vscode.boot.metadata.types.TypeParser; import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; +import org.springframework.ide.vscode.commons.java.IField; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IMember; import org.springframework.ide.vscode.commons.java.IMethod; import org.springframework.ide.vscode.commons.java.IType; -import org.springframework.ide.vscode.commons.util.FuzzyMap; import org.springframework.ide.vscode.commons.util.StringUtil; -import org.springframework.ide.vscode.commons.util.text.IDocument; -import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key; -import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; -import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value; import com.google.common.collect.ImmutableList; @@ -40,117 +38,57 @@ public class PropertiesDefinitionCalculator { private static final Logger log = LoggerFactory.getLogger(PropertiesDefinitionCalculator.class); - final private PropertyFinder propertyFinder; - final private IJavaProject project; - final private JavaElementLocationProvider javaElementLocationProvider; - - public PropertiesDefinitionCalculator(JavaElementLocationProvider javaElementLocationProvider, FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { - this.javaElementLocationProvider = javaElementLocationProvider; - this.propertyFinder = new PropertyFinder(index, typeUtil, doc, offset); - this.project = typeUtil.getJavaProject(); + public static List getLocations(JavaElementLocationProvider locationProvider, IJavaProject project, + Collection propertyJavaElements) { + return propertyJavaElements.stream().map(member -> locationProvider.findLocation(project, member)) + .filter(Objects::nonNull).collect(Collectors.toList()); } - public List calculate() { - Node node = propertyFinder.findNode(); - if (node instanceof Key) { - Collection propertyJavaElements = getPropertyJavaElements(((Key) node).decode()); - return getPropertyDefintion(propertyJavaElements); - } else if (node instanceof Value) { - Value value = (Value) node; - Key key = value.getParent().getKey(); - - String javaTypeFqName = getPropertyJavaTypeFqName(key.decode()); - - if (javaTypeFqName != null) { - - // Class reference value link - if ("java.lang.Class".equals(javaTypeFqName)) { - IType javaType = project.findType(value.decode()); - if (javaType != null) { - Location location = findLocation(javaType); - if (location != null) { - return ImmutableList.of(location); - } - } - } - - IType javaType = project.findType(javaTypeFqName); - if (javaType != null) { - // Enum value link - if (javaType.isEnum()) { - String enumValue = StringUtil.hyphensToUpperCase(StringUtil.camelCaseToHyphens(value.decode())); - ImmutableList.Builder list = ImmutableList.builder(); - javaType.getFields().forEach(field -> { - if (field.getElementName().equals(enumValue)) { - Location location = findLocation(field); - if (location != null) { - list.add(location); - } - } - }); - return list.build(); - } - } - - } - - - } - return ImmutableList.of(); - } - - private List getPropertyDefintion(Collection propertyJavaElements) { - return propertyJavaElements.stream().map(this::findLocation).filter(Objects::nonNull).collect(Collectors.toList()); - } - - private Location findLocation(IMember element) { - return javaElementLocationProvider.findLocation(project, element); - } - - private String getPropertyJavaTypeFqName(String propertyKey) { + public static Type getPropertyType(PropertyFinder propertyFinder, String propertyKey) { PropertyInfo best = propertyFinder.findBestHoverMatch(propertyKey); if (best != null) { - String type = best.getType(); - // Trim down generic type if present - int idx = type == null ? -1 : type.indexOf('<'); - return idx < 0 ? type : type.substring(0, idx); + return TypeParser.parse(best.getType()); } return null; } - private Collection getPropertyJavaElements(String propertyKey) { + public static Collection getPropertyJavaElements(PropertyFinder propertyFinder, IJavaProject project, String propertyKey) { PropertyInfo best = propertyFinder.findBestHoverMatch(propertyKey); if (best != null) { - List sources = best.getSources(); - if (sources != null) { - ImmutableList.Builder elements = ImmutableList.builder(); - for (PropertySource source : sources) { - String typeName = source.getSourceType(); - if (typeName!=null) { - IType type = project.findType(typeName); - IMethod method = null; - if (type!=null) { - String methodSig = source.getSourceMethod(); - if (methodSig!=null) { - method = getMethod(type, methodSig); - } else { - method = getSetter(type, best); - } - } - if (method!=null) { - elements.add(method); - } else if (type!=null) { - elements.add(type); - } - } - } - return elements.build(); - } + return getPropertyJavaElement(project, best); } return ImmutableList.of(); } - private IMethod getMethod(IType type, String methodSig) { + public static Collection getPropertyJavaElement(IJavaProject project, PropertyInfo property) { + List sources = property.getSources(); + ImmutableList.Builder elements = ImmutableList.builder(); + if (sources != null) { + for (PropertySource source : sources) { + String typeName = source.getSourceType(); + if (typeName!=null) { + IType type = project.findType(typeName); + IMethod method = null; + if (type!=null) { + String methodSig = source.getSourceMethod(); + if (methodSig!=null) { + method = getMethod(type, methodSig); + } else { + method = getPropertyMethod(type, property.getName()); + } + } + if (method!=null) { + elements.add(method); + } else if (type!=null) { + elements.add(type); + } + } + } + } + return elements.build(); + } + + private static IMethod getMethod(IType type, String methodSig) { String name = getMethodName(methodSig); //TODO: This code assumes 0 arguments, which is the case currently for all // 'real' data in spring jars. @@ -184,10 +122,9 @@ public class PropertiesDefinitionCalculator { * Attempt to find corresponding setter method for a given property. * @return setter method, or null if not found. */ - private IMethod getSetter(IType type, PropertyInfo propertyInfo) { + private static IMethod getAccessor(IType type, String getOrSet, String propName) { try { - String propName = propertyInfo.getName(); - String setterName = "set" + String setterName = getOrSet +Character.toUpperCase(propName.charAt(0)) +toCamelCase(propName.substring(1)); String sloppySetterName = setterName.toLowerCase(); @@ -209,11 +146,22 @@ public class PropertiesDefinitionCalculator { } } + public static IMethod getPropertyMethod(IType type, String propName) { + String[] accessors = { "set", "get", "is" }; + for (String a : accessors) { + IMethod propertyMethod = getAccessor(type, a, propName); + if (propertyMethod != null) { + return propertyMethod; + } + } + return null; + } + /** * Convert hyphened name to camel case name. It is * safe to call this on an already camel-cased name. */ - private String toCamelCase(String name) { + private static String toCamelCase(String name) { if (name.isEmpty()) { return name; } else { @@ -234,4 +182,58 @@ public class PropertiesDefinitionCalculator { } } + public static IField getEnumField(IType type, String value) { + String[] enumValues = { + value, + StringUtil.hyphensToUpperCase(value), + StringUtil.hyphensToUpperCase(StringUtil.camelCaseToHyphens(value)) + }; + for (String enumValue : enumValues) { + IField field = type.getField(enumValue); + if (field != null) { + return field; + } + } + return null; + } + + private static List getEnumValueDefinitionLocation(JavaElementLocationProvider javaElementLocationProvider, IJavaProject project, Type type, String value) { + IType javaType = project.findType(type.getErasure()); + if (javaType != null) { + IField field = getEnumField(javaType, value); + if (field != null) { + Location location = javaElementLocationProvider.findLocation(project, field); + if (location != null) { + return ImmutableList.of(location); + } + } + } + return ImmutableList.of(); + } + + private static List getClassValueDefinitionLocation(JavaElementLocationProvider javaElementLocationProvider, IJavaProject project, String value) { + IType javaType = project.findType(value); + if (javaType != null) { + Location location = javaElementLocationProvider.findLocation(project, javaType); + if (location != null) { + return ImmutableList.of(location); + } + } + return ImmutableList.of(); + } + + public static List getValueDefinitionLocations(JavaElementLocationProvider javaElementLocationProvider, TypeUtil typeUtil, Type type, String value) { + IJavaProject project = typeUtil.getJavaProject(); + + if (TypeUtil.isClass(type)) { + return getClassValueDefinitionLocation(javaElementLocationProvider, project, value); + } + + if (typeUtil.isEnum(type)) { + return getEnumValueDefinitionLocation(javaElementLocationProvider, project, type, value); + } + + return ImmutableList.of(); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertyFinder.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertyFinder.java index 79708e519..121dae451 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertyFinder.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertyFinder.java @@ -21,7 +21,7 @@ import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser; import org.springframework.ide.vscode.java.properties.parser.ParseResults; import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; -class PropertyFinder { +public class PropertyFinder { final FuzzyMap index; final TypeUtil typeUtil; @@ -29,7 +29,7 @@ class PropertyFinder { final int offset; final AntlrParser parser; - PropertyFinder(FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { + public PropertyFinder(FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { this.index = index; this.typeUtil = typeUtil; this.doc = doc; @@ -37,12 +37,12 @@ class PropertyFinder { this.parser = new AntlrParser(); } - Node findNode() { + public Node findNode() { ParseResults parseResults = parser.parse(doc.get()); return parseResults.ast.findNode(offset); } - DocumentRegion createRegion(Node value) { + public DocumentRegion createRegion(Node value) { // Trim trailing spaces (there is no leading white space already) int length = value.getLength(); try { @@ -56,7 +56,7 @@ class PropertyFinder { /** * Search known properties for the best 'match' to show as hover data. */ - PropertyInfo findBestHoverMatch(String propName) { + public PropertyInfo findBestHoverMatch(String propName) { PropertyInfo propertyInfo = index.get(propName); if (propertyInfo == null) { propertyInfo = SpringPropertyIndex.findLongestValidProperty(index, propName); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java index 30c7c9045..06b732904 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.lsp4j.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.common.InformationTemplates; @@ -27,6 +28,7 @@ import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; import org.springframework.ide.vscode.boot.common.RelaxedNameConfig; import org.springframework.ide.vscode.boot.configurationmetadata.Deprecation; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.metadata.IndexNavigator; @@ -41,10 +43,13 @@ import org.springframework.ide.vscode.boot.metadata.types.TypeUtil.BeanPropertyN import org.springframework.ide.vscode.boot.metadata.types.TypeUtil.EnumCaseMode; import org.springframework.ide.vscode.boot.metadata.types.TypedProperty; import org.springframework.ide.vscode.boot.metadata.util.PropertyDocUtils; +import org.springframework.ide.vscode.boot.properties.hover.PropertiesDefinitionCalculator; import org.springframework.ide.vscode.commons.java.IField; import org.springframework.ide.vscode.commons.java.IJavaElement; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IMember; +import org.springframework.ide.vscode.commons.java.IMethod; +import org.springframework.ide.vscode.commons.java.IType; 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.LazyProposalApplier; @@ -97,10 +102,13 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon public final TypeUtil typeUtil; - public ApplicationYamlAssistContext(YamlDocument doc, int documentSelector, YamlPath contextPath, TypeUtil typeUtil, RelaxedNameConfig conf) { + public final JavaElementLocationProvider javaElementLocationProvider; + + public ApplicationYamlAssistContext(YamlDocument doc, int documentSelector, YamlPath contextPath, TypeUtil typeUtil, RelaxedNameConfig conf, JavaElementLocationProvider javaElementLocationProvider) { super(doc, documentSelector, contextPath); this.typeUtil = typeUtil; this.conf = conf; + this.javaElementLocationProvider = javaElementLocationProvider; } /** @@ -130,8 +138,8 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon */ protected abstract Type getType(); - public static ApplicationYamlAssistContext subdocument(YamlDocument doc, int documentSelector, FuzzyMap index, PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) { - return new IndexContext(doc, documentSelector, YamlPath.EMPTY, IndexNavigator.with(index), completionFactory, typeUtil, conf); + public static ApplicationYamlAssistContext subdocument(YamlDocument doc, int documentSelector, FuzzyMap index, PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf, JavaElementLocationProvider javaElementLocationProvider) { + return new IndexContext(doc, documentSelector, YamlPath.EMPTY, IndexNavigator.with(index), completionFactory, typeUtil, conf, javaElementLocationProvider); } private static class TypeContext extends ApplicationYamlAssistContext { @@ -142,8 +150,8 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon private HintProvider hints; public TypeContext(ApplicationYamlAssistContext parent, YamlPath contextPath, Type type, - PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf, HintProvider hints) { - super(parent.getDocument(), parent.documentSelector, contextPath, typeUtil, conf); + PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf, HintProvider hints, JavaElementLocationProvider javaElementLocationProvider) { + super(parent.getDocument(), parent.documentSelector, contextPath, typeUtil, conf, javaElementLocationProvider); this.parent = parent; this.completionFactory = completionFactory; this.type = type; @@ -341,7 +349,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon private AbstractYamlAssistContext contextWith(YamlPathSegment s, Type nextType) { if (nextType!=null) { return new TypeContext(this, contextPath.append(s), nextType, completionFactory, typeUtil, conf, - new YamlPath(s).traverse(hints)); + new YamlPath(s).traverse(hints), javaElementLocationProvider); } return null; } @@ -382,6 +390,42 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon return null; } + @Override + public List getDefinitionsForPropertyKey() { + if (parent instanceof IndexContext) { + //this context is in fact an 'alias' of its parent, representing the + // point in the context hierarchy where a we transition from navigating + // the index to navigating type/bean properties + return parent.getDefinitionsForPropertyKey(); + } else { + String propName = contextPath.getBeanPropertyName(); + Type parentType = parent.getType(); + IJavaProject javaProject = typeUtil.getJavaProject(); + Type keyType = typeUtil.getMapKeyType(parentType); + if (keyType != null) { + String keyValue = contextPath.getLastSegment().toPropString(); + return PropertiesDefinitionCalculator.getValueDefinitionLocations(javaElementLocationProvider, typeUtil, keyType, keyValue); + } else { + IType javaType = javaProject.findType(parentType.getErasure()); + if (javaType != null) { + IMethod method = PropertiesDefinitionCalculator.getPropertyMethod(javaType, propName); + if (method != null) { + Location location = javaElementLocationProvider.findLocation(javaProject, method); + if (location != null) { + return ImmutableList.of(location); + } + } + } + } + return ImmutableList.of(); + } + } + + @Override + public List getDefinitionsForPropertyValue(DocumentRegion valueRegion) { + return PropertiesDefinitionCalculator.getValueDefinitionLocations(javaElementLocationProvider, typeUtil, type, valueRegion.toString().trim()); + } + } private static class IndexContext extends ApplicationYamlAssistContext { @@ -390,8 +434,8 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon PropertyCompletionFactory completionFactory; public IndexContext(YamlDocument doc, int documentSelector, YamlPath contextPath, IndexNavigator indexNav, - PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) { - super(doc, documentSelector, contextPath, typeUtil, conf); + PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf, JavaElementLocationProvider javaElementLocationProvider) { + super(doc, documentSelector, contextPath, typeUtil, conf, javaElementLocationProvider); this.indexNav = indexNav; this.completionFactory = completionFactory; } @@ -467,11 +511,11 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } } if (subIndex.getExtensionCandidate()!=null) { - return new IndexContext(getDocument(), documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf); + return new IndexContext(getDocument(), documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf, javaElementLocationProvider); } else if (subIndex.getExactMatch()!=null) { - IndexContext asIndexContext = new IndexContext(getDocument(), documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf); + IndexContext asIndexContext = new IndexContext(getDocument(), documentSelector, contextPath.append(s), subIndex, completionFactory, typeUtil, conf, javaElementLocationProvider); PropertyInfo prop = subIndex.getExactMatch(); - return new TypeContext(asIndexContext, contextPath.append(s), TypeParser.parse(prop.getType()), completionFactory, typeUtil, conf, prop.getHints(typeUtil, true)); + return new TypeContext(asIndexContext, contextPath.append(s), TypeParser.parse(prop.getType()), completionFactory, typeUtil, conf, prop.getHints(typeUtil, true), javaElementLocationProvider); } } //Unsuported navigation => no context for assist @@ -502,6 +546,23 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon return null; } + @Override + public List getDefinitionsForPropertyKey() { + PropertyInfo prop = indexNav.getExactMatch(); + if (prop != null) { + IJavaProject project = typeUtil.getJavaProject(); + Collection elements = PropertiesDefinitionCalculator.getPropertyJavaElement(project, prop); + return PropertiesDefinitionCalculator.getLocations(javaElementLocationProvider, project, elements); + } + return ImmutableList.of(); + } + + @Override + public List getDefinitionsForPropertyValue(DocumentRegion valueRegion) { + // Shouldn't be reaching this point. TypeContext should be supplying the value definition + return super.getDefinitionsForPropertyValue(valueRegion); + } + @Override public Renderable getHoverInfo(YamlPathSegment lastSegment) { // TODO Auto-generated method stub @@ -521,11 +582,11 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon return null; } - public static YamlAssistContext global(YamlDocument doc, final FuzzyMap index, final PropertyCompletionFactory completionFactory, final TypeUtil typeUtil, final RelaxedNameConfig conf) { + public static YamlAssistContext global(YamlDocument doc, final FuzzyMap index, final PropertyCompletionFactory completionFactory, final TypeUtil typeUtil, final RelaxedNameConfig conf, JavaElementLocationProvider javaElementLocationProvider) { return new TopLevelAssistContext() { @Override protected YamlAssistContext getDocumentContext(int documentSelector) { - return subdocument(doc, documentSelector, index, completionFactory, typeUtil, conf); + return subdocument(doc, documentSelector, index, completionFactory, typeUtil, conf, javaElementLocationProvider); } @Override diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/PropertyEditorTestConf.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/PropertyEditorTestConf.java index fa21ddbac..81f372c6a 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/PropertyEditorTestConf.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/PropertyEditorTestConf.java @@ -1,3 +1,13 @@ +/******************************************************************************* + * 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.bootiful; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -6,12 +16,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness; +import org.springframework.ide.vscode.boot.java.links.JavaDocumentUriProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; import org.springframework.ide.vscode.boot.metadata.types.TypeUtilProvider; +import org.springframework.ide.vscode.boot.test.DefinitionLinkAsserts; 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.util.SimpleLanguageServer; @@ -64,4 +76,8 @@ import org.springframework.ide.vscode.project.harness.MockRunningAppProvider; return SourceLinkFactory.NO_SOURCE_LINKS; } + @Bean DefinitionLinkAsserts definitionLinkAsserts(JavaDocumentUriProvider javaDocumentUriProvider, CompilationUnitCache cuCache) { + return new DefinitionLinkAsserts(javaDocumentUriProvider, cuCache); + } + } \ No newline at end of file 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 e56d877db..51b68d75f 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 @@ -13,33 +13,18 @@ package org.springframework.ide.vscode.boot.test; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.ide.vscode.boot.properties.reconcile.ApplicationPropertiesProblemType.PROP_DUPLICATE_KEY; +import static org.springframework.ide.vscode.boot.test.DefinitionLinkAsserts.field; +import static org.springframework.ide.vscode.boot.test.DefinitionLinkAsserts.method; import static org.springframework.ide.vscode.languageserver.testharness.ClasspathTestUtil.getOutputFolder; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; -import java.net.URI; -import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Path; import java.time.Duration; -import java.util.Arrays; import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import org.eclipse.jdt.core.dom.ASTVisitor; -import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; -import org.eclipse.jdt.core.dom.EnumConstantDeclaration; -import org.eclipse.jdt.core.dom.EnumDeclaration; -import org.eclipse.jdt.core.dom.MethodDeclaration; -import org.eclipse.jdt.core.dom.SimpleName; -import org.eclipse.jdt.core.dom.SingleVariableDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Range; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,24 +36,17 @@ import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.PropertyEditorTestConf; import org.springframework.ide.vscode.boot.editor.harness.AbstractPropsEditorTest; import org.springframework.ide.vscode.boot.editor.harness.StyledStringMatcher; -import org.springframework.ide.vscode.boot.java.links.JavaDocumentUriProvider; -import org.springframework.ide.vscode.boot.java.links.SourceLinks; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.metadata.CachingValueProvider; import org.springframework.ide.vscode.boot.metadata.PropertiesLoader; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IType; -import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; -import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.LanguageId; -import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCustomizer; import org.springframework.test.context.junit4.SpringRunner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; /** @@ -82,13 +60,7 @@ import com.google.common.io.Files; public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { @Autowired - CompilationUnitCache cuCache; - - @Autowired - SimpleTextDocumentService docService; - - @Autowired - JavaDocumentUriProvider javaDocumentUriProvider; + private DefinitionLinkAsserts definitionLinkAsserts; @Configuration static class TestConf { @Bean LanguageId defaultLanguageId() { @@ -317,258 +289,20 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "flyway.init-sqls=a,b,c\n" ); - assertLinkTargets(editor, "server", p, + definitionLinkAsserts.assertLinkTargets(editor, "server", p, method("org.springframework.boot.autoconfigure.web.ServerProperties", "setPort", "java.lang.Integer")); - assertLinkTargets(editor, "data", p, + definitionLinkAsserts.assertLinkTargets(editor, "data", p, method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "hikariDataSource"), method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "tomcatDataSource"), method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "dbcpDataSource") ); - assertLinkTargets(editor, "flyway", p, method("org.springframework.boot.autoconfigure.flyway.FlywayProperties", "setInitSqls", "java.util.List")); + definitionLinkAsserts.assertLinkTargets(editor, "flyway", p, method("org.springframework.boot.autoconfigure.flyway.FlywayProperties", "setInitSqls", "java.util.List")); System.out.println("<<< testHyperlinkTargets"); } - void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaMethod... methods) throws Exception { - Set expectedLocations = Arrays.stream(methods).map(method -> { - try { - return getLocation(project, method); - } catch (Exception e) { - throw new IllegalStateException(e); - } - }).collect(Collectors.toSet()); - editor.assertLinkTargets(hoverOver, expectedLocations); - } - - void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, String typeFqName) throws Exception { - - Location expectedLocation = getLocation(project, typeFqName); - - System.out.println("Expected Location: " + expectedLocation); - - editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); - } - - void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaField field) throws Exception { - - Location expectedLocation = getLocation(project, field); - - System.out.println("Expected Location: " + expectedLocation); - - editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); - } - - static private JavaMethod method(String fqClassName, String methodName, String... params) { - return new JavaMethod(fqClassName, methodName, params); - } - - static private JavaField field(String fqClassName, String name) { - return new JavaField(fqClassName, name); - } - - static class JavaMethod { - public final String fqName; - public final String methodName; - public final String[] params; - public JavaMethod(String fqClassName, String methodName, String... params) { - this.fqName = fqClassName; - this.methodName = methodName; - this.params = params; - } - @Override - public String toString() { - return "JavaMethod [fqName=" + fqName + ", methodName=" + methodName + ", params=" + Arrays.toString(params) - + "]"; - } - } - - static class JavaField { - public final String fqName; - public final String fieldName; - public JavaField(String fqName, String fieldName) { - super(); - this.fqName = fqName; - this.fieldName = fieldName; - } - @Override - public String toString() { - return "JavaField [fqName=" + fqName + ", fieldName=" + fieldName + "]"; - } - } - - private Location getLocation(IJavaProject project, String fqName) throws Exception { - Location loc = new Location(); - Optional sourceUrl = SourceLinks.source(project, fqName); - if (sourceUrl.isPresent()) { - - URI docUri = javaDocumentUriProvider.docUri(project, fqName); - loc.setUri(docUri.toString()); - - String typeName = fqName.substring(fqName.lastIndexOf('.') + 1); - URI sourceUri = sourceUrl.get().toURI(); - Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { - try { - TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); - doc.setText(cuCache.fetchContent(sourceUri)); - AtomicReference range = new AtomicReference<>(null); - cu.accept(new ASTVisitor() { - - private boolean proceessTypeNode(TextDocument doc, String typeName, - AtomicReference range, AbstractTypeDeclaration node) { - SimpleName nameNode = node.getName(); - if (nameNode.getIdentifier().equals(typeName)) { - try { - range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); - return false; - } catch (BadLocationException e) { - throw new IllegalStateException(e); - } - } - return true; - } - - @Override - public boolean visit(TypeDeclaration node) { - return proceessTypeNode(doc, typeName, range, node); - } - - @Override - public boolean visit(EnumDeclaration node) { - return proceessTypeNode(doc, typeName, range, node); - } - - }); - return range.get(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - }); - if (r == null) { - throw new IllegalStateException("Couldn't find " + fqName); - } - loc.setRange(r); - } - return loc; - } - - private Location getLocation(IJavaProject project, JavaMethod method) throws Exception { - Location loc = new Location(); - Optional sourceUrl = SourceLinks.source(project, method.fqName); - if (sourceUrl.isPresent()) { - - URI docUri = javaDocumentUriProvider.docUri(project, method.fqName); - loc.setUri(docUri.toString()); - - URI sourceUri = sourceUrl.get().toURI(); - Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { - try { - AtomicReference range = new AtomicReference<>(null); - TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); - doc.setText(cuCache.fetchContent(sourceUri)); - cu.accept(new ASTVisitor() { - - @Override - public boolean visit(MethodDeclaration node) { - SimpleName nameNode = node.getName(); - if (nameNode.getIdentifier().equals(method.methodName)) { - if (node.parameters().size() != method.params.length) { - return false; - } - int i = 0; - for (Object _p : node.parameters()) { - if (_p instanceof SingleVariableDeclaration) { - SingleVariableDeclaration p = (SingleVariableDeclaration) _p; - String fqName = p.getType().resolveBinding().getErasure().getQualifiedName(); - if (!fqName.equals(method.params[i++])) { - return false; - } - } else { - return false; - } - } - try { - range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); - } catch (BadLocationException e) { - throw new IllegalStateException(e); - } - } - return false; - } - - }); - return range.get(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - }); - if (r == null) { - throw new IllegalStateException("Couldn't find " + method); - } - loc.setRange(r); - } - return loc; - } - - private Location getLocation(IJavaProject project, JavaField field) throws Exception { - Location loc = new Location(); - - Optional sourceUrl = SourceLinks.source(project, field.fqName); - if (sourceUrl.isPresent()) { - - URI sourceUri = sourceUrl.get().toURI(); - - URI docUri = javaDocumentUriProvider.docUri(project, field.fqName); - loc.setUri(docUri.toString()); - - Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { - try { - AtomicReference range = new AtomicReference<>(null); - TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); - doc.setText(cuCache.fetchContent(sourceUri)); - cu.accept(new ASTVisitor() { - - boolean foundType = false; - - @Override - public boolean visit(EnumConstantDeclaration node) { - if (foundType) { - SimpleName nameNode = node.getName(); - if (nameNode.getIdentifier().equals(field.fieldName)) { - try { - range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); - } catch (BadLocationException e) { - throw new IllegalStateException(e); - } - } - } - return true; - } - - @Override - public boolean visit(EnumDeclaration node) { - if (node.getName().getIdentifier() - .equals(field.fqName.substring(field.fqName.lastIndexOf('.') + 1))) { - foundType = true; - return true; - } - return false; - } - - }); - return range.get(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - }); - if (r == null) { - throw new IllegalStateException("Couldn't find " + field); - } - loc.setRange(r); - } - return loc; - } @Test public void testHyperlinkTargetsLoggingLevel() throws Exception { System.out.println(">>> testHyperlinkTargetsLoggingLevel"); @@ -580,7 +314,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "logging.level.com.acme=INFO\n" ); - assertLinkTargets(editor, "level", p, "org.springframework.boot.logging.LoggingApplicationListener"); + definitionLinkAsserts.assertLinkTargets(editor, "level", p, "org.springframework.boot.logging.LoggingApplicationListener"); System.out.println("<<< testHyperlinkTargetsLoggingLevel"); } @@ -1702,7 +1436,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "spring.data.mongodb.field-naming-strategy=org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy\n" + "#more stuff" ); - assertLinkTargets(editor, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", project, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); + definitionLinkAsserts.assertLinkTargets(editor, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", project, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); //Linking should also work for types that aren't valid based on the constraints @@ -1713,7 +1447,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "spring.data.mongodb.field-naming-strategy=java.lang.String\n" + "#more stuff" ); - assertLinkTargets(editor, "java.lang.String", project, "java.lang.String"); + definitionLinkAsserts.assertLinkTargets(editor, "java.lang.String", project, "java.lang.String"); // Instead of java.lang.String editor = newEditor( @@ -1721,7 +1455,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "spring.data.mongodb.field-naming-strategy=org.springframework.core.io.Resource\n" + "#more stuff" ); - assertLinkTargets(editor, "org.springframework.core.io.Resource", project, "org.springframework.core.io.Resource"); + definitionLinkAsserts.assertLinkTargets(editor, "org.springframework.core.io.Resource", project, "org.springframework.core.io.Resource"); } @@ -1894,12 +1628,12 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { editor = newEditor( "my.background: RED" ); - assertLinkTargets(editor, "RED", project, field("demo.Color", "RED")); + definitionLinkAsserts.assertLinkTargets(editor, "RED", project, field("demo.Color", "RED")); editor = newEditor( "my.background=red" ); - assertLinkTargets(editor, "red", project, field("demo.Color", "RED")); + definitionLinkAsserts.assertLinkTargets(editor, "red", project, field("demo.Color", "RED")); } @Test public void testEnumInPojoField() throws Exception { @@ -1911,8 +1645,8 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { editor = newEditor( "my.screen.background=green" ); - assertLinkTargets(editor, "background", project, method("com.example.demo.MyProperties$Screen", "getScreen")); - assertLinkTargets(editor, "green", project, field("com.example.demo.Color", "GREEN")); + definitionLinkAsserts.assertLinkTargets(editor, "background", project, method("com.example.demo.MyProperties$Screen", "getScreen")); + definitionLinkAsserts.assertLinkTargets(editor, "green", project, field("com.example.demo.Color", "GREEN")); } @Test public void testNoHoverForUnrecognizedProperty() throws Exception { 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 9e5c117fa..7a685874d 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 @@ -13,6 +13,8 @@ package org.springframework.ide.vscode.boot.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.springframework.ide.vscode.boot.test.DefinitionLinkAsserts.field; +import static org.springframework.ide.vscode.boot.test.DefinitionLinkAsserts.method; import static org.springframework.ide.vscode.languageserver.testharness.Editor.INDENTED_COMPLETION; import java.time.Duration; @@ -20,10 +22,10 @@ import java.util.Optional; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; -import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -34,6 +36,7 @@ import org.springframework.ide.vscode.boot.editor.harness.StyledStringMatcher; import org.springframework.ide.vscode.boot.metadata.CachingValueProvider; import org.springframework.ide.vscode.boot.metadata.PropertyInfo; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; import org.springframework.ide.vscode.commons.util.RunnableWithException; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.LanguageId; @@ -52,6 +55,9 @@ import org.springframework.test.context.junit4.SpringRunner; @Import(PropertyEditorTestConf.class) public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { + @Autowired + private DefinitionLinkAsserts definitionLinkAsserts; + @Configuration static class TestConf { @Bean LanguageId defaultLanguageId() { return LanguageId.BOOT_PROPERTIES_YAML; @@ -384,7 +390,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { } - @Ignore @Test public void testUserDefinedHoversandLinkTargets() throws Exception { + @Ignore @Test public void testUserDefinedHovers() throws Exception { useProject(createPredefinedMavenProject("enums-boot-1.3.2-app")); data("foo.link-tester", "demo.LinkTestSubject", null, "for testing different Pojo link cases"); Editor editor = newEditor( @@ -407,12 +413,30 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { editor.assertHoverContains("name", "Set the name"); // javadoc from setter editor.assertHoverContains("next", "Get the next"); // javadoc from getter - editor.assertLinkTargets("data", "demo.FooProperties.setdata(ColorData)"); - editor.assertLinkTargets("wavelen", "demo.ColorData.setWavelen(double)"); - } - @Ignore @Test public void testHyperlinkTargets() throws Exception { + @Test public void testUserDefinedLinkTargets() throws Exception { + MavenJavaProject project = createPredefinedMavenProject("enums-boot-1.3.2-app"); + useProject(project); + data("foo.link-tester", "demo.LinkTestSubject", null, "for testing different Pojo link cases"); + Editor editor = newEditor( + "#A comment at the start\n" + + "foo:\n" + + " data:\n" + + " wavelen: 666\n" + + " name: foo\n" + + " next: green\n" + + " link-tester:\n" + + " has-it-all: nice\n" + + " strange: weird\n" + + " getter-only: getme\n" + ); + + definitionLinkAsserts.assertLinkTargets(editor, "data", project, method("demo.FooProperties", "setData", "demo.ColorData")); + definitionLinkAsserts.assertLinkTargets(editor, "wavelen", project, method("demo.ColorData", "setWavelen", "double")); + } + + @Test public void testHyperlinkTargets() throws Exception { IJavaProject p = createPredefinedMavenProject("tricky-getters-boot-1.3.1-app"); useProject(p); @@ -426,16 +450,16 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " init-sqls: a,b,c\n" ); - editor.assertLinkTargets("port", - "org.springframework.boot.autoconfigure.web.ServerProperties.setPort(Integer)" + definitionLinkAsserts.assertLinkTargets(editor, "port", p, + method("org.springframework.boot.autoconfigure.web.ServerProperties", "setPort", "java.lang.Integer") ); - editor.assertLinkTargets("login-", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.hikariDataSource()", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.tomcatDataSource()", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.dbcpDataSource()" + definitionLinkAsserts.assertLinkTargets(editor, "login-", p, + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "hikariDataSource"), + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "tomcatDataSource"), + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "dbcpDataSource") ); - editor.assertLinkTargets("init-sql", - "org.springframework.boot.autoconfigure.flyway.FlywayProperties.setInitSqls(List)"); + definitionLinkAsserts.assertLinkTargets(editor, "init-sql", p, + method("org.springframework.boot.autoconfigure.flyway.FlywayProperties", "setInitSqls", "java.util.List")); } @Test public void testReconcile() throws Exception { @@ -3552,9 +3576,10 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { ); } - @Ignore @Test public void testClassReferenceInValueLink() throws Exception { + @Test public void testClassReferenceInValueLink() throws Exception { Editor editor; - useProject(createPredefinedMavenProject("empty-boot-1.3.0-with-mongo")); + MavenJavaProject project = createPredefinedMavenProject("empty-boot-1.3.0-with-mongo"); + useProject(project); editor = newEditor( "spring:\n" + @@ -3562,7 +3587,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " mongodb:\n" + " field-naming-strategy: org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy\n" ); - editor.assertLinkTargets("org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); + definitionLinkAsserts.assertLinkTargets(editor, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", project, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); editor = newEditor( "spring:\n" + @@ -3571,7 +3596,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " field-naming-strategy:\n" + " org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy\n" ); - editor.assertLinkTargets("org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); + definitionLinkAsserts.assertLinkTargets(editor, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", project, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); //Linking should also work for types that aren't valid based on the constraints editor = newEditor( @@ -3581,7 +3606,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " field-naming-strategy: java.lang.String\n" + "#more stuff" ); - editor.assertLinkTargets("java.lang.String", "java.lang.String"); + definitionLinkAsserts.assertLinkTargets(editor, "java.lang.String", project, "java.lang.String"); } @Test public void test_STS_3335_reconcile_list_nested_in_Map_of_String() throws Exception { @@ -3716,27 +3741,29 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { editor.assertHoverContains("RED", "Hot and delicious"); } - @Ignore @Test public void testHyperLinkEnumValue() throws Exception { + @Test public void testHyperLinkEnumValue() throws Exception { Editor editor; - useProject(createPredefinedMavenProject("enums-boot-1.3.2-app")); + MavenJavaProject project = createPredefinedMavenProject("enums-boot-1.3.2-app"); + useProject(project); data("my.background", "demo.Color", null, "Color to use as default background."); editor = newEditor( "my:\n" + " background: RED" ); - editor.assertLinkTargets("RED", "demo.Color.RED"); + definitionLinkAsserts.assertLinkTargets(editor, "RED", project, field("demo.Color", "RED")); editor = newEditor( "my:\n" + " background: red" ); - editor.assertLinkTargets("red", "demo.Color.RED"); + definitionLinkAsserts.assertLinkTargets(editor, "red", project, field("demo.Color", "RED")); } - @Ignore @Test public void testHyperLinkEnumValueInMapKey() throws Exception { + @Test public void testHyperLinkEnumValueInMapKey() throws Exception { Editor editor; - useProject(createPredefinedMavenProject("enums-boot-1.3.2-app")); + MavenJavaProject project = createPredefinedMavenProject("enums-boot-1.3.2-app"); + useProject(project); data("my.color.map", "java.util.Map", null, "Pretty names for the colors."); editor = newEditor( @@ -3746,8 +3773,8 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " RED: Rood\n" + " green: Groen\n" ); - editor.assertLinkTargets("RED", "demo.Color.RED"); - editor.assertLinkTargets("green", "demo.Color.GREEN"); + definitionLinkAsserts.assertLinkTargets(editor, "RED", project, field("demo.Color", "RED")); + definitionLinkAsserts.assertLinkTargets(editor, "green", project, field("demo.Color", "GREEN")); editor = newEditor( "spring:\n" + @@ -3755,8 +3782,8 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " serialization:\n" + " INDENT_OUTPUT: true" ); - editor.assertLinkTargets("INDENT_OUTPUT", - "com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT" + definitionLinkAsserts.assertLinkTargets(editor, "INDENT_OUTPUT", project, + field("com.fasterxml.jackson.databind.SerializationFeature", "INDENT_OUTPUT") ); editor = newEditor( @@ -3765,8 +3792,8 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " serialization:\n" + " indent-output: true" ); - editor.assertLinkTargets("indent-output", - "com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT" + definitionLinkAsserts.assertLinkTargets(editor, "indent-output", project, + field("com.fasterxml.jackson.databind.SerializationFeature", "INDENT_OUTPUT") ); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/DefinitionLinkAsserts.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/DefinitionLinkAsserts.java new file mode 100644 index 000000000..8c18fd225 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/DefinitionLinkAsserts.java @@ -0,0 +1,293 @@ +/******************************************************************************* + * 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.test; + +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.EnumConstantDeclaration; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; +import org.springframework.ide.vscode.boot.java.links.JavaDocumentUriProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.languageserver.testharness.Editor; + +import com.google.common.collect.ImmutableSet; + +public class DefinitionLinkAsserts { + + private JavaDocumentUriProvider javaDocumentUriProvider; + private CompilationUnitCache cuCache; + + public static JavaMethod method(String fqClassName, String methodName, String... params) { + return new JavaMethod(fqClassName, methodName, params); + } + + public static JavaField field(String fqClassName, String name) { + return new JavaField(fqClassName, name); + } + + public static class JavaMethod { + public final String fqName; + public final String methodName; + public final String[] params; + public JavaMethod(String fqClassName, String methodName, String... params) { + this.fqName = fqClassName; + this.methodName = methodName; + this.params = params; + } + @Override + public String toString() { + return "JavaMethod [fqName=" + fqName + ", methodName=" + methodName + ", params=" + Arrays.toString(params) + + "]"; + } + } + + public static class JavaField { + public final String fqName; + public final String fieldName; + public JavaField(String fqName, String fieldName) { + super(); + this.fqName = fqName; + this.fieldName = fieldName; + } + @Override + public String toString() { + return "JavaField [fqName=" + fqName + ", fieldName=" + fieldName + "]"; + } + } + + public DefinitionLinkAsserts(JavaDocumentUriProvider javaDocumentUriProvider, CompilationUnitCache cuCache) { + this.javaDocumentUriProvider = javaDocumentUriProvider; + this.cuCache = cuCache; + + } + + public void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaMethod... methods) throws Exception { + Set expectedLocations = Arrays.stream(methods).map(method -> { + try { + return getLocation(project, method); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }).collect(Collectors.toSet()); + + editor.assertLinkTargets(hoverOver, expectedLocations); + } + + public void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, String typeFqName) throws Exception { + + Location expectedLocation = getLocation(project, typeFqName); + + System.out.println("Expected Location: " + expectedLocation); + + editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); + } + + public void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaField field) throws Exception { + + Location expectedLocation = getLocation(project, field); + + System.out.println("Expected Location: " + expectedLocation); + + editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); + } + + private Location getLocation(IJavaProject project, String fqName) throws Exception { + Location loc = new Location(); + Optional sourceUrl = SourceLinks.source(project, fqName); + if (sourceUrl.isPresent()) { + + URI docUri = javaDocumentUriProvider.docUri(project, fqName); + loc.setUri(docUri.toString()); + + String typeName = fqName.substring(fqName.lastIndexOf('.') + 1); + URI sourceUri = sourceUrl.get().toURI(); + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + AtomicReference range = new AtomicReference<>(null); + cu.accept(new ASTVisitor() { + + private boolean proceessTypeNode(TextDocument doc, String typeName, + AtomicReference range, AbstractTypeDeclaration node) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(typeName)) { + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + return false; + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + return true; + } + + @Override + public boolean visit(TypeDeclaration node) { + return proceessTypeNode(doc, typeName, range, node); + } + + @Override + public boolean visit(EnumDeclaration node) { + return proceessTypeNode(doc, typeName, range, node); + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + fqName); + } + loc.setRange(r); + } + return loc; + } + + private Location getLocation(IJavaProject project, JavaMethod method) throws Exception { + Location loc = new Location(); + Optional sourceUrl = SourceLinks.source(project, method.fqName); + if (sourceUrl.isPresent()) { + + URI docUri = javaDocumentUriProvider.docUri(project, method.fqName); + loc.setUri(docUri.toString()); + + URI sourceUri = sourceUrl.get().toURI(); + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + AtomicReference range = new AtomicReference<>(null); + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodDeclaration node) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(method.methodName)) { + if (node.parameters().size() != method.params.length) { + return false; + } + int i = 0; + for (Object _p : node.parameters()) { + if (_p instanceof SingleVariableDeclaration) { + SingleVariableDeclaration p = (SingleVariableDeclaration) _p; + String fqName = p.getType().resolveBinding().getErasure().getQualifiedName(); + if (!fqName.equals(method.params[i++])) { + return false; + } + } else { + return false; + } + } + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + return false; + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + method); + } + loc.setRange(r); + } + return loc; + } + + private Location getLocation(IJavaProject project, JavaField field) throws Exception { + Location loc = new Location(); + + Optional sourceUrl = SourceLinks.source(project, field.fqName); + if (sourceUrl.isPresent()) { + + URI sourceUri = sourceUrl.get().toURI(); + + URI docUri = javaDocumentUriProvider.docUri(project, field.fqName); + loc.setUri(docUri.toString()); + + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + AtomicReference range = new AtomicReference<>(null); + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + cu.accept(new ASTVisitor() { + + boolean foundType = false; + + @Override + public boolean visit(EnumConstantDeclaration node) { + if (foundType) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(field.fieldName)) { + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + } + return true; + } + + @Override + public boolean visit(EnumDeclaration node) { + if (node.getName().getIdentifier() + .equals(field.fqName.substring(field.fqName.lastIndexOf('.') + 1))) { + foundType = true; + return true; + } + return false; + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + field); + } + loc.setRange(r); + } + return loc; + } + +}