diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingsParser20.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingsParser20.java index fb1fbc27f..52f706527 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingsParser20.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingsParser20.java @@ -30,10 +30,10 @@ public class RequestMappingsParser20 { .getJSONObject(contextId) .getJSONObject("mappings"); JSONArray rmArray = null; - if (mappings.has("dispatcherServlets")) { + if (mappings.has("dispatcherServlets") && mappings.getJSONObject("dispatcherServlets").has("dispatcherServlet")) { // Regular Web starter endpoints RMs JMX beans format rmArray = mappings.getJSONObject("dispatcherServlets").getJSONArray("dispatcherServlet"); - } else if (mappings.has("dispatcherHandlers")) { + } else if (mappings.has("dispatcherHandlers") && mappings.getJSONObject("dispatcherHandlers").has("webHandler")) { // WebFlux endpoints RMs JMX bean format rmArray = mappings.getJSONObject("dispatcherHandlers").getJSONArray("webHandler"); } diff --git a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/Boot2xRequestMappingsTest.java b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/Boot2xRequestMappingsTest.java index 5d9c4e29d..424d47285 100644 --- a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/Boot2xRequestMappingsTest.java +++ b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/Boot2xRequestMappingsTest.java @@ -89,4 +89,45 @@ public class Boot2xRequestMappingsTest { .collect(Collectors.toSet()) ); } + + @Test + public void testWebEurekaRms() throws Exception { + String json = IOUtils.toString(Boot2xRequestMappingsTest.class.getResourceAsStream("/live-rm-beans/rms-boot2-web-eureka.json")); + Collection rms = RequestMappingsParser20.parse(new JSONObject(json)); + assertEquals(9, rms.size()); + + ImmutableSet expected = ImmutableSet.of( + "/error", + "/actuator", + "/actuator/health", + "/actuator/info", + "/welcome" + ); + assertEquals(expected, + rms.stream() + .flatMap(rm -> Arrays.stream(rm.getSplitPath())) + .collect(Collectors.toSet()) + ); + + } + + @Test + public void testWebFluxEurekaRms() throws Exception { + String json = IOUtils.toString(Boot2xRequestMappingsTest.class.getResourceAsStream("/live-rm-beans/rms-boot2-webflux-eureka.json")); + Collection rms = RequestMappingsParser20.parse(new JSONObject(json)); + assertEquals(6, rms.size()); + + ImmutableSet expected = ImmutableSet.of( + "/actuator", + "/actuator/health", + "/actuator/info", + "/welcome" + ); + assertEquals(expected, + rms.stream() + .flatMap(rm -> Arrays.stream(rm.getSplitPath())) + .collect(Collectors.toSet()) + ); + + } } diff --git a/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-web-eureka.json b/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-web-eureka.json new file mode 100644 index 000000000..a69ce71c4 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-web-eureka.json @@ -0,0 +1,270 @@ +{ + "contexts": { + "welcome-messages-1": { + "mappings": { + "dispatcherServlets": { + "dispatcherServlet": [ + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@22fd98a9]]", + "predicate": "/**/favicon.ico" + }, + { + "handler": "public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map)", + "predicate": "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/health" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" + } + } + }, + { + "handler": "public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map)", + "predicate": "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/info" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;" + } + } + }, + { + "handler": "protected java.util.Map> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)", + "predicate": "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "links", + "className": "org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Ljava/util/Map;" + } + } + }, + { + "handler": "public com.example.demo.Greeting com.example.demo.WelcomeMessageServiceApplication.greeting()", + "predicate": "{[/welcome],methods=[GET]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/welcome" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "greeting", + "className": "com.example.demo.WelcomeMessageServiceApplication", + "descriptor": "()Lcom/example/demo/Greeting;" + } + } + }, + { + "handler": "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)", + "predicate": "{[/error]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/error" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "error", + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;)Lorg/springframework/http/ResponseEntity;" + } + } + }, + { + "handler": "public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)", + "predicate": "{[/error],produces=[text/html]}", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [], + "patterns": [ + "/error" + ], + "produces": [ + { + "negated": false, + "mediaType": "text/html" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "errorHtml", + "className": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController", + "descriptor": "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Lorg/springframework/web/servlet/ModelAndView;" + } + } + }, + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@474c937f]]", + "predicate": "/webjars/**" + }, + { + "handler": "ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@6a7b18f7]]", + "predicate": "/**" + } + ] + }, + "servletFilters": [ + { + "name": "webMvcMetricsFilter", + "className": "org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "requestContextFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "Tomcat WebSocket (JSR356) Filter", + "className": "org.apache.tomcat.websocket.server.WsFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "httpPutFormContentFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "hiddenHttpMethodFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "characterEncodingFilter", + "className": "org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + }, + { + "name": "httpTraceFilter", + "className": "org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter", + "urlPatternMappings": [ + "/*" + ], + "servletNameMappings": [] + } + ], + "servlets": [ + { + "mappings": [], + "name": "default", + "className": "org.apache.catalina.servlets.DefaultServlet" + }, + { + "mappings": [ + "/" + ], + "name": "dispatcherServlet", + "className": "org.springframework.web.servlet.DispatcherServlet" + } + ] + }, + "parentId": "welcome-messages-1" + }, + "bootstrap": { + "mappings": { + "dispatcherServlets": {}, + "servletFilters": [], + "servlets": [] + } + } + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-webflux-eureka.json b/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-webflux-eureka.json new file mode 100644 index 000000000..50419fe7f --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/test/resources/live-rm-beans/rms-boot2-webflux-eureka.json @@ -0,0 +1,145 @@ +{ + "contexts": { + "welcome-messages-1": { + "mappings": { + "dispatcherHandlers": { + "webHandler": [ + { + "predicate": "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/health" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "public org.reactivestreams.Publisher> org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping$ReadOperationHandler.handle(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator/info" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "handle", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Lorg/reactivestreams/Publisher;" + } + } + }, + { + "predicate": "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}", + "handler": "protected java.util.Map> org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping.links(org.springframework.web.server.ServerWebExchange)", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/actuator" + ], + "produces": [ + { + "negated": false, + "mediaType": "application/vnd.spring-boot.actuator.v2+json" + }, + { + "negated": false, + "mediaType": "application/json" + } + ], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "links", + "className": "org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping", + "descriptor": "(Lorg/springframework/web/server/ServerWebExchange;)Ljava/util/Map;" + } + } + }, + { + "predicate": "{[/welcome],methods=[GET]}", + "handler": "public com.example.demo.Greeting com.example.demo.WelcomeMessageServiceApplication.greeting()", + "details": { + "requestMappingConditions": { + "headers": [], + "methods": [ + "GET" + ], + "patterns": [ + "/welcome" + ], + "produces": [], + "params": [], + "consumes": [] + }, + "handlerMethod": { + "name": "greeting", + "className": "com.example.demo.WelcomeMessageServiceApplication", + "descriptor": "()Lcom/example/demo/Greeting;" + } + } + }, + { + "predicate": "/webjars/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@7030aee8]]" + }, + { + "predicate": "/**", + "handler": "ResourceWebHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/]], resolvers=[org.springframework.web.reactive.resource.PathResourceResolver@2c2efafe]]" + } + ] + } + }, + "parentId": "welcome-messages-1" + }, + "bootstrap": { + "mappings": { + "dispatcherHandlers": {} + } + } + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index fbc1bbbd3..65a7a4564 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -26,6 +26,7 @@ import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; +import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor; import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; @@ -63,7 +64,6 @@ import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog; import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor; import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider; import org.springframework.ide.vscode.boot.java.value.ValuePropertyReferencesProvider; -import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; @@ -73,6 +73,7 @@ import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserve import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @@ -265,48 +266,61 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent protected ICompletionEngine createCompletionEngine( JavaProjectFinder javaProjectFinder, SpringPropertyIndexProvider indexProvider, - ProjectBasedPropertyIndexProvider adHocIndexProvider - ) { + ProjectBasedPropertyIndexProvider adHocIndexProvider) { + Map providers = new HashMap<>(); providers.put(org.springframework.ide.vscode.boot.java.scope.Constants.SPRING_SCOPE, new ScopeCompletionProcessor()); providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocIndexProvider)); + providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor()); - JavaSnippetManager snippetManager = new JavaSnippetManager(server::createSnippetBuilder); - snippetManager.add( - new JavaSnippet("RequestMapping method", JavaSnippetContext.BOOT_MEMBERS, CompletionItemKind.Method, - ImmutableList.of("org.springframework.web.bind.annotation.RequestMapping", - "org.springframework.web.bind.annotation.RequestMethod", - "org.springframework.web.bind.annotation.RequestParam"), - "@RequestMapping(value=\"${path}\", method=RequestMethod.${GET})\n" - + "public ${SomeData} ${requestMethodName}(@RequestParam ${String} ${param}) {\n" - + " return new ${SomeData}(${cursor});\n" + "}\n")); - snippetManager - .add(new JavaSnippet("GetMapping method", JavaSnippetContext.BOOT_MEMBERS, CompletionItemKind.Method, - ImmutableList.of("org.springframework.web.bind.annotation.GetMapping", - "org.springframework.web.bind.annotation.RequestParam"), - "@GetMapping(value=\"${path}\")\n" - + "public ${SomeData} ${getMethodName}(@RequestParam ${String} ${param}) {\n" - + " return new ${SomeData}(${cursor});\n" + "}\n")); - snippetManager.add(new JavaSnippet("PostMapping method", JavaSnippetContext.BOOT_MEMBERS, - CompletionItemKind.Method, - ImmutableList.of("org.springframework.web.bind.annotation.PostMapping", - "org.springframework.web.bind.annotation.RequestBody"), - "@PostMapping(value=\"${path}\")\n" - + "public ${SomeEnityData} ${postMethodName}(@RequestBody ${SomeEnityData} ${entity}) {\n" - + " //TODO: process POST request\n" + " ${cursor}\n" + " return ${entity};\n" + "}\n")); - snippetManager.add(new JavaSnippet("PutMapping method", JavaSnippetContext.BOOT_MEMBERS, - CompletionItemKind.Method, - ImmutableList.of("org.springframework.web.bind.annotation.PutMapping", - "org.springframework.web.bind.annotation.RequestBody", - "org.springframework.web.bind.annotation.PathVariable"), - "@PutMapping(value=\"${path}/{${id}}\")\n" - + "public ${SomeEnityData} ${putMethodName}(@PathVariable ${pvt:String} ${id}, @RequestBody ${SomeEnityData} ${entity}) {\n" - + " //TODO: process PUT request\n" + " ${cursor}\n" + " return ${entity};\n" + "}")); + JavaSnippetManager snippetManager = getSnippets(); return new BootJavaCompletionEngine(this, providers, snippetManager); } + protected JavaSnippetManager getSnippets() { + JavaSnippetManager snippetManager = new JavaSnippetManager(server::createSnippetBuilder); + + // PT 160529904: Eclipse templates are duplicated, due to templates in Eclipse also being contributed by + // STS3 bundle. Therefore do not include templates if client is Eclipse + // TODO: REMOVE this check once STS3 is no longer supported + if (LspClient.currentClient() != LspClient.Client.ECLIPSE) { + snippetManager.add( + new JavaSnippet("RequestMapping method", JavaSnippetContext.BOOT_MEMBERS, CompletionItemKind.Method, + ImmutableList.of("org.springframework.web.bind.annotation.RequestMapping", + "org.springframework.web.bind.annotation.RequestMethod", + "org.springframework.web.bind.annotation.RequestParam"), + "@RequestMapping(value=\"${path}\", method=RequestMethod.${GET})\n" + + "public ${SomeData} ${requestMethodName}(@RequestParam ${String} ${param}) {\n" + + " return new ${SomeData}(${cursor});\n" + "}\n")); + snippetManager + .add(new JavaSnippet("GetMapping method", JavaSnippetContext.BOOT_MEMBERS, CompletionItemKind.Method, + ImmutableList.of("org.springframework.web.bind.annotation.GetMapping", + "org.springframework.web.bind.annotation.RequestParam"), + "@GetMapping(value=\"${path}\")\n" + + "public ${SomeData} ${getMethodName}(@RequestParam ${String} ${param}) {\n" + + " return new ${SomeData}(${cursor});\n" + "}\n")); + snippetManager.add(new JavaSnippet("PostMapping method", JavaSnippetContext.BOOT_MEMBERS, + CompletionItemKind.Method, + ImmutableList.of("org.springframework.web.bind.annotation.PostMapping", + "org.springframework.web.bind.annotation.RequestBody"), + "@PostMapping(value=\"${path}\")\n" + + "public ${SomeEnityData} ${postMethodName}(@RequestBody ${SomeEnityData} ${entity}) {\n" + + " //TODO: process POST request\n" + " ${cursor}\n" + " return ${entity};\n" + "}\n")); + snippetManager.add(new JavaSnippet("PutMapping method", JavaSnippetContext.BOOT_MEMBERS, + CompletionItemKind.Method, + ImmutableList.of("org.springframework.web.bind.annotation.PutMapping", + "org.springframework.web.bind.annotation.RequestBody", + "org.springframework.web.bind.annotation.PathVariable"), + "@PutMapping(value=\"${path}/{${id}}\")\n" + + "public ${SomeEnityData} ${putMethodName}(@PathVariable ${pvt:String} ${id}, @RequestBody ${SomeEnityData} ${entity}) {\n" + + " //TODO: process PUT request\n" + " ${cursor}\n" + " return ${entity};\n" + "}")); + } + + return snippetManager; + } + protected BootJavaHoverProvider createHoverHandler(JavaProjectFinder javaProjectFinder, RunningAppProvider runningAppProvider, SourceLinks sourceLinks) { AnnotationHierarchyAwareLookup providers = new AnnotationHierarchyAwareLookup<>(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index 3f293b3fc..8bb2b3757 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -71,32 +71,30 @@ public class AutowiredHoverProvider implements HoverProvider { } @Override - public Collection getLiveHintCodeLenses(IJavaProject project, Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { + public Collection getLiveHintCodeLenses(IJavaProject project, Annotation annotation, TextDocument doc, + SpringBootApp[] runningApps) { + ImmutableList.Builder builder = ImmutableList.builder(); if (runningApps.length > 0) { LiveBean definedBean = getDefinedBeanForTypeDeclaration(ASTUtils.findDeclaringType(annotation)); - // Annotation is MarkerNode, parent is some field, method, variable declaration node. + // Annotation is MarkerNode, parent is some field, method, variable declaration + // node. ASTNode declarationNode = annotation.getParent(); try { Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); - return getLiveHoverHints(project, declarationNode, hoverRange, runningApps, definedBean); + for (SpringBootApp app : runningApps) { + List relevantBeans = getRelevantAutowiredBeans(project, declarationNode, app, + definedBean); + if (!relevantBeans.isEmpty()) { + builder.addAll(LiveHoverUtils.createCodeLensesForBeans(hoverRange, relevantBeans, + BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, + INLINE_BEANS_STRING_SEPARATOR)); + } + } } catch (BadLocationException e) { log.error("", e); } } - return null; - } - - private Collection getLiveHoverHints(IJavaProject project, ASTNode declarationNode, Range range, - SpringBootApp[] runningApps, LiveBean definedBean) { - if (declarationNode != null && definedBean != null) { - for (SpringBootApp app : runningApps) { - List relevantBeans = getRelevantAutowiredBeans(project, declarationNode, app, definedBean); - if (!relevantBeans.isEmpty()) { - return LiveHoverUtils.createCodeLensesForBeans(range, relevantBeans, BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR); - } - } - } - return null; + return builder.build(); } @Override @@ -125,16 +123,12 @@ public class AutowiredHoverProvider implements HoverProvider { StringBuilder hover = new StringBuilder(); - boolean hasContent = false; - for (SpringBootApp app : runningApps) { List autowiredBeans = getRelevantAutowiredBeans(project, declarationNode, app, definedBean); if (!autowiredBeans.isEmpty()) { - if (!hasContent) { - hasContent = true; - } else { + if (hover.length() > 0) { hover.append(" \n \n"); } createHoverContentForBeans(sourceLinks, project, hover, autowiredBeans); @@ -145,7 +139,7 @@ public class AutowiredHoverProvider implements HoverProvider { } } - if (hasContent) { + if (hover.length() > 0) { return new Hover(ImmutableList.of(Either.forLeft(hover.toString()))); } } @@ -189,7 +183,7 @@ public class AutowiredHoverProvider implements HoverProvider { } @SuppressWarnings("unchecked") - private static List findAutowiredBeans(IJavaProject project, ASTNode declarationNode, Collection beans) { + public static List findAutowiredBeans(IJavaProject project, ASTNode declarationNode, Collection beans) { if (declarationNode instanceof MethodDeclaration) { MethodDeclaration methodDeclaration = (MethodDeclaration)declarationNode; return ((List)methodDeclaration.parameters()).stream() @@ -325,16 +319,54 @@ public class AutowiredHoverProvider implements HoverProvider { } @Override - public Collection getLiveHintCodeLenses(IJavaProject project, MethodDeclaration methodDeclaration, TextDocument doc, - SpringBootApp[] runningApps) { - LiveBean definedBean = getDefinedBeanForImplicitAutowiredConstructor(methodDeclaration); - try { - Range hoverRange = doc.toRange(methodDeclaration.getName().getStartPosition(), methodDeclaration.getName().getLength()); - return getLiveHoverHints(project, methodDeclaration, hoverRange, runningApps, definedBean); - } catch (BadLocationException e) { - log.error("", e); + public Hover provideMethodParameterHover(SingleVariableDeclaration parameter, int offset, TextDocument doc, + IJavaProject project, SpringBootApp[] runningApps) { + MethodDeclaration method = (MethodDeclaration) parameter.getParent(); + LiveBean definedBean = getDefinedBeanForImplicitAutowiredConstructor(method); + Hover hover = provideHover(definedBean, parameter, offset, doc, project, runningApps); + if (hover != null) { + SimpleName name = parameter.getName(); + try { + hover.setRange(doc.toRange(name.getStartPosition(), name.getLength())); + } catch (BadLocationException e) { + log.error("", e); + } } - return null; + return hover; + } + + @Override + public Collection getLiveHintCodeLenses(IJavaProject project, MethodDeclaration methodDeclaration, + TextDocument doc, SpringBootApp[] runningApps) { + ImmutableList.Builder builder = ImmutableList.builder(); + LiveBean definedBean = getDefinedBeanForImplicitAutowiredConstructor(methodDeclaration); + if (definedBean != null) { + try { + Range hoverRange = doc.toRange(methodDeclaration.getName().getStartPosition(), + methodDeclaration.getName().getLength()); + + for (SpringBootApp app : runningApps) { + List relevantBeans = getRelevantAutowiredBeans(project, methodDeclaration, app, + definedBean); + if (!relevantBeans.isEmpty()) { + + // CodeLens for the method + builder.addAll(LiveHoverUtils.createCodeLensesForBeans(hoverRange, relevantBeans, + BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, + INLINE_BEANS_STRING_SEPARATOR)); + + // CodeLenses for the method parameters. Only ranges just to provide a highlight + // for the hover + builder.addAll(LiveHoverUtils.createCodeLensForMethodParameters(app, project, methodDeclaration, doc, relevantBeans)); + + } + } + } catch (BadLocationException e) { + log.error("", e); + } + + } + return builder.build(); } private LiveBean getDefinedBeanForImplicitAutowiredConstructor(MethodDeclaration methodDeclaration) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/Constants.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/Constants.java new file mode 100644 index 000000000..dd4ec8883 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/Constants.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +public class Constants { + + public static final String REPOSITORY_TYPE = "org.springframework.data.repository.Repository"; + +} 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 new file mode 100644 index 000000000..ab0c837f8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.Collection; +import java.util.Optional; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.internal.core.util.ASTNodeFinder; +import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +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.BadLocationException; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.util.StringUtils; + +/** + * @author Martin Lippert + */ +public class DataRepositoryCompletionProcessor implements CompletionProvider { + + @Override + public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, + int offset, IDocument doc, Collection completions) { + } + + @Override + public void provideCompletions(ASTNode node, int offset, IDocument doc, Collection completions) { + TypeDeclaration type = ASTUtils.findDeclaringType(node); + DataRepositoryDefinition repo = getDataRepositoryDefinition(type); + if (repo != null) { + DomainType domainType = repo.getDomainType(); + if (domainType != null) { + + String prefix = ""; + try { + IRegion line = doc.getLineInformationOfOffset(offset); + prefix = doc.get(line.getOffset(), offset - line.getOffset()).trim(); + } catch (BadLocationException e) { + // ignore if there is a problem computing the prefix, continue without prefix + } + + DomainProperty[] properties = domainType.getProperties(); + for (DomainProperty property : properties) { + completions.add(generateCompletionProposal(offset, prefix, repo, property)); + } + } + } + } + + protected ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty) { + StringBuilder label = new StringBuilder(); + label.append("findBy"); + label.append(StringUtils.capitalize(domainProperty.getName())); + label.append("("); + label.append(domainProperty.getType().getSimpleName()); + label.append(" "); + label.append(StringUtils.uncapitalize(domainProperty.getName())); + label.append(");"); + + DocumentEdits edits = new DocumentEdits(null); + + StringBuilder completion = new StringBuilder(); + completion.append("List<"); + completion.append(repoDef.getDomainType().getSimpleName()); + completion.append("> findBy"); + completion.append(StringUtils.capitalize(domainProperty.getName())); + completion.append("("); + completion.append(domainProperty.getType().getSimpleName()); + completion.append(" "); + completion.append(StringUtils.uncapitalize(domainProperty.getName())); + completion.append(");"); + + String filter = label.toString(); + if (prefix != null && label.toString().startsWith(prefix)) { + edits.replace(offset - prefix.length(), offset, completion.toString()); + } + else if (prefix != null && completion.toString().startsWith(prefix)) { + edits.replace(offset - prefix.length(), offset, completion.toString()); + filter = completion.toString(); + } + else { + edits.insert(offset, completion.toString()); + } + + DocumentEdits additionalEdits = new DocumentEdits(null); + return new FindByCompletionProposal(label.toString(), CompletionItemKind.Method, edits, null, null, Optional.of(additionalEdits), filter); + } + + private DataRepositoryDefinition getDataRepositoryDefinition(TypeDeclaration type) { + if (type != null) { + ITypeBinding resolvedType = type.resolveBinding(); + return getDataRepositoryDefinition(type, resolvedType); + } + + return null; + } + + private DataRepositoryDefinition getDataRepositoryDefinition(TypeDeclaration type, ITypeBinding resolvedType) { + if (resolvedType != null) { + + // interface analysis + ITypeBinding[] interfaces = resolvedType.getInterfaces(); + for (ITypeBinding resolvedInterface : interfaces) { + String simplifiedType = null; + if (resolvedInterface.isParameterizedType()) { + simplifiedType = resolvedInterface.getBinaryName(); + } + else { + simplifiedType = resolvedType.getQualifiedName(); + } + + if (Constants.REPOSITORY_TYPE.equals(simplifiedType)) { + DomainType domainType = null; + 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(type, resolvedInterface); + if (repo != null) { + return repo; + } + } + } + + // super type analysis + ITypeBinding superclass = resolvedType.getSuperclass(); + if (superclass != null) { + return getDataRepositoryDefinition(type, 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/DataRepositoryDefinition.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java new file mode 100644 index 000000000..243518032 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +/** + * @author Martin Lippert + */ +public class DataRepositoryDefinition { + + private final DomainType domainType; + + public DataRepositoryDefinition(DomainType domainType) { + super(); + this.domainType = domainType; + } + + public DomainType getDomainType() { + return domainType; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java index e4a5d6bc3..2ec66829b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java @@ -36,8 +36,6 @@ import reactor.util.function.Tuples; * @author Martin Lippert */ public class DataRepositorySymbolProvider implements SymbolProvider { - - private static final String REPOSITORY_TYPE = "org.springframework.data.repository.Repository"; @Override public Collection getSymbols(TypeDeclaration typeDeclaration, TextDocument doc) { @@ -96,10 +94,10 @@ public class DataRepositorySymbolProvider implements SymbolProvider { simplifiedType = resolvedType.getQualifiedName(); } - if (REPOSITORY_TYPE.equals(simplifiedType)) { + if (Constants.REPOSITORY_TYPE.equals(simplifiedType)) { String beanName = getBeanName(typeDeclaration); String beanType = resolvedInterface.getName(); - + String domainType = null; if (resolvedInterface.isParameterizedType()) { ITypeBinding[] typeParameters = resolvedInterface.getTypeArguments(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainProperty.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainProperty.java new file mode 100644 index 000000000..904ac40c4 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainProperty.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +/** + * @author Martin Lippert + */ +public class DomainProperty { + + private final String name; + private final DomainType type; + + public DomainProperty(String name, DomainType type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public DomainType getType() { + return type; + } + +} 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 new file mode 100644 index 000000000..53f99f007 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; + +/** + * @author Martin Lippert + */ +public class DomainType { + + private final String packageName; + private final String fullName; + private final String simpleName; + private DomainProperty[] properties; + + public DomainType(String packageName, String fullName, String simpleName) { + this.packageName = packageName; + this.fullName = fullName; + this.simpleName = simpleName; + } + + public DomainType(ITypeBinding typeBinding) { + this.packageName = typeBinding.getPackage().getName(); + this.fullName = typeBinding.getQualifiedName(); + this.simpleName = typeBinding.getName(); + + if (!this.packageName.startsWith("java")) { + IMethodBinding[] methods = typeBinding.getDeclaredMethods(); + if (methods != null && methods.length > 0) { + List properties = new ArrayList<>(); + + for (IMethodBinding 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()))); + } + } + this.properties = (DomainProperty[]) properties.toArray(new DomainProperty[properties.size()]); + } + else { + this.properties = new DomainProperty[0]; + } + } + } + + public String getPackageName() { + return packageName; + } + + public String getFullName() { + return fullName; + } + + public String getSimpleName() { + return simpleName; + } + + public DomainProperty[] getProperties() { + return properties; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java new file mode 100644 index 000000000..c497eb1a2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.Optional; + +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.InsertTextFormat; +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; +import org.springframework.ide.vscode.commons.util.text.IDocument; + +public class FindByCompletionProposal implements ICompletionProposal { + + private String label; + private CompletionItemKind kind; + private DocumentEdits edits; + private String details; + private Renderable doc; + private Optional additionalEdits; + private String filter; + + public FindByCompletionProposal(String label, CompletionItemKind kind, DocumentEdits edits, String details, + Renderable doc, Optional additionalEdits, String filter) { + super(); + this.label = label; + this.kind = kind; + this.edits = edits; + this.details = details; + this.doc = doc; + this.additionalEdits = additionalEdits; + this.filter = filter; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public CompletionItemKind getKind() { + return kind; + } + + @Override + public DocumentEdits getTextEdit() { + return edits; + } + + @Override + public String getDetail() { + return details; + } + + @Override + public Renderable getDocumentation() { + return doc; + } + + @Override + public Optional getAdditionalEdit() { + return additionalEdits; + } + + @Override + public InsertTextFormat getInsertTextFormat() { + return InsertTextFormat.Snippet; + } + + @Override + public String getFilterText() { + return filter; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java index 0dd720cec..d84abf4a3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java @@ -49,8 +49,9 @@ public class BootJavaCompletionEngine implements ICompletionEngine { if (node != null) { Collection completions = new ArrayList(); - completions.addAll(collectCompletionsForAnnotations(node, offset, document)); - completions.addAll(snippets.getCompletions(document, offset, node, cu)); + collectCompletionsForAnnotations(node, offset, document, completions); + collectCompletions(node, offset, document, completions); + snippets.getCompletions(document, offset, node, cu, completions); return completions; } } @@ -59,7 +60,7 @@ public class BootJavaCompletionEngine implements ICompletionEngine { }); } - private Collection collectCompletionsForAnnotations(ASTNode node, int offset, IDocument doc) { + private void collectCompletionsForAnnotations(ASTNode node, int offset, IDocument doc, Collection completions) { Annotation annotation = null; ASTNode exactNode = node; @@ -75,13 +76,19 @@ public class BootJavaCompletionEngine implements ICompletionEngine { if (qualifiedName != null) { CompletionProvider provider = this.completionProviders.get(qualifiedName); if (provider != null) { - return provider.provideCompletions(exactNode, annotation, type, offset, doc); + provider.provideCompletions(exactNode, annotation, type, offset, doc, completions); } } } } + } - return Collections.emptyList(); + private void collectCompletions(ASTNode node, int offset, TextDocument document, Collection completions) { + if (node != null) { + for (CompletionProvider completionProvider : this.completionProviders.values()) { + completionProvider.provideCompletions(node, offset, document, completions); + } + } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index 584c451bc..8acf4c699 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.handlers; +import java.net.URI; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Optional; @@ -24,6 +25,7 @@ import org.eclipse.jdt.core.dom.NodeFinder; import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.Hover; @@ -101,7 +103,7 @@ public class BootJavaHoverProvider implements HoverHandler { if (!project.isPresent()) return new CodeLens[0]; if (!hasActuatorDependency(project.get())) return new CodeLens[0]; - return server.getCompilationUnitCache().withCompilationUnit(document, cu -> { + return server.getCompilationUnitCache().withCompilationUnit(project.get(), URI.create(document.getUri()), cu -> { Collection result = new LinkedHashSet<>(); try { if (cu != null) { @@ -210,7 +212,7 @@ public class BootJavaHoverProvider implements HoverHandler { private Hover provideHover(TextDocument document, int offset) throws Exception { IJavaProject project = getProject(document).orElse(null); if (project != null) { - return server.getCompilationUnitCache().withCompilationUnit(document, cu -> { + return server.getCompilationUnitCache().withCompilationUnit(project, URI.create(document.getUri()), cu -> { ASTNode node = NodeFinder.perform(cu, offset, 0); if (node != null) { return provideHover(node, offset, document, project); @@ -239,6 +241,23 @@ public class BootJavaHoverProvider implements HoverHandler { return provideHoverForTypeDeclaration(node, (TypeDeclaration) parent, offset, doc, project); } else if (parent instanceof MethodDeclaration) { return provideHoverForMethodDeclaration((MethodDeclaration) parent, offset, doc, project); + } else if (parent instanceof SingleVariableDeclaration && parent.getParent() instanceof MethodDeclaration) { + return provideHoverForMethodParameter((SingleVariableDeclaration) parent, offset, doc, project); + } + } + return null; + } + + private Hover provideHoverForMethodParameter(SingleVariableDeclaration parameter, int offset, TextDocument doc, + IJavaProject project) { + SpringBootApp[] runningApps = getRunningSpringApps(project); + + if (runningApps.length > 0) { + for (HoverProvider provider : this.hoverProviders.getAll()) { + Hover hover = provider.provideMethodParameterHover(parameter, offset, doc, project, runningApps); + if (hover != null) { + return hover; + } } } return null; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/CompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/CompletionProvider.java index ae1ec5148..25ae403cb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/CompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/CompletionProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -23,6 +23,7 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; */ public interface CompletionProvider { - Collection provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, int offset, IDocument doc); + void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, int offset, IDocument doc, Collection completions); + void provideCompletions(ASTNode node, int offset, IDocument doc, Collection completions); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/HoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/HoverProvider.java index 2bbd04600..5b9b9b25c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/HoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/HoverProvider.java @@ -16,6 +16,7 @@ import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.Hover; @@ -31,13 +32,19 @@ public interface HoverProvider { default Hover provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, IJavaProject project, SpringBootApp[] runningApps) { return null; } + default Hover provideHover(ASTNode node, TypeDeclaration typeDeclaration, ITypeBinding type, int offset, TextDocument doc, IJavaProject project, SpringBootApp[] runningApps) { return null; } + default Hover provideHover(MethodDeclaration methodDeclaration, int offset, TextDocument doc, IJavaProject project, SpringBootApp[] runningApps) { return null; } + default Hover provideMethodParameterHover(SingleVariableDeclaration parameter, int offset, TextDocument doc, IJavaProject project, SpringBootApp[] runningApps) { + return null; + } + default Collection getLiveHintCodeLenses(IJavaProject project, Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { return null; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index a5bab7e44..29bd1e5d4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -10,7 +10,6 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -65,7 +64,7 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider if (Stream.of(runningApps).anyMatch(app -> LiveHoverUtils.hasRelevantBeans(app, definedBean))) { Optional nameRange = ASTUtils.nameRange(doc, annotation); if (nameRange.isPresent()) { - List codeLenses = assembleCodeLenses(project, runningApps, definedBean, nameRange.get(), annotation); + List codeLenses = assembleCodeLenses(project, runningApps, definedBean, doc, nameRange.get(), annotation); return codeLenses.isEmpty() ? ImmutableList.of(new CodeLens(nameRange.get())) : codeLenses; } } @@ -84,7 +83,7 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider LiveBean definedBean = getDefinedBean(annotation); if (definedBean != null) { - Hover hover = assembleHover(project, runningApps, definedBean, annotation); + Hover hover = assembleHover(project, runningApps, definedBean, annotation, true, true); if (hover != null) { Optional nameRange = ASTUtils.nameRange(doc, annotation); if (nameRange.isPresent()) { @@ -97,77 +96,84 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider return null; } - protected List assembleCodeLenses(IJavaProject project, SpringBootApp[] runningApps, LiveBean definedBean, Range range, ASTNode astNode) { - List codeLensList = new ArrayList<>(); - for (SpringBootApp app : runningApps) { - - List relevantBeans = LiveHoverUtils.findRelevantBeans(app, definedBean); - - if (!relevantBeans.isEmpty()) { - List injectedBeans = getRelevantInjectedIntoBeans(project, app, definedBean, relevantBeans); - ImmutableList.Builder builder = ImmutableList.builder(); - if (!injectedBeans.isEmpty()) { - // Break out of the loop. Just look for the first app with injected into beans - List injectedCodeLenses = LiveHoverUtils.createCodeLensesForBeans(range, injectedBeans, BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR); - builder.addAll(injectedCodeLenses.isEmpty() ? ImmutableList.of(new CodeLens(range)) : injectedCodeLenses); - } - - // Wired beans code lenses - List wiredBeans = findWiredBeans(project, app, relevantBeans, astNode); - builder.addAll(LiveHoverUtils.createCodeLensesForBeans(range, wiredBeans, - AutowiredHoverProvider.BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, - INLINE_BEANS_STRING_SEPARATOR)); - - return builder.build(); - } - } - return codeLensList; - } - - protected List findWiredBeans(IJavaProject project, SpringBootApp app, List relevantBeans, ASTNode astNode) { - return Collections.emptyList(); - } - - protected Hover assembleHover(IJavaProject project, SpringBootApp[] runningApps, LiveBean definedBean, ASTNode astNode) { - StringBuilder hover = new StringBuilder(); - - boolean hasContent = false; - + protected List assembleCodeLenses(IJavaProject project, SpringBootApp[] runningApps, LiveBean definedBean, + TextDocument doc, Range range, ASTNode node) { for (SpringBootApp app : runningApps) { List relevantBeans = LiveHoverUtils.findRelevantBeans(app, definedBean); if (!relevantBeans.isEmpty()) { List injectedBeans = getRelevantInjectedIntoBeans(project, app, definedBean, relevantBeans); + ImmutableList.Builder builder = ImmutableList.builder(); + if (!injectedBeans.isEmpty()) { + // Break out of the loop. Just look for the first app with injected into beans + List injectedCodeLenses = LiveHoverUtils.createCodeLensesForBeans(range, injectedBeans, + BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR); + builder.addAll( + injectedCodeLenses.isEmpty() ? ImmutableList.of(new CodeLens(range)) : injectedCodeLenses); + } - if (!hasContent) { - hasContent = true; - } else { + // Wired beans code lenses + List wiredBeans = findWiredBeans(project, app, relevantBeans, node); + builder.addAll(assembleCodeLenseForAutowired(wiredBeans, project, app, doc, range, node)); + + List codeLenses = builder.build(); + return codeLenses.isEmpty() ? ImmutableList.of(new CodeLens(range)) : codeLenses; + } + } + return ImmutableList.of(); + } + + protected List assembleCodeLenseForAutowired(List wiredBeans, IJavaProject project, SpringBootApp app, TextDocument doc, Range nameRange, ASTNode astNode) { + return LiveHoverUtils.createCodeLensesForBeans(nameRange, wiredBeans, + AutowiredHoverProvider.BEANS_PREFIX_PLAIN_TEXT, MAX_INLINE_BEANS_STRING_LENGTH, + INLINE_BEANS_STRING_SEPARATOR); + } + + protected List findWiredBeans(IJavaProject project, SpringBootApp app, List relevantBeans, ASTNode astNode) { + return Collections.emptyList(); + } + + protected Hover assembleHover(IJavaProject project, SpringBootApp[] runningApps, LiveBean definedBean, ASTNode astNode, boolean injected, boolean wired) { + StringBuilder hover = new StringBuilder(); + + for (SpringBootApp app : runningApps) { + + List relevantBeans = LiveHoverUtils.findRelevantBeans(app, definedBean); + + if (!relevantBeans.isEmpty()) { + if (hover.length() > 0) { hover.append(" \n \n"); } - if (!injectedBeans.isEmpty()) { - hover.append("**"); - hover.append(LiveHoverUtils.createBeansTitleMarkdown(sourceLinks, project, injectedBeans, BEANS_PREFIX_MARKDOWN, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR)); - hover.append("**\n"); - hover.append(injectedBeans.stream() - .map(b -> "- " + LiveHoverUtils.showBeanWithResource(sourceLinks, b, " ", project)) - .collect(Collectors.joining("\n"))); - hover.append("\n \n"); - } - List wiredBeans = findWiredBeans(project, app, relevantBeans, astNode); - if (!wiredBeans.isEmpty()) { - AutowiredHoverProvider.createHoverContentForBeans(sourceLinks, project, hover, wiredBeans); + if (injected) { + List injectedBeans = getRelevantInjectedIntoBeans(project, app, definedBean, relevantBeans); + if (!injectedBeans.isEmpty()) { + hover.append("**"); + hover.append(LiveHoverUtils.createBeansTitleMarkdown(sourceLinks, project, injectedBeans, BEANS_PREFIX_MARKDOWN, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR)); + hover.append("**\n"); + hover.append(injectedBeans.stream() + .map(b -> "- " + LiveHoverUtils.showBeanWithResource(sourceLinks, b, " ", project)) + .collect(Collectors.joining("\n"))); + hover.append("\n \n"); + } } - hover.append("Bean id: `"); - hover.append(definedBean.getId()); - hover.append("` \n"); - hover.append(LiveHoverUtils.niceAppName(app)); + if (wired) { + List wiredBeans = findWiredBeans(project, app, relevantBeans, astNode); + if (!wiredBeans.isEmpty()) { + AutowiredHoverProvider.createHoverContentForBeans(sourceLinks, project, hover, wiredBeans); + } + + hover.append("Bean id: `"); + hover.append(definedBean.getId()); + hover.append("` \n"); + hover.append(LiveHoverUtils.niceAppName(app)); + } } } - if (hasContent) { + if (hover.length() > 0) { return new Hover(ImmutableList.of(Either.forLeft(hover.toString()))); } else { return null; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java index c13ab5b42..3ed8e2cf5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java @@ -16,7 +16,16 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.Range; +import org.gradle.internal.impldep.com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; @@ -24,9 +33,12 @@ import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.Optionals; +import org.springframework.ide.vscode.commons.util.text.TextDocument; public class BeanInjectedIntoHoverProvider extends AbstractInjectedIntoHoverProvider { + private static final Logger log = LoggerFactory.getLogger(BeanInjectedIntoHoverProvider.class); + public BeanInjectedIntoHoverProvider(SourceLinks sourceLinks) { super(sourceLinks); } @@ -75,12 +87,79 @@ public class BeanInjectedIntoHoverProvider extends AbstractInjectedIntoHoverProv @Override protected List findWiredBeans(IJavaProject project, SpringBootApp app, List relevantBeans, ASTNode astNode) { if (astNode instanceof Annotation) { + // @Bean annotation case MethodDeclaration beanMethod = ASTUtils.getAnnotatedMethod((Annotation) astNode); if (beanMethod != null) { return AutowiredHoverProvider.getRelevantAutowiredBeans(project, beanMethod, app, relevantBeans); } + } else if (astNode instanceof SingleVariableDeclaration) { + // Bean method parameter case + return AutowiredHoverProvider.getRelevantAutowiredBeans(project, astNode, app, relevantBeans); } return Collections.emptyList(); } + @Override + protected List assembleCodeLenseForAutowired(List wiredBeans, IJavaProject project, + SpringBootApp app, TextDocument doc, Range nameRange, ASTNode astNode) { + ImmutableList.Builder builder = ImmutableList.builder(); + + // Code lens for the @Bean annotation + builder.addAll(super.assembleCodeLenseForAutowired(wiredBeans, project, app, doc, nameRange, astNode)); + + if (astNode instanceof Annotation) { + // Add code lenses for method parameters + MethodDeclaration beanMethod = ASTUtils.getAnnotatedMethod((Annotation) astNode); + if (beanMethod != null) { + builder.addAll(LiveHoverUtils.createCodeLensForMethodParameters(app, project, beanMethod, doc, wiredBeans)); + } + } + + return builder.build(); + } + + @Override + public Hover provideMethodParameterHover(SingleVariableDeclaration parameter, int offset, TextDocument doc, + IJavaProject project, SpringBootApp[] runningApps) { + try { + if (runningApps.length > 0) { + Range range = ASTUtils.nodeRegion(doc, parameter.getName()).asRange(); + MethodDeclaration method = (MethodDeclaration) parameter.getParent(); + Annotation beanAnnotation = getBeanAnnotation(method); + if (beanAnnotation != null) { + LiveBean definedBean = getDefinedBean(beanAnnotation); + if (definedBean != null) { + Hover hover = assembleHover(project, runningApps, definedBean, parameter, false, true); + if (hover != null) { + hover.setRange(range); + } + return hover; + } + + } + } + } catch (Exception e) { + log.error("", e); + } + return null; + } + + private static Annotation getBeanAnnotation(MethodDeclaration method) { + List modifiers = method.modifiers(); + for (Object modifier : modifiers) { + if (modifier instanceof Annotation) { + Annotation annotation = (Annotation) modifier; + ITypeBinding typeBinding = annotation.resolveTypeBinding(); + if (typeBinding != null) { + String fqName = typeBinding.getQualifiedName(); + if (Annotations.BEAN.equals(fqName)) { + return annotation; + } + } + } + } + return null; + } + + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 76d555d63..54326c8ce 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -111,7 +111,7 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP if (Stream.of(runningApps).anyMatch(app -> LiveHoverUtils.hasRelevantBeans(app, definedBean))) { Optional nameRange = Optional.of(ASTUtils.nodeRegion(doc, typeDeclaration.getName()).asRange()); if (nameRange.isPresent()) { - List codeLenses = assembleCodeLenses(project, runningApps, definedBean, nameRange.get(), typeDeclaration); + List codeLenses = assembleCodeLenses(project, runningApps, definedBean, doc, nameRange.get(), typeDeclaration); return codeLenses.isEmpty() ? ImmutableList.of(new CodeLens(nameRange.get())) : codeLenses; } } @@ -131,7 +131,7 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP LiveBean definedBean = getDefinedBeanForType(typeDeclaration, null); if (definedBean != null) { - Hover hover = assembleHover(project, runningApps, definedBean, typeDeclaration); + Hover hover = assembleHover(project, runningApps, definedBean, typeDeclaration, true, true); if (hover != null) { SimpleName name = typeDeclaration.getName(); try { @@ -146,8 +146,6 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP return null; } - - @Override protected List findWiredBeans(IJavaProject project, SpringBootApp app, List relevantBeans, ASTNode astNode) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index 75c0ba6e3..9873ee7de 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -17,22 +17,32 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.Range; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.SpringResource; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; +import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; public class LiveHoverUtils { + private static final Logger log = LoggerFactory.getLogger(LiveHoverUtils.class); + public static final LiveBean CANT_MATCH_PROPER_BEAN = LiveBean.builder().id("UNKNOWN").build(); public static String showBean(LiveBean bean) { @@ -161,7 +171,7 @@ public class LiveHoverUtils { try { return niceAppName(app.getProcessID(), app.getProcessName()); } catch (Exception e) { - e.printStackTrace(); + log.error("", e); return app.toString(); } } @@ -189,6 +199,27 @@ public class LiveHoverUtils { } + @SuppressWarnings("unchecked") + public static List createCodeLensForMethodParameters(SpringBootApp app, IJavaProject project, MethodDeclaration method, TextDocument doc, List wiredBeans) { + ImmutableList.Builder builder = ImmutableList.builder(); + method.parameters().forEach(p -> { + if (p instanceof SingleVariableDeclaration) { + SingleVariableDeclaration parameter = (SingleVariableDeclaration) p; + List parameterMatchingBean = AutowiredHoverProvider.findAutowiredBeans(project, parameter, wiredBeans); + if (parameterMatchingBean.size() == 0) { + log.warn("No Live Bean matching parameter `" + parameter.getName().getIdentifier() + " for method " + method); + } else { + try { + builder.add(new CodeLens(ASTUtils.nodeRegion(doc, parameter.getName()).asRange())); + } catch (BadLocationException e) { + // ignore + } + } + } + }); + return builder.build(); + } + public static StringBuilder createBeansTitlePlainText(Collection beans, String prefix, int maxInlineBeansStringLength, String beansSeparator) { StringBuilder sb = new StringBuilder(prefix); if (LiveHoverUtils.doBeansFitInline(beans, maxInlineBeansStringLength, beansSeparator)) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/scope/ScopeCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/scope/ScopeCompletionProcessor.java index c997ef076..d61ec295b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/scope/ScopeCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/scope/ScopeCompletionProcessor.java @@ -10,9 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.scope; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; @@ -30,10 +28,8 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; public class ScopeCompletionProcessor implements CompletionProvider { @Override - public Collection provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, - int offset, IDocument doc) { - - List result = new ArrayList<>(); + public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, + int offset, IDocument doc, Collection completions) { try { if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair) { @@ -43,7 +39,7 @@ public class ScopeCompletionProcessor implements CompletionProvider { if ("value".equals(memberPair.getName().toString()) && memberPair.getValue().toString().equals("$missing$")) { for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) { ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, ""); - result.add(proposal); + completions.add(proposal); } } } @@ -51,7 +47,7 @@ public class ScopeCompletionProcessor implements CompletionProvider { else if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) { ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, ""); - result.add(proposal); + completions.add(proposal); } } else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { @@ -61,7 +57,7 @@ public class ScopeCompletionProcessor implements CompletionProvider { for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) { if (completion.getValue().startsWith(prefix)) { ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix); - result.add(proposal); + completions.add(proposal); } } } @@ -75,7 +71,7 @@ public class ScopeCompletionProcessor implements CompletionProvider { for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) { if (completion.getValue().startsWith(prefix)) { ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix); - result.add(proposal); + completions.add(proposal); } } } @@ -84,8 +80,10 @@ public class ScopeCompletionProcessor implements CompletionProvider { catch (Exception e) { e.printStackTrace(); } + } - return result; + @Override + public void provideCompletions(ASTNode node, int offset, IDocument doc, Collection completions) { } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java index bbfd6d5bc..b3bc1d630 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -47,9 +47,7 @@ public class JavaSnippetManager { } - public Collection getCompletions(IDocument doc, int offset, ASTNode node, CompilationUnit cu) { - Collection completions = new ArrayList<>(); - + public void getCompletions(IDocument doc, int offset, ASTNode node, CompilationUnit cu, Collection completions) { DocumentRegion query = PREFIX_FINDER.getPrefixRegion(doc, offset); for (JavaSnippet javaSnippet : snippets) { @@ -58,8 +56,6 @@ public class JavaSnippetManager { .ifPresent((completion) -> completions.add(completion)); } } - - return completions; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java index 033d89e80..3b93cadb8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueCompletionProcessor.java @@ -12,7 +12,6 @@ package org.springframework.ide.vscode.boot.java.value; import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -56,10 +55,8 @@ public class ValueCompletionProcessor implements CompletionProvider { } @Override - public Collection provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, - int offset, IDocument doc) { - - List result = new ArrayList<>(); + public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, + int offset, IDocument doc, Collection completions) { try { // case: @Value(<*>) @@ -76,40 +73,42 @@ public class ValueCompletionProcessor implements CompletionProvider { // if it cannot resolve it. If sending this as plain text, then insertion happens correctly ValuePropertyKeyProposal proposal = new ValuePropertyKeyProposal(edits, match, InsertTextFormat.PlainText); - result.add(proposal); + completions.add(proposal); } } // case: @Value(prefix<*>) else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { - computeProposalsForSimpleName(node, result, offset, doc); + computeProposalsForSimpleName(node, completions, offset, doc); } // case: @Value(value=<*>) else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { - computeProposalsForSimpleName(node, result, offset, doc); + computeProposalsForSimpleName(node, completions, offset, doc); } // case: @Value("prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(node, result, offset, doc); + computeProposalsForStringLiteral(node, completions, offset, doc); } } // case: @Value(value="prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(node, result, offset, doc); + computeProposalsForStringLiteral(node, completions, offset, doc); } } } catch (Exception e) { e.printStackTrace(); } - - return result; } - private void computeProposalsForSimpleName(ASTNode node, List completions, int offset, + @Override + public void provideCompletions(ASTNode node, int offset, IDocument doc, Collection completions) { + } + + private void computeProposalsForSimpleName(ASTNode node, Collection completions, int offset, IDocument doc) { String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); @@ -132,7 +131,7 @@ public class ValueCompletionProcessor implements CompletionProvider { } } - private void computeProposalsForStringLiteral(ASTNode node, List completions, int offset, + private void computeProposalsForStringLiteral(ASTNode node, Collection completions, int offset, IDocument doc) throws BadLocationException { String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1)); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java index 8f5d18559..bff49b62b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java @@ -507,7 +507,7 @@ public class AutowiredHoverProviderTest { "}\n" ); - editor.assertHighlights("@Component", "SomeComponent"); + editor.assertHighlights("@Component", "SomeComponent", "depA", "depB"); editor.assertTrimmedHover("SomeComponent", 2, "**← `DependencyA` `DependencyB`**\n" + @@ -519,6 +519,25 @@ public class AutowiredHoverProviderTest { "Bean id: `someComponent` \n" + "Process [PID=111, name=`the-app`]\n" ); + + editor.assertTrimmedHover("depA", 2, + "**← `DependencyA`**\n" + + "- Bean: `dependencyA` \n" + + " Type: `com.example.DependencyA`\n" + + " \n" + + "Bean id: `someComponent` \n" + + "Process [PID=111, name=`the-app`]\n" + ); + + editor.assertTrimmedHover("depB", 2, + "**← `DependencyB`**\n" + + "- Bean: `dependencyB` \n" + + " Type: `com.example.DependencyB`\n" + + " \n" + + "Bean id: `someComponent` \n" + + "Process [PID=111, name=`the-app`]\n" + ); + } @Test @@ -718,7 +737,7 @@ public class AutowiredHoverProviderTest { " private IDependency a;\n" + " private IDependency b;\n" + "\n" + - " public SomeComponent(@Qualifier(\"dependencyA\") IDependency a, @Qualifier(\"dependencyB\") IDependency b) {\n" + + " public SomeComponent(@Qualifier(\"dependencyA\") IDependency depA, @Qualifier(\"dependencyB\") IDependency depB) {\n" + " this.a = a;\n" + " this.b = b;\n" + " }\n" + @@ -726,7 +745,7 @@ public class AutowiredHoverProviderTest { "}\n" ); - editor.assertHighlights("@Component", "SomeComponent"); + editor.assertHighlights("@Component", "SomeComponent", "depA", "depB"); editor.assertTrimmedHover("SomeComponent", 2, "**← `DependencyA` `DependencyB`**\n" + "- Bean: `dependencyA` \n" + @@ -737,6 +756,25 @@ public class AutowiredHoverProviderTest { "Bean id: `someComponent` \n" + "Process [PID=111, name=`the-app`]\n" ); + + editor.assertTrimmedHover("depA", + "**← `DependencyA`**\n" + + "- Bean: `dependencyA` \n" + + " Type: `com.example.DependencyA`\n" + + " \n" + + "Bean id: `someComponent` \n" + + "Process [PID=111, name=`the-app`]\n" + ); + + editor.assertTrimmedHover("depB", + "**← `DependencyB`**\n" + + "- Bean: `dependencyB` \n" + + " Type: `com.example.DependencyB`\n" + + " \n" + + "Bean id: `someComponent` \n" + + "Process [PID=111, name=`the-app`]\n" + ); + } @Test 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 new file mode 100644 index 000000000..3539fc9e1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2018 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data.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.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; + +/** + * @author Martin Lippert + */ +@RunWith(SpringRunner.class) +@BootLanguageServerTest +@Import(HoverTestConf.class) +public class DataRepositoryCompletionProcessorTest { + + @Autowired private BootLanguageServerHarness harness; + private Editor editor; + + @Before + public void setup() throws Exception { + IJavaProject testProject = ProjectsHarness.INSTANCE.mavenProject("test-spring-data-symbols"); + harness.useProject(testProject); + harness.intialize(null); + } + + @Test + public void testStandardFindByCompletions() throws Exception { + prepareCase("{", "{<*>"); + assertContainsAnnotationCompletions( + "List findByFirstName(String firstName);", + "List findByLastName(String lastName);"); + } + + private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java"); + String content = IOUtils.toString(resource); + + content = content.replace(selectedAnnotation, annotationStatementBeforeTest); + editor = new Editor(harness, content, LanguageId.JAVA); + } + + private void assertContainsAnnotationCompletions(String... expectedResultsFromCompletion) throws Exception { + List completions = editor.getCompletions(); + + int i = 0; + for (CompletionItem foundCompletion : completions) { + Editor clonedEditor = editor.clone(); + clonedEditor.apply(foundCompletion); + + if (clonedEditor.getText().contains(expectedResultsFromCompletion[i])) { + i++; + } + } + + assertEquals(expectedResultsFromCompletion.length, i); + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java index f79d04f30..cc7f50fd0 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositorySymbolProviderTest.java @@ -63,7 +63,7 @@ public class DataRepositorySymbolProviderTest { } @Test - public void testSimpleReppositorySymbol() throws Exception { + public void testSimpleRepositorySymbol() throws Exception { String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString(); List symbols = indexer.getSymbols(docUri); assertEquals(1, symbols.size()); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java index 60da3571c..6eecae621 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java @@ -238,6 +238,80 @@ public class BeanInjectedIntoHoverProviderTest { ); } + @Test + public void beanWithOneInjectionAndWiring() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("hello.FooImplementation") + .dependencies("message") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("hello.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("message") + .type("java.lang.String") + .build() + ) + .add(LiveBean.builder() + .id("irrelevantBean") + .type("com.example.IrrelevantBean") + .dependencies("myController") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo(String msg) {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + // !!! 2 highlights over @Bean. 1 for injected beans CodeLens, 1 for wired beans CodeLens + editor.assertHighlights("@Bean", "@Bean", "msg"); + editor.assertTrimmedHover("@Bean", + "**→ `MyController`**\n" + + "- Bean: `myController` \n" + + " Type: `hello.MyController`\n" + + " \n" + + "**← `String`**\n" + + "- Bean: `message` \n" + + " Type: `java.lang.String`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + editor.assertTrimmedHover("msg", + "**← `String`**\n" + + "- Bean: `message` \n" + + " Type: `java.lang.String`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + } + @Test public void beanFromInnerClassWithOneInjection() throws Exception { LiveBeansModel beans = LiveBeansModel.builder() @@ -549,12 +623,12 @@ public class BeanInjectedIntoHoverProviderTest { "public class LocalConfig {\n" + " \n" + " @Bean(\"fooImplementation\")\n" + - " Foo someFoo(DependencyA a) {\n" + + " Foo someFoo(DependencyA depA) {\n" + " return new FooImplementation();\n" + " }\n" + "}" ); - editor.assertHighlights("@Bean"); + editor.assertHighlights("@Bean", "depA"); editor.assertTrimmedHover("@Bean", "**← `DependencyA`**\n" + "- Bean: `depA` \n" + @@ -563,6 +637,16 @@ public class BeanInjectedIntoHoverProviderTest { "Bean id: `fooImplementation` \n" + "Process [PID=111, name=`the-app`]" ); + + editor.assertTrimmedHover("depA", + "**← `DependencyA`**\n" + + "- Bean: `depA` \n" + + " Type: `hello.DependencyA`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + } @Test @@ -603,12 +687,12 @@ public class BeanInjectedIntoHoverProviderTest { "public class LocalConfig {\n" + " \n" + " @Bean(\"fooImplementation\")\n" + - " Foo someFoo(DependencyA a, DependencyB b) {\n" + + " Foo someFoo(DependencyA depA, DependencyB depB) {\n" + " return new FooImplementation();\n" + " }\n" + "}" ); - editor.assertHighlights("@Bean"); + editor.assertHighlights("@Bean", "depA", "depB"); editor.assertTrimmedHover("@Bean", "**← `DependencyA` `DependencyB`**\n" + "- Bean: `depA` \n" + @@ -619,6 +703,25 @@ public class BeanInjectedIntoHoverProviderTest { "Bean id: `fooImplementation` \n" + "Process [PID=111, name=`the-app`]" ); + + editor.assertTrimmedHover("depA", + "**← `DependencyA`**\n" + + "- Bean: `depA` \n" + + " Type: `hello.DependencyA`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + + editor.assertTrimmedHover("depB", + "**← `DependencyB`**\n" + + "- Bean: `depB` \n" + + " Type: `hello.DependencyB`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + } @Test @@ -664,7 +767,7 @@ public class BeanInjectedIntoHoverProviderTest { " }\n" + "}" ); - editor.assertHighlights("@Bean"); + editor.assertHighlights("@Bean", "deps"); editor.assertTrimmedHover("@Bean", "**← `DependencyA` `DependencyB`**\n" + "- Bean: `depA` \n" + @@ -675,7 +778,18 @@ public class BeanInjectedIntoHoverProviderTest { "Bean id: `fooImplementation` \n" + "Process [PID=111, name=`the-app`]" ); - } + + editor.assertTrimmedHover("deps", + "**← `DependencyA` `DependencyB`**\n" + + "- Bean: `depA` \n" + + " Type: `hello.DependencyA`\n" + + "- Bean: `depB` \n" + + " Type: `hello.DependencyB`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); +} @Test public void beanWithQualifierWiring() throws Exception { @@ -715,7 +829,7 @@ public class BeanInjectedIntoHoverProviderTest { " }\n" + "}" ); - editor.assertHighlights("@Bean"); + editor.assertHighlights("@Bean", "deps"); editor.assertTrimmedHover("@Bean", "**← `DependencyB`**\n" + "- Bean: `depB` \n" + @@ -724,5 +838,15 @@ public class BeanInjectedIntoHoverProviderTest { "Bean id: `fooImplementation` \n" + "Process [PID=111, name=`the-app`]" ); + + editor.assertTrimmedHover("deps", + "**← `DependencyB`**\n" + + "- Bean: `depB` \n" + + " Type: `hello.DependencyB`\n" + + " \n" + + "Bean id: `fooImplementation` \n" + + "Process [PID=111, name=`the-app`]" + ); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java index 8243defbb..6feea840e 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java @@ -501,7 +501,7 @@ public class ComponentInjectionsHoverProviderTest { " }\n" + "}\n" ); - editor.assertHighlights("@Component", "AutowiredClass"); + editor.assertHighlights("@Component", "AutowiredClass", "depA", "depB"); editor.assertTrimmedHover("@Component", "**← `DependencyA` `DependencyB`**\n" + "- Bean: `dependencyA` \n" + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java new file mode 100644 index 000000000..1da41bae8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java @@ -0,0 +1,8 @@ +package org.test; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +public interface TestCustomerRepositoryForCompletions extends CrudRepository { +}