From 1281abbf92944112fb062985a131a7536bd12062 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Mon, 8 Apr 2019 16:05:38 +0200 Subject: [PATCH] first steps towards auto completion for bean class attribute in spring XML config files --- .../boot/xml/SpringXMLCompletionEngine.java | 57 ++++++++++- .../SpringXMLLanguageServerComponents.java | 7 +- .../boot/xml/TypeCompletionProposal.java | 63 +++++++++++++ .../xml/TypeCompletionProposalProvider.java | 94 +++++++++++++++++++ .../boot/xml/XMLCompletionProvider.java | 29 ++++++ .../boot/xml/XMLCompletionProviderKey.java | 78 +++++++++++++++ 6 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposal.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposalProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java 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 b9329175c..4f8684a82 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 @@ -12,9 +12,19 @@ package org.springframework.ide.vscode.boot.xml; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.eclipse.lsp4xml.dom.DOMAttr; +import org.eclipse.lsp4xml.dom.DOMDocument; +import org.eclipse.lsp4xml.dom.DOMNode; +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.commons.languageserver.completion.ICompletionEngine; 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.text.TextDocument; /** @@ -22,13 +32,54 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; */ public class SpringXMLCompletionEngine implements ICompletionEngine { - public SpringXMLCompletionEngine(SpringXMLLanguageServerComponents springXMLLanguageServerComponents) { + private static final String BEANS_NAMESPACE = "http://www.springframework.org/schema/beans"; + + private static final String BEAN_ELEMENT = "bean"; + private static final String BEAN_CLASS_ATTRIBUTE = "class"; + + + 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)); } @Override - public Collection getCompletions(TextDocument document, int offset) throws Exception { - System.out.println(" SUPER COOL SPRING XML COMPLETION PROPOSALS COMING SOON... STAY TUNED..."); + public Collection getCompletions(TextDocument doc, int offset) throws Exception { + String content = doc.get(); + DOMParser parser = DOMParser.getInstance(); + DOMDocument dom = parser.parse(content, "", null); + + DOMNode node = dom.findNodeBefore(offset); + + if (node != null) { + String namespace = node.getNamespaceURI(); + + Scanner scanner = XMLScanner.createScanner(content, node.getStart(), false); + TokenType token = scanner.scan(); + while (token != TokenType.EOS && scanner.getTokenOffset() <= offset) { + switch (token) { + case AttributeValue: + if (scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd()) { + DOMAttr attributeAt = dom.findAttrAt(offset); + + if (attributeAt != null) { + XMLCompletionProviderKey key = new XMLCompletionProviderKey(namespace, node.getNodeName(), attributeAt.getNodeName()); + XMLCompletionProvider completionProvider = this.completionProviders.get(key); + if (completionProvider != null) { + return completionProvider.getCompletions(doc, namespace, node, attributeAt, scanner, offset); + } + } + } + break; + default: + break; + } + token = scanner.scan(); + } + } return Collections.emptyList(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLLanguageServerComponents.java index 8dfd6fe35..f1b9c6589 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLLanguageServerComponents.java @@ -17,10 +17,9 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; 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.languageserver.util.SimpleWorkspaceService; import org.springframework.ide.vscode.commons.util.text.LanguageId; import com.google.common.collect.ImmutableSet; @@ -36,12 +35,14 @@ public class SpringXMLLanguageServerComponents implements LanguageServerComponen private final SimpleLanguageServer server; private final BootLanguageServerParams serverParams; + private final JavaProjectFinder projectFinder; public SpringXMLLanguageServerComponents( SimpleLanguageServer server, BootLanguageServerParams serverParams) { this.server = server; this.serverParams = serverParams; + this.projectFinder = serverParams.projectFinder; server.doOnInitialized(this::initialized); server.onShutdown(this::shutdown); @@ -54,7 +55,7 @@ public class SpringXMLLanguageServerComponents implements LanguageServerComponen @Override public ICompletionEngine getCompletionEngine() { - return new SpringXMLCompletionEngine(this); + return new SpringXMLCompletionEngine(this, projectFinder); } @Override 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/TypeCompletionProposal.java new file mode 100644 index 000000000..691918646 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposal.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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; + +import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.util.Renderable; + +/** + * @author Martin Lippert + */ +public class TypeCompletionProposal implements ICompletionProposal { + + private final String label; + private final CompletionItemKind kind; + private final DocumentEdits edits; + private final String detail; + private final Renderable documentation; + + public TypeCompletionProposal(String label, CompletionItemKind kind, DocumentEdits edits, String detail, Renderable documentation) { + super(); + this.label = label; + this.kind = kind; + this.edits = edits; + this.detail = detail; + this.documentation = documentation; + } + + @Override + public String getLabel() { + return this.label; + } + + @Override + public CompletionItemKind getKind() { + return this.kind; + } + + @Override + public DocumentEdits getTextEdit() { + return this.edits; + } + + @Override + public String getDetail() { + return this.detail; + } + + @Override + public Renderable getDocumentation() { + return this.documentation; + } + +} 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/TypeCompletionProposalProvider.java new file mode 100644 index 000000000..5fa789e4f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/TypeCompletionProposalProvider.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * 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; + +import java.util.Collection; +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.commons.java.IJavaProject; +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.text.TextDocument; + +import reactor.core.publisher.Flux; +import reactor.util.function.Tuple2; + +/** + * @author Martin Lippert + */ +public class TypeCompletionProposalProvider implements XMLCompletionProvider { + + private final JavaProjectFinder projectFinder; + + public TypeCompletionProposalProvider(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); + } + + Flux> types = project.getIndex().fuzzySearchTypes(prefix, true, true); + + return types.collectSortedList((o1, o2) -> o2.getT2().compareTo(o1.getT2())) + .flatMapIterable(l -> l) + .filter(result -> result.getT1() != null && result.getT1().getElementName() != null && result.getT1().getElementName().length() > 0) + .map(t -> createProposal(t, doc, offset, tokenOffset, tokenEnd)).collectList().block(); + }; + + return null; + } + + private ICompletionProposal createProposal(Tuple2 t, TextDocument doc, int offset, int tokenStart, int tokenEnd) { + IType type = t.getT1(); + + String label = type.getElementName(); + CompletionItemKind kind; + + if (type.isClass()) { + kind = CompletionItemKind.Class; + } + else if (type.isInterface()) { + kind = CompletionItemKind.Interface; + } + else { + kind = CompletionItemKind.Value; + } + + DocumentEdits edits = new DocumentEdits(doc); + edits.replace(tokenStart, tokenEnd, "\"" + type.getFullyQualifiedName() + "\""); + + Renderable renderable = null; + + return new TypeCompletionProposal(label, kind, edits, type.getFullyQualifiedName(), renderable); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProvider.java new file mode 100644 index 000000000..f089bcffb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProvider.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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; + +import java.util.Collection; + +import org.eclipse.lsp4xml.dom.DOMAttr; +import org.eclipse.lsp4xml.dom.DOMNode; +import org.eclipse.lsp4xml.dom.parser.Scanner; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public interface XMLCompletionProvider { + + Collection getCompletions(TextDocument doc, String namespace, DOMNode node, + DOMAttr attributeAt, Scanner scanner, int 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 new file mode 100644 index 000000000..7c6aaa34e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/XMLCompletionProviderKey.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * 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; + +/** + * @author Martin Lippert + */ +public class XMLCompletionProviderKey { + + private final String namespaceURI; + private final String elementName; + private final String attributeName; + + public XMLCompletionProviderKey(String namespaceURI, String elementName, String attributeName) { + super(); + this.namespaceURI = namespaceURI; + this.elementName = elementName; + this.attributeName = attributeName; + } + + public String getNamespaceURI() { + return namespaceURI; + } + + public String getElementName() { + return elementName; + } + + public String getAttributeName() { + return attributeName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((attributeName == null) ? 0 : attributeName.hashCode()); + result = prime * result + ((elementName == null) ? 0 : elementName.hashCode()); + result = prime * result + ((namespaceURI == null) ? 0 : namespaceURI.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + XMLCompletionProviderKey other = (XMLCompletionProviderKey) obj; + if (attributeName == null) { + if (other.attributeName != null) + return false; + } else if (!attributeName.equals(other.attributeName)) + return false; + if (elementName == null) { + if (other.elementName != null) + return false; + } else if (!elementName.equals(other.elementName)) + return false; + if (namespaceURI == null) { + if (other.namespaceURI != null) + return false; + } else if (!namespaceURI.equals(other.namespaceURI)) + return false; + return true; + } + +}