From aed3398ecb61eccdb017e2ca56eea8127c781807 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 8 Feb 2022 16:19:35 -0500 Subject: [PATCH] Snippets and Spring Data completions --- .../DataRepositoryCompletionProcessor.java | 58 ++++---- .../ide/vscode/boot/java/data/DomainType.java | 26 ++-- .../boot/java/links/AbstractSourceLinks.java | 8 +- .../boot/java/links/VSCodeSourceLinks.java | 2 +- .../java/snippets/JavaSnippetContext.java | 6 +- ...DataRepositoryCompletionProcessorTest.java | 1 - .../test/RequestMappingSnippetTests.java | 130 ++++++++++++++++++ 7 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java index e854d9500..c77fbbe41 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java @@ -15,13 +15,15 @@ import java.util.List; import java.util.Optional; import org.eclipse.lsp4j.CompletionItemKind; +import org.openrewrite.internal.lang.Nullable; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.J.Annotation; import org.openrewrite.java.tree.J.ClassDeclaration; +import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.JavaType.FullyQualified; +import org.openrewrite.java.tree.JavaType.Parameterized; import org.openrewrite.java.tree.TypeUtils; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; -import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.ORAstUtils; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; @@ -104,42 +106,32 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider { } private DataRepositoryDefinition getDataRepositoryDefinition(ClassDeclaration declaration, FullyQualified type) { - if (type != null) { - - // interface analysis - List interfaces = type.getInterfaces(); - for (FullyQualified resolvedInterface : interfaces) { - String simplifiedType = resolvedInterface.getFullyQualifiedName(); - - if (Constants.REPOSITORY_TYPE.equals(simplifiedType)) { - DomainType domainType = null; - // TODO Fix for OR AST -// if (resolvedInterface.isParameterizedType()) { -// ITypeBinding[] typeParameters = resolvedInterface.getTypeArguments(); -// if (typeParameters != null && typeParameters.length > 0) { -// domainType = new DomainType(typeParameters[0]); -// } -// } - return createDataRepositoryDefinitionFromType(domainType); - } - else { - DataRepositoryDefinition repo = getDataRepositoryDefinition(declaration, resolvedInterface); - if (repo != null) { - return repo; - } - } + if (type != null && TypeUtils.isAssignableTo(Constants.REPOSITORY_TYPE, type)) { + JavaType domainType = getFirstParameterType(type); + return new DataRepositoryDefinition(domainType instanceof FullyQualified ? new DomainType((FullyQualified) domainType) : null); + } + return null; + } + + private JavaType getFirstParameterType(FullyQualified type) { + if (type instanceof Parameterized) { + List params = ((Parameterized) type).getTypeParameters(); + if (!params.isEmpty()) { + return params.get(0); } - - // super type analysis - FullyQualified superclass = type.getSupertype(); - if (superclass != null) { - return getDataRepositoryDefinition(declaration, superclass); + } + for (FullyQualified i : type.getInterfaces()) { + JavaType parameType = getFirstParameterType(i); + if (parameType != null) { + return parameType; } } + @Nullable + FullyQualified superclass = type.getSupertype(); + if (superclass != null) { + return getFirstParameterType(superclass); + } return null; } - private DataRepositoryDefinition createDataRepositoryDefinitionFromType(DomainType domainType) { - return new DataRepositoryDefinition(domainType); - } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java index 17081a704..62a536a6f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 2022 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 @@ -13,8 +13,9 @@ package org.springframework.ide.vscode.boot.java.data; import java.util.ArrayList; import java.util.List; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.JavaType.FullyQualified; +import org.openrewrite.java.tree.JavaType.Method; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -35,22 +36,25 @@ public class DomainType { this.simpleName = simpleName; } - public DomainType(ITypeBinding typeBinding) { - this.packageName = typeBinding.getPackage().getName(); - this.fullName = typeBinding.getQualifiedName(); - this.simpleName = typeBinding.getName(); + public DomainType(FullyQualified type) { + this.packageName = type.getPackageName(); + this.fullName = type.getFullyQualifiedName(); + this.simpleName = type.getClassName(); this.properties = Suppliers.memoize(() -> { if (!this.packageName.startsWith("java")) { - IMethodBinding[] methods = typeBinding.getDeclaredMethods(); - if (methods != null && methods.length > 0) { + List methods = type.getMethods(); + if (methods != null && !methods.isEmpty()) { List properties = new ArrayList<>(); - for (IMethodBinding method : methods) { + for (Method method : methods) { String methodName = method.getName(); if (methodName != null && methodName.startsWith("get")) { String propertyName = methodName.substring(3); - properties.add(new DomainProperty(propertyName, new DomainType(method.getReturnType()))); + JavaType returnType = method.getReturnType(); + if (returnType instanceof FullyQualified) { + properties.add(new DomainProperty(propertyName, new DomainType((FullyQualified) returnType))); + } } } return (DomainProperty[]) properties.toArray(new DomainProperty[properties.size()]); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java index 5194727d6..4e80e5fa4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java @@ -139,24 +139,22 @@ public abstract class AbstractSourceLinks implements SourceLinks { if (cu == null) { return null; } - int[] values = new int[] {0, -1}; AtomicReference range = new AtomicReference<>(); int lastDotIndex = fqName.lastIndexOf('.'); String packageName = fqName.substring(0, lastDotIndex); String typeName = fqName.substring(lastDotIndex + 1); - if (packageName.equals(TypeUtils.asFullyQualified(cu.getPackageDeclaration().getExpression().getType()).getFullyQualifiedName())) { + if (packageName.equals(cu.getPackageDeclaration().getExpression().printTrimmed())) { Stack visitedType = new Stack<>(); new JavaIsoVisitor<>() { public org.openrewrite.java.tree.J.ClassDeclaration visitClassDeclaration(org.openrewrite.java.tree.J.ClassDeclaration classDecl, Object p) { - String fqName = classDecl.getType().getFullyQualifiedName(); - visitedType.push(fqName); + visitedType.push(classDecl.getSimpleName()); if (range.get() == null) { if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { range.set(classDecl.getName().getMarkers().findFirst(Range.class).orElseThrow()); } } - if (values[1] < 0) { + if (range.get() == null) { return super.visitClassDeclaration(classDecl, visitedType); } else { return classDecl; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java index 9451b38bb..fa643603d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java @@ -49,7 +49,7 @@ public class VSCodeSourceLinks extends AbstractSourceLinks { sb.append('#'); sb.append(line); sb.append(','); - sb.append(column + 1); // 1-based columns? + sb.append(column); return sb.toString(); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java index 4e683c7bb..e8d29ba64 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 2022 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 @@ -11,10 +11,12 @@ package org.springframework.ide.vscode.boot.java.snippets; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.J.Block; import org.openrewrite.java.tree.J.ClassDeclaration; +import org.springframework.ide.vscode.boot.java.utils.ORAstUtils; public interface JavaSnippetContext { - JavaSnippetContext BOOT_MEMBERS = (node) -> node instanceof ClassDeclaration; + JavaSnippetContext BOOT_MEMBERS = (node) -> node instanceof Block && ORAstUtils.getParent(node) instanceof ClassDeclaration; boolean appliesTo(J node); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java index f87cb09e9..0551097ee 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java @@ -27,7 +27,6 @@ import org.springframework.ide.vscode.boot.bootiful.HoverTestConf; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.languageserver.testharness.Editor; -import org.springframework.ide.vscode.languageserver.testharness.TestAsserts; import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; import org.springframework.ide.vscode.project.harness.ProjectsHarness; import org.springframework.test.context.junit4.SpringRunner; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java new file mode 100644 index 000000000..d92b1191e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2022 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.java.requestmapping.test; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.eclipse.lsp4j.CompletionItem; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.HoverTestConf; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@BootLanguageServerTest +@Import(HoverTestConf.class) +public class RequestMappingSnippetTests { + + @Autowired private BootLanguageServerHarness harness; + private Editor editor; + + @Before + public void setup() throws Exception { + IJavaProject testProject = ProjectsHarness.INSTANCE.mavenProject("test-request-mapping-live-hover"); + harness.useProject(testProject); + harness.intialize(null); + } + + @Test + public void getMapping() throws Exception { + prepareCase("Get<*>"); + assertOneSnippet("package example;\n" + + "\n" + + "import org.springframework.stereotype.Controller;\n" + + "import org.springframework.web.bind.annotation.DeleteMapping;\n" + + "import org.springframework.web.bind.annotation.GetMapping;\n" + + "import org.springframework.web.bind.annotation.PathVariable;\n" + + "import org.springframework.web.bind.annotation.PostMapping;\n" + + "import org.springframework.web.bind.annotation.PutMapping;\n" + + "import org.springframework.web.bind.annotation.RequestBody;\n" + + "import org.springframework.web.bind.annotation.RequestMapping;\n" + + "import org.springframework.web.bind.annotation.ResponseBody;\n" + + "\n" + + "/** Boot Java - Test Completion */\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@GetMapping(value=\"${1:path}\")\n" + + "public ${2:SomeData} ${3:getMethodName}(@RequestParam ${4:String} ${5:param}) {\n" + + " return new ${2:SomeData}($0);\n" + + "}\n" + + "<*>\n" + + "\n" + + "\n" + + " @RequestMapping(\"/hello\")\n" + + " @ResponseBody\n" + + " public String hello() {\n" + + " return \"Hello there!\";\n" + + " }\n" + + " \n" + + " \n" + + " @RequestMapping(\"/goodbye\")\n" + + " @ResponseBody\n" + + " public String goodbye() {\n" + + " return \"Good bye\";\n" + + " }\n" + + "\n" + + " @GetMapping(\"/person/{name}\")\n" + + " public String getMapping(@PathVariable String name) {\n" + + " return \"Hello \" + name;\n" + + " }\n" + + "\n" + + " @DeleteMapping(\"/delete/{id}\")\n" + + " public String removeMe(@PathVariable int id) {\n" + + " System.out.println(\"You are removed: \" + id);\n" + + " return \"Done\";\n" + + " }\n" + + "\n" + + " @PostMapping(\"/postHello\")\n" + + " public String postMethod(@RequestBody String name) {\n" + + " System.out.println(\"Posted hello: \" + name);\n" + + " return name;\n" + + " }\n" + + "\n" + + " @PutMapping(\"/put/{id}\")\n" + + " public String putMethod(@PathVariable int id, @RequestBody String name) {\n" + + " System.out.println(\"Added \" + name + \" with ID: \" + id);\n" + + " return name;\n" + + " }\n" + + "}\n" + + ""); + } + + private void prepareCase(String prefix) throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-request-mapping-live-hover/src/main/java/example/RestApi.java"); + String content = IOUtils.toString(resource); + + content = content.replace("class RestApi {", "class RestApi {\n\n" + prefix); + editor = new Editor(harness, content, LanguageId.JAVA); + } + + private void assertOneSnippet(String expected) throws Exception { + List completions = editor.getCompletions(); + assertEquals(completions.size(), 1); + Editor clonedEditor = editor.clone(); + clonedEditor.apply(completions.get(0)); + assertEquals(expected, clonedEditor.getText()); + } + +}