diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java index 653147a91..ead4826e6 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java @@ -141,6 +141,13 @@ public class StringUtil { return ""; } + public static String lowerCaseFirstChar(String string) { + if (StringUtil.hasText(string)) { + return Character.toLowerCase(string.charAt(0)) + string.substring(1); + } + return ""; + } + public static String datestamp() { Date d = new Date(); SimpleDateFormat f = new SimpleDateFormat("yyyyMMdd"); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java index f07052060..4c01c7cb5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java @@ -22,6 +22,8 @@ import org.eclipse.lsp4xml.dom.DOMParser; import org.eclipse.lsp4xml.dom.parser.Scanner; import org.eclipse.lsp4xml.dom.parser.TokenType; import org.eclipse.lsp4xml.dom.parser.XMLScanner; +import org.springframework.ide.vscode.boot.xml.completions.PropertyNameCompletionProposalProvider; +import org.springframework.ide.vscode.boot.xml.completions.TypeCompletionProposalProvider; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -37,12 +39,15 @@ public class SpringXMLCompletionEngine implements ICompletionEngine { private static final String BEAN_ELEMENT = "bean"; private static final String BEAN_CLASS_ATTRIBUTE = "class"; + private static final String PROPERTY_ELEMENT = "property"; + private static final String PROPERTY_NAME_ATTRIBUTE = "name"; private final Map completionProviders; public SpringXMLCompletionEngine(SpringXMLLanguageServerComponents springXMLLanguageServerComponents, JavaProjectFinder projectFinder) { this.completionProviders = new HashMap<>(); - this.completionProviders.put(new XMLCompletionProviderKey(BEANS_NAMESPACE, BEAN_ELEMENT, BEAN_CLASS_ATTRIBUTE), new TypeCompletionProposalProvider(projectFinder)); + this.completionProviders.put(new XMLCompletionProviderKey(BEANS_NAMESPACE, null, BEAN_ELEMENT, BEAN_CLASS_ATTRIBUTE), new TypeCompletionProposalProvider(projectFinder, true)); + this.completionProviders.put(new XMLCompletionProviderKey(BEANS_NAMESPACE, BEAN_ELEMENT, PROPERTY_ELEMENT, PROPERTY_NAME_ATTRIBUTE), new PropertyNameCompletionProposalProvider(projectFinder)); } @Override @@ -66,7 +71,14 @@ public class SpringXMLCompletionEngine implements ICompletionEngine { DOMAttr attributeAt = dom.findAttrAt(offset); if (attributeAt != null) { - XMLCompletionProviderKey key = new XMLCompletionProviderKey(namespace, node.getLocalName(), attributeAt.getNodeName()); + XMLCompletionProviderKey key = new XMLCompletionProviderKey(namespace, null, node.getLocalName(), attributeAt.getNodeName()); + + if (!this.completionProviders.containsKey(key)) { + DOMNode parentNode = node.getParentNode(); + String parentNodeName = parentNode != null ? parentNode.getLocalName() : null; + key = new XMLCompletionProviderKey(namespace, parentNodeName, node.getLocalName(), attributeAt.getNodeName()); + } + XMLCompletionProvider completionProvider = this.completionProviders.get(key); if (completionProvider != null) { Collection completions = completionProvider.getCompletions(doc, namespace, node, attributeAt, scanner, offset); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java index 7c6aaa34e..6712ea1db 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java @@ -18,10 +18,12 @@ public class XMLCompletionProviderKey { private final String namespaceURI; private final String elementName; private final String attributeName; + private final String parentNodeName; - public XMLCompletionProviderKey(String namespaceURI, String elementName, String attributeName) { + public XMLCompletionProviderKey(String namespaceURI, String parentNodeName, String elementName, String attributeName) { super(); this.namespaceURI = namespaceURI; + this.parentNodeName = parentNodeName; this.elementName = elementName; this.attributeName = attributeName; } @@ -30,6 +32,10 @@ public class XMLCompletionProviderKey { return namespaceURI; } + public String getParentNodeName() { + return parentNodeName; + } + public String getElementName() { return elementName; } @@ -45,6 +51,7 @@ public class XMLCompletionProviderKey { result = prime * result + ((attributeName == null) ? 0 : attributeName.hashCode()); result = prime * result + ((elementName == null) ? 0 : elementName.hashCode()); result = prime * result + ((namespaceURI == null) ? 0 : namespaceURI.hashCode()); + result = prime * result + ((parentNodeName == null) ? 0 : parentNodeName.hashCode()); return result; } @@ -72,6 +79,11 @@ public class XMLCompletionProviderKey { return false; } else if (!namespaceURI.equals(other.namespaceURI)) return false; + if (parentNodeName == null) { + if (other.parentNodeName != null) + return false; + } else if (!parentNodeName.equals(other.parentNodeName)) + return false; return true; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/PropertyNameCompletionProposalProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/PropertyNameCompletionProposalProvider.java new file mode 100644 index 000000000..129359a64 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/PropertyNameCompletionProposalProvider.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.xml.completions; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4xml.dom.DOMAttr; +import org.eclipse.lsp4xml.dom.DOMNode; +import org.eclipse.lsp4xml.dom.parser.Scanner; +import org.springframework.ide.vscode.boot.xml.XMLCompletionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +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.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.Renderable; +import org.springframework.ide.vscode.commons.util.StringUtil; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class PropertyNameCompletionProposalProvider implements XMLCompletionProvider { + + private final JavaProjectFinder projectFinder; + + public PropertyNameCompletionProposalProvider(JavaProjectFinder projectFinder) { + this.projectFinder = projectFinder; + } + + @Override + public Collection getCompletions(TextDocument doc, String namespace, DOMNode node, DOMAttr attributeAt, + Scanner scanner, int offset) { + + int tokenOffset = scanner.getTokenOffset(); + int tokenEnd = scanner.getTokenEnd(); + String tokenText = scanner.getTokenText(); + + Optional foundProject = this.projectFinder.find(doc.getId()); + if (foundProject.isPresent()) { + IJavaProject project = foundProject.get(); + + String prefix = tokenText.substring(0, offset - tokenOffset); + if (prefix.startsWith("\"")) { + prefix = prefix.substring(1); + } + + String beanClass = identifyBeanClass(node); + if (beanClass != null) { + IType beanType = project.getIndex().findType(beanClass); + + final String searchPrefix = prefix; + return beanType.getMethods() + .filter(method -> isPropertyWriteMethod(method)) + .filter(method -> getPropertyName(method).startsWith(searchPrefix)) + .map(method -> createProposal(method, doc, offset, tokenOffset, tokenEnd)) + .collect(Collectors.toList()); + } + }; + + return Collections.emptyList(); + } + + private String identifyBeanClass(DOMNode node) { + DOMNode parentNode = node.getParentNode(); + if (parentNode != null) { + if ("bean".equals(parentNode.getLocalName())) { + String beanClassAttribute = parentNode.getAttribute("class"); + return beanClassAttribute; + } + } + return null; + } + + private ICompletionProposal createProposal(IMethod method, TextDocument doc, int offset, int tokenStart, int tokenEnd) { + String label = getPropertyName(method); + CompletionItemKind kind = CompletionItemKind.Method; + + DocumentEdits edits = new DocumentEdits(doc); + + String replaceString = "\"" + label + "\""; + int replaceStart = tokenStart; + + if (tokenStart < offset) { + replaceStart = offset; + replaceString = replaceString.substring(offset - tokenStart); + } + edits.replace(replaceStart, tokenEnd, replaceString); + + Renderable renderable = null; + + return new TypeCompletionProposal(label, kind, edits, label, renderable, 1d); + } + + private boolean isPropertyWriteMethod(IMethod method) { + return method != null + && method.getElementName().startsWith("set") + && method.getElementName().length() > 3; + } + + private String getPropertyName(IMethod method) { + String methodName = method.getElementName(); + if (methodName.startsWith("set")) { + String propertyName = methodName.substring(3); + if (propertyName.length() > 0) { + return StringUtil.lowerCaseFirstChar(propertyName); + } + } + return methodName; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposal.java similarity index 96% rename from headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposal.java rename to headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposal.java index 7c28b3c98..27aae876d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposal.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.xml; +package org.springframework.ide.vscode.boot.xml.completions; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposalProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposalProvider.java similarity index 90% rename from headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposalProvider.java rename to headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposalProvider.java index 1a3817182..cb21dc9e4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposalProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/TypeCompletionProposalProvider.java @@ -8,15 +8,17 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.xml; +package org.springframework.ide.vscode.boot.xml.completions; import java.util.Collection; +import java.util.Collections; import java.util.Optional; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4xml.dom.DOMAttr; import org.eclipse.lsp4xml.dom.DOMNode; import org.eclipse.lsp4xml.dom.parser.Scanner; +import org.springframework.ide.vscode.boot.xml.XMLCompletionProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IType; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -34,9 +36,11 @@ import reactor.util.function.Tuple2; public class TypeCompletionProposalProvider implements XMLCompletionProvider { private final JavaProjectFinder projectFinder; + private final boolean classesOnly; - public TypeCompletionProposalProvider(JavaProjectFinder projectFinder) { + public TypeCompletionProposalProvider(JavaProjectFinder projectFinder, boolean classesOnly) { this.projectFinder = projectFinder; + this.classesOnly = classesOnly; } @Override @@ -61,11 +65,12 @@ public class TypeCompletionProposalProvider implements XMLCompletionProvider { return types .filter(result -> result.getT1() != null && result.getT1().getElementName() != null && result.getT1().getElementName().length() > 0) + .filter(result -> classesOnly ? result.getT1().isClass() : true) .map(t -> createProposal(t, doc, offset, tokenOffset, tokenEnd)) .collectList().block(); }; - return null; + return Collections.emptyList(); } private ICompletionProposal createProposal(Tuple2 t, TextDocument doc, int offset, int tokenStart, int tokenEnd) {