From 889c2324982b81e1714d9322ddda9fa0ed54cc32 Mon Sep 17 00:00:00 2001 From: vudayani Date: Tue, 24 Sep 2024 20:56:25 +0530 Subject: [PATCH 1/2] GH-1323, GH-1324: Cron Expressions completion proposals and inlay hints --- .../testharness/LanguageServerHarness.java | 8 + .../spring-boot-language-server/pom.xml | 7 + .../BootJavaCompletionEngineConfigurer.java | 7 +- .../ide/vscode/boot/app/JdtConfig.java | 5 + ...nnotationAttributeCompletionProcessor.java | 49 +++-- ...AnnotationAttributeCompletionProvider.java | 12 +- .../CronExpressionCompletionProvider.java | 45 +++++ .../CronExpressionsInlayHintsProvider.java | 149 +++++++++++++++ .../CronExpressionCompletionProviderTest.java | 172 ++++++++++++++++++ ...CronExpressionsInlayHintsProviderTest.java | 119 ++++++++++++ .../src/main/java/org/test/CronSchedular.java | 56 ++++++ .../src/main/java/org/test/Scheduler.java | 12 ++ 12 files changed, 623 insertions(+), 18 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronSchedular.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/Scheduler.java diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index f0e2de13dc..b33c81ab5a 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -79,6 +79,8 @@ import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintParams; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.MarkupContent; @@ -681,6 +683,12 @@ public List getCodeLenses(TextDocumentInfo document) throws params.setTextDocument(document.getId()); return getServer().getTextDocumentService().codeLens(params).get(); } + + public List getInlayHints(TextDocumentInfo document) throws Exception { + InlayHintParams params = new InlayHintParams(); + params.setTextDocument(document.getId()); + return getServer().getTextDocumentService().inlayHint(params).get(); + } public List getDocumentHighlights(TextDocumentIdentifier docId, Position cursor) throws InterruptedException, ExecutionException { return getServer().getTextDocumentService().documentHighlight(new DocumentHighlightParams(docId, cursor)).get(); diff --git a/headless-services/spring-boot-language-server/pom.xml b/headless-services/spring-boot-language-server/pom.xml index 2209558561..e20e3770ff 100644 --- a/headless-services/spring-boot-language-server/pom.xml +++ b/headless-services/spring-boot-language-server/pom.xml @@ -194,6 +194,13 @@ commons-util 1.58.0-SNAPSHOT + + + + com.cronutils + cron-utils + 9.2.0 + diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index eb9c1bc0b2..50b739412a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -30,6 +30,9 @@ import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider; +import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceCompletionProcessor; +import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor; +import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider; import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; @@ -40,8 +43,6 @@ import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor; -import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor; -import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceCompletionProcessor; import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -132,6 +133,8 @@ BootJavaCompletionEngine javaCompletionEngine( providers.put(Annotations.NAMED_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex)))); providers.put(Annotations.NAMED_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex)))); + + providers.put(Annotations.SCHEDULED, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("cron", new CronExpressionCompletionProvider()))); return new BootJavaCompletionEngine(cuCache, providers, snippetManager); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index e4864e0071..e26b51aa36 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.ide.vscode.boot.java.cron.CronExpressionsInlayHintsProvider; import org.springframework.ide.vscode.boot.java.cron.CronReconciler; import org.springframework.ide.vscode.boot.java.cron.CronSemanticTokens; import org.springframework.ide.vscode.boot.java.cron.JdtCronReconciler; @@ -136,6 +137,10 @@ public class JdtConfig { return new JdtDataQueriesInlayHintsProvider(semanticTokensProvider); } + @Bean CronExpressionsInlayHintsProvider cronExpressionsInlayHintsProvider() { + return new CronExpressionsInlayHintsProvider(); + } + @Bean JdtQueryDocHighlightsProvider jdtDocHighlightsProvider(JdtDataQuerySemanticTokensProvider semanticTokensProvider) { return new JdtQueryDocHighlightsProvider(semanticTokensProvider); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java index 304c3a2dd2..27d1573741 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java @@ -28,6 +28,7 @@ import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.StringLiteral; +import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -114,8 +115,9 @@ else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotat /** * create the concrete completion proposal */ - private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, String attributeName, Collection completions, int startOffset, int endOffset, - String filterPrefix, Function createReplacementText) { + private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, String attributeName, + Collection completions, int startOffset, int endOffset, String filterPrefix, + Function createReplacementText) { Set alreadyMentionedValues = alreadyMentionedValues(node); @@ -123,25 +125,42 @@ private void createCompletionProposals(IJavaProject project, TextDocument doc, A if (completionProvider != null) { List candidates = completionProvider.getCompletionCandidates(project); - List filteredCandidates = candidates.stream() + if (completionProvider instanceof CronExpressionCompletionProvider) { + Map proposals = completionProvider.getCompletionCandidatesWithLabels(project); + Map filteredProposals = proposals.entrySet().stream() + .filter(candidate -> candidate.getKey().toLowerCase().contains(filterPrefix.toLowerCase())) + .filter(candidate -> !alreadyMentionedValues.contains(candidate.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + double score = filteredProposals.size(); + for (Map.Entry entry : filteredProposals.entrySet()) { + String candidate = entry.getKey(); + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); + AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, + candidate, entry.getValue(), null, score--); + completions.add(proposal); + + } + } else { + + List filteredCandidates = candidates.stream() // .filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase())) - .filter(candidate -> candidate.toLowerCase().contains(filterPrefix.toLowerCase())) - .filter(candidate -> !alreadyMentionedValues.contains(candidate)) - .collect(Collectors.toList()); + .filter(candidate -> candidate.toLowerCase().contains(filterPrefix.toLowerCase())) + .filter(candidate -> !alreadyMentionedValues.contains(candidate)).collect(Collectors.toList()); + double score = filteredCandidates.size(); + for (String candidate : filteredCandidates) { - double score = filteredCandidates.size(); - for (String candidate : filteredCandidates) { - - DocumentEdits edits = new DocumentEdits(doc, false); - edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); - - AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, candidate, candidate, null, score--); - completions.add(proposal); + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); + + AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, + candidate, candidate, null, score--); + completions.add(proposal); + } } } } - // // internal computation of the right positions, prefixes, etc. // diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java index 2f96e6c69a..16212224ea 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java @@ -10,12 +10,22 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.annotations; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.ide.vscode.commons.java.IJavaProject; public interface AnnotationAttributeCompletionProvider { - List getCompletionCandidates(IJavaProject project); + default List getCompletionCandidates(IJavaProject project) { + return new ArrayList<>(); + } + + + default Map getCompletionCandidatesWithLabels(IJavaProject project) { + return new HashMap<>(); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java new file mode 100644 index 0000000000..684de6bf93 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java @@ -0,0 +1,45 @@ +package org.springframework.ide.vscode.boot.java.cron; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public class CronExpressionCompletionProvider implements AnnotationAttributeCompletionProvider { + + private static final Map CRON_EXPRESSIONS_MAP = new LinkedHashMap<>(); + + static { + CRON_EXPRESSIONS_MAP.put("0 0 * * * 1-5", "every hour every day between Monday and Friday"); + CRON_EXPRESSIONS_MAP.put("0 */5 * * * *", "every 5 minutes"); + CRON_EXPRESSIONS_MAP.put("0 * * * * *", "every minute"); + CRON_EXPRESSIONS_MAP.put("0 0 */6 * * *", "every 6 hours at minute 0"); + CRON_EXPRESSIONS_MAP.put("0 0 * * * *", "every hour"); + CRON_EXPRESSIONS_MAP.put("0 0 * * * SUN", "every hour at Sunday day"); + CRON_EXPRESSIONS_MAP.put("0 0 0 * * *", "at 00:00"); + CRON_EXPRESSIONS_MAP.put("0 0 0 * * SAT,SUN", "at 00:00 on Saturday and Sunday"); + CRON_EXPRESSIONS_MAP.put("0 0 0 * * 6,0", "at 00:00 at Saturday and Sunday days"); + CRON_EXPRESSIONS_MAP.put("0 0 0 1-7 * SUN", "at 00:00 every day between 1 and 7 at Sunday day"); + CRON_EXPRESSIONS_MAP.put("0 0 0 1 * *", "at 00:00 at 1 day"); + CRON_EXPRESSIONS_MAP.put("0 0 0 1 1 *", "at 00:00 at 1 day at January month"); + CRON_EXPRESSIONS_MAP.put("0 0 8-18 * * *", "every hour between 8 and 18"); + CRON_EXPRESSIONS_MAP.put("0 0 9 * * MON", "at 09:00 at Monday day"); + CRON_EXPRESSIONS_MAP.put("0 0 10 * * *", "at 10:00"); + CRON_EXPRESSIONS_MAP.put("0 30 9 * JAN MON", "at 09:30 at January month at Monday day"); + CRON_EXPRESSIONS_MAP.put("10 * * * * *", "every minute at second 10"); + CRON_EXPRESSIONS_MAP.put("0 0 8-10 * * *", "every hour between 8 and 10"); + CRON_EXPRESSIONS_MAP.put("0 0/30 8-10 * * *", "every 30 minutes every hour between 8 and 10"); + CRON_EXPRESSIONS_MAP.put("0 0 0 L * *", " at 00:00 last day of month"); + CRON_EXPRESSIONS_MAP.put("0 0 0 1W * *", "at 00:00 the nearest weekday to the 1 of the month"); + CRON_EXPRESSIONS_MAP.put("0 0 0 * * THUL", "at 00:00 last Thursday of every month"); + CRON_EXPRESSIONS_MAP.put("0 0 0 ? * 5#2", "at 00:00 Friday 2 of every month"); + CRON_EXPRESSIONS_MAP.put("0 0 0 ? * MON#1", "at 00:00 Monday 1 of every month"); + } + + + @Override + public Map getCompletionCandidatesWithLabels(IJavaProject project) { + return CRON_EXPRESSIONS_MAP; + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProvider.java new file mode 100644 index 0000000000..ef926b1fb4 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProvider.java @@ -0,0 +1,149 @@ +package org.springframework.ide.vscode.boot.java.cron; + +import java.util.Locale; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TextBlock; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintKind; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.JdtInlayHintsProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.scheduling.support.CronExpression; + +import com.cronutils.descriptor.CronDescriptor; +import com.cronutils.model.definition.CronDefinition; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.parser.CronParser; + +import static com.cronutils.model.CronType.SPRING; + +public class CronExpressionsInlayHintsProvider implements JdtInlayHintsProvider { + + protected static Logger logger = LoggerFactory.getLogger(CronExpressionsInlayHintsProvider.class); + + private static final String SCHEDULED = "Scheduled"; + + public record EmbeddedCronExpression(Expression expression, String text, int offset) { + }; + + @Override + public boolean isApplicable(IJavaProject project) { + return true; + } + + @Override + public ASTVisitor getInlayHintsComputer(IJavaProject project, TextDocument doc, CompilationUnit cu, + Collector collector) { + return new ASTVisitor() { + + @Override + public boolean visit(NormalAnnotation node) { + EmbeddedCronExpression cron = extractCronExpression(node); + if (cron != null) { + processCron(project, doc, collector, cron, node); + } + return super.visit(node); + } + + @Override + public boolean visit(SingleMemberAnnotation node) { + EmbeddedCronExpression cron = extractCronExpression(node); + if (cron != null) { + processCron(project, doc, collector, cron, node); + } + return super.visit(node); + } + + }; + } + + private void processCron(IJavaProject project, TextDocument doc, Collector collector, + EmbeddedCronExpression cronExp, Annotation node) { + boolean isValidExpression = CronExpression.isValidExpression(cronExp.text()); + + try { + if (isValidExpression) { + CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(SPRING); + CronParser parser = new CronParser(cronDefinition); + CronDescriptor descriptor = CronDescriptor.instance(Locale.US); + String cronDescription = descriptor.describe(parser.parse(cronExp.text().toUpperCase())); + + InlayHint hint = new InlayHint(); + hint.setKind(InlayHintKind.Type); + hint.setLabel(Either.forLeft(cronDescription)); + hint.setTooltip(cronDescription); + hint.setPaddingLeft(true); + hint.setPaddingRight(true); + hint.setPosition(doc.toPosition(node.getStartPosition() + node.getLength())); + collector.accept(hint); + } + } catch (Exception e) { + // ignore + } + } + + public static EmbeddedCronExpression extractCronExpression(SingleMemberAnnotation a) { + if (isScheduledAnnotation(a)) { + EmbeddedCronExpression expression = extractEmbeddedExpression(a.getValue(), a); + return expression == null ? null + : new EmbeddedCronExpression(expression.expression(), expression.text(), expression.offset()); + } + return null; + } + + public static EmbeddedCronExpression extractCronExpression(NormalAnnotation a) { + Expression cronExpression = null; + if (isScheduledAnnotation(a)) { + for (Object value : a.values()) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if ("cron".equals(name)) { + cronExpression = pair.getValue(); + break; + } + } + } + } + if (cronExpression != null) { + EmbeddedCronExpression e = extractEmbeddedExpression(cronExpression, a); + if (e != null) { + return new EmbeddedCronExpression(e.expression(), e.text(), e.offset()); + } + } + return null; + } + + public static EmbeddedCronExpression extractEmbeddedExpression(Expression valueExp, Annotation node) { + String text = null; + int offset = 0; + if (valueExp instanceof StringLiteral sl) { + text = sl.getEscapedValue(); + text = text.substring(1, text.length() - 1); + offset = sl.getStartPosition() + 1; // +1 to skip over opening " + } else if (valueExp instanceof TextBlock tb) { + text = tb.getEscapedValue(); + text = text.substring(3, text.length() - 3).trim(); + offset = tb.getStartPosition() + 3; // +3 to skip over opening """ + } + return text == null ? null : new EmbeddedCronExpression(valueExp, text, offset); + } + + static boolean isScheduledAnnotation(Annotation a) { + return Annotations.SCHEDULED.equals(a.getTypeName().getFullyQualifiedName()) + || SCHEDULED.equals(a.getTypeName().getFullyQualifiedName()); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProviderTest.java new file mode 100644 index 0000000000..bdc85f8dfe --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProviderTest.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.cron; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Udayani V + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class CronExpressionCompletionProviderTest { + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + private String tempJavaDocUri; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotations/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + + } + + @Test + public void testCronExpressionCompletionWithoutPrefix() throws Exception { + assertCompletions("@Scheduled(<*>)", new String[] {}, 0, null); + } + + @Test + public void testCronExpressionCompletionInsideOfQuotesWithoutPrefix() throws Exception { + assertCompletions("@Scheduled(\"<*>\")", new String[] {}, 0, null); + } + + @Test + public void testCronExpressionCompletionWithoutQuotesWithPrefix() throws Exception { + assertCompletions("@Scheduled(0<*>)", new String[] {}, 0, null); + } + + @Test + public void testCronExpressionCompletionInsideOfQuotesWithPrefix() throws Exception { + assertCompletions("@Scheduled(\"10<*>\")", new String[] {}, 0, null); + } + + @Test + public void testCronExpressionCompletionWithoutQuotesWithAttributeName() throws Exception { + assertCompletions("@Scheduled(cron=<*>)", 24, "@Scheduled(cron=\"0 0 * * * *\"<*>)"); + } + + @Test + public void testCronExpressionCompletionWithAttributeNameAndPrefix() throws Exception { + assertCompletions("@Scheduled(cron=\"0<*>\")", 24, "@Scheduled(cron=\"0 0 * * * *<*>\")"); + } + + @Test + public void testCronExpressionCompletionWithFilteredMatches() throws Exception { + assertCompletions("@Scheduled(cron=\"MON<*>\")", 3, "@Scheduled(cron=\"0 0 9 * * MON<*>\")"); + } + + @Test + public void testCronExpressionCompletionPrefixWithFilteredMatches() throws Exception { + assertCompletions("@Scheduled(cron=\"W<*>\")", 1, "@Scheduled(cron=\"0 0 0 1W * *<*>\")"); + } + + @Test + public void testCronExpressionCompletionWithNoMatches() throws Exception { + assertCompletions("@Scheduled(cron=\"WED<*>\")", 0, null); + } + + @Test + public void testCronExpressionCompletionWithMultipleAttributes() throws Exception { + assertCompletions("@Scheduled(cron=\"JAN<*>\", fixedDelay = 1000)", 1, "@Scheduled(cron=\"0 30 9 * JAN MON<*>\", fixedDelay = 1000)"); + } + + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, noOfExpectedCompletions, null, 0, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.scheduling.annotation.Scheduled; + + public class CronScheduler { + + """ + + completionLine + "\n" + + """ + public void cronCompletionTest() { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExcpectedCompletions, completions.size()); + + if (expectedCompletions != null) { + String[] completionItems = completions.stream() + .map(item -> item.getLabel()) + .toArray(size -> new String[size]); + + assertArrayEquals(expectedCompletions, completionItems); + } + + if (noOfExcpectedCompletions > 0) { + editor.apply(completions.get(chosenCompletion)); + assertEquals(""" + package org.test; + + import org.springframework.scheduling.annotation.Scheduled; + + public class CronScheduler { + + """ + expectedCompletedLine + "\n" + + """ + public void cronCompletionTest() { + } + """, editor.getText()); + } + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProviderTest.java new file mode 100644 index 0000000000..0968364ce9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionsInlayHintsProviderTest.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2017, 2024 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.cron; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.TextDocumentInfo; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Udayani V + */ +@SuppressWarnings("deprecation") +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class CronExpressionsInlayHintsProviderTest { + + @Autowired + private BootLanguageServerHarness harness; + @Autowired + private JavaProjectFinder projectFinder; + @Autowired + private SpringSymbolIndex indexer; + private SimpleLanguageServer server; + private File directory; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotations/").toURI()); + String projectDir = directory.toURI().toString(); + server = mock(SimpleLanguageServer.class); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + public void test_cronExpresionInlayHints() throws Exception { + + String docUri = directory.toPath().resolve("src/main/java/org/test/CronSchedular.java").toUri().toString(); + TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId()); + TextDocumentInfo openedDoc = harness.openDocument(doc); + + List inlayHints = harness.getInlayHints(openedDoc); + + assertEquals(9, inlayHints.size()); + + assertTrue(containsInlayHints(inlayHints.get(0), 10, 33, "every hour")); + assertTrue(containsInlayHints(inlayHints.get(1), 15, 33, "every minute at second 01")); + assertTrue(containsInlayHints(inlayHints.get(2), 20, 34, "every minute at second 10")); + assertTrue(containsInlayHints(inlayHints.get(3), 25, 36, "every hour between 8 and 10")); + assertTrue(containsInlayHints(inlayHints.get(4), 30, 36, "at 6 and 19 hours")); + assertTrue(containsInlayHints(inlayHints.get(5), 35, 39, "every 30 minutes every hour between 8 and 10")); + assertTrue(containsInlayHints(inlayHints.get(6), 40, 42, + "every hour between 9 and 17 every day between Monday and Friday")); + assertTrue(containsInlayHints(inlayHints.get(7), 45, 35, "every hour every day between Monday and Friday")); + assertTrue( + containsInlayHints(inlayHints.get(8), 51, 9, "every second between 0 and 59 at 10 minute at 13 hour")); + } + + @Test + public void test_noInlayHintsForInvalidCronExp() throws Exception { + + String docUri = directory.toPath().resolve("src/main/java/org/test/Scheduler.java").toUri().toString(); + TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId()); + TextDocumentInfo openedDoc = harness.openDocument(doc); + + List inlayHints = harness.getInlayHints(openedDoc); + + assertEquals(0, inlayHints.size()); + } + + private boolean containsInlayHints(InlayHint inlayHint, int line, int character, String label) { + Position pos = inlayHint.getPosition(); + String inlayText = inlayHint.getLabel().getLeft(); + if (pos.getLine() == line && pos.getCharacter() == character && inlayText.equals(label) + && inlayText.equals(label)) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronSchedular.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronSchedular.java new file mode 100644 index 0000000000..d4f5252130 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronSchedular.java @@ -0,0 +1,56 @@ +package org.test; + +import org.springframework.scheduling.annotation.Scheduled; + +public class CronScheduler { + + @Scheduled(cron = "") + public void cronCompletionTest() { + } + + @Scheduled(cron = "0 0 * * * *") + public void performTask1() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "1 * * * * *") + public void performTask2() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "10 * * * * *") + public void performTask3() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0 0 8-10 * * *") + public void performTask4() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0 0 6,19 * * *") + public void performTask5() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0 0/30 8-10 * * *") + public void performTask6() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0 0 9-17 * * MON-FRI") + public void performTask7() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0 0 * * * 1-5") + public void performTask8() { + System.out.println("Scheduled task executed"); + } + + @Scheduled(cron = "0-59 10 13 * * *", zone = "UTC", fixedRate = 5000, initialDelay + = 1000) + public void performTask9() { + System.out.println("Scheduled task executed"); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/Scheduler.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/Scheduler.java new file mode 100644 index 0000000000..a02a56cf43 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/Scheduler.java @@ -0,0 +1,12 @@ +package org.test; + +import org.springframework.scheduling.annotation.Scheduled; + +public class Scheduler { + + @Scheduled(cron = "MON 10 13 * * *") + public void invalidExp() { + System.out.println("Scheduled task executed"); + } + +} From 5f3707b30e8b6d592fa5aaa9aa9648749e6528b6 Mon Sep 17 00:00:00 2001 From: vudayani Date: Wed, 25 Sep 2024 21:08:24 +0530 Subject: [PATCH 2/2] refactor AnnotationAttributeCompletionProviders to also return labels --- ...nnotationAttributeCompletionProcessor.java | 47 ++++++------------- ...AnnotationAttributeCompletionProvider.java | 12 +---- .../beans/DependsOnCompletionProcessor.java | 7 +-- .../java/beans/NamedCompletionProvider.java | 7 +-- .../java/beans/ProfileCompletionProvider.java | 7 +-- .../beans/QualifierCompletionProvider.java | 7 +-- .../beans/ResourceCompletionProvider.java | 7 +-- ...ditionalOnResourceCompletionProcessor.java | 11 +++-- .../CronExpressionCompletionProvider.java | 2 +- .../java/scope/ScopeCompletionProcessor.java | 22 ++++----- 10 files changed, 53 insertions(+), 76 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java index 27d1573741..53399807c9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java @@ -28,7 +28,6 @@ import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.StringLiteral; -import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -123,40 +122,22 @@ private void createCompletionProposals(IJavaProject project, TextDocument doc, A AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get(attributeName); if (completionProvider != null) { - List candidates = completionProvider.getCompletionCandidates(project); - - if (completionProvider instanceof CronExpressionCompletionProvider) { - Map proposals = completionProvider.getCompletionCandidatesWithLabels(project); - Map filteredProposals = proposals.entrySet().stream() - .filter(candidate -> candidate.getKey().toLowerCase().contains(filterPrefix.toLowerCase())) - .filter(candidate -> !alreadyMentionedValues.contains(candidate.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - double score = filteredProposals.size(); - for (Map.Entry entry : filteredProposals.entrySet()) { - String candidate = entry.getKey(); - DocumentEdits edits = new DocumentEdits(doc, false); - edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); - AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, - candidate, entry.getValue(), null, score--); - completions.add(proposal); - } - } else { - - List filteredCandidates = candidates.stream() -// .filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase())) - .filter(candidate -> candidate.toLowerCase().contains(filterPrefix.toLowerCase())) - .filter(candidate -> !alreadyMentionedValues.contains(candidate)).collect(Collectors.toList()); - double score = filteredCandidates.size(); - for (String candidate : filteredCandidates) { + Map proposals = completionProvider.getCompletionCandidates(project); + Map filteredProposals = proposals.entrySet().stream() + .filter(candidate -> candidate.getKey().toLowerCase().contains(filterPrefix.toLowerCase())) + .filter(candidate -> !alreadyMentionedValues.contains(candidate.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + double score = filteredProposals.size(); + for (Map.Entry entry : filteredProposals.entrySet()) { + String candidate = entry.getKey(); + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); + + AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, + candidate, entry.getValue(), null, score--); + completions.add(proposal); - DocumentEdits edits = new DocumentEdits(doc, false); - edits.replace(startOffset, endOffset, createReplacementText.apply(candidate)); - - AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, - candidate, candidate, null, score--); - completions.add(proposal); - } } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java index 16212224ea..5a0b6d331e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProvider.java @@ -10,22 +10,12 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.annotations; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.ide.vscode.commons.java.IJavaProject; public interface AnnotationAttributeCompletionProvider { - default List getCompletionCandidates(IJavaProject project) { - return new ArrayList<>(); - } - - - default Map getCompletionCandidatesWithLabels(IJavaProject project) { - return new HashMap<>(); - } + Map getCompletionCandidates(IJavaProject project); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java index 10a4539a13..13bcd3215a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java @@ -11,7 +11,8 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; @@ -235,11 +236,11 @@ public DependsOnCompletionProcessor(SpringMetamodelIndex springIndex) { // } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { return Arrays.stream(this.springIndex.getBeansOfProject(project.getElementName())) .map(bean -> bean.getName()) .distinct() - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java index 895e27099b..7d864ba43b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/NamedCompletionProvider.java @@ -11,7 +11,8 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; @@ -32,7 +33,7 @@ public NamedCompletionProvider(SpringMetamodelIndex springIndex) { } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); @@ -40,7 +41,7 @@ public List getCompletionCandidates(IJavaProject project) { findAllNamedValues(beans), Arrays.stream(beans).map(bean -> bean.getName())) .distinct() - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); } private Stream findAllNamedValues(Bean[] beans) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileCompletionProvider.java index 85cc4d0dc8..47d6860e48 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileCompletionProvider.java @@ -11,7 +11,8 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; @@ -32,13 +33,13 @@ public ProfileCompletionProvider(SpringMetamodelIndex springIndex) { } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); return findAllProfiles(beans) .distinct() - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); } private Stream findAllProfiles(Bean[] beans) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProvider.java index edd49d7ecc..bc6daf4f20 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProvider.java @@ -11,7 +11,8 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; @@ -32,7 +33,7 @@ public QualifierCompletionProvider(SpringMetamodelIndex springIndex) { } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); @@ -40,7 +41,7 @@ public List getCompletionCandidates(IJavaProject project) { findAllQualifiers(beans), Arrays.stream(beans).map(bean -> bean.getName())) .distinct() - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); } private Stream findAllQualifiers(Bean[] beans) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java index d9f51a5766..d16f8b63b6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java @@ -11,7 +11,8 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; @@ -30,13 +31,13 @@ public ResourceCompletionProvider(SpringMetamodelIndex springIndex) { } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); return Arrays.stream(beans).map(bean -> bean.getName()) .distinct() - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java index b18d8760e1..0fb39b9b2f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java @@ -12,7 +12,8 @@ import java.nio.file.Paths; import java.util.Comparator; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; import org.springframework.ide.vscode.commons.java.IClasspathUtil; @@ -23,8 +24,8 @@ */ public class ConditionalOnResourceCompletionProcessor implements AnnotationAttributeCompletionProvider { - private List findResources(IJavaProject project) { - List resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream() + private Map findResources(IJavaProject project) { + Map resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream() .distinct() .sorted(new Comparator() { @Override @@ -34,13 +35,13 @@ public int compare(String o1, String o2) { }) .map(r -> r.replaceAll("\\\\", "/")) .map(r -> "classpath:" + r) - .toList(); + .collect(Collectors.toMap(key -> key, value -> value)); return resources; } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { return findResources(project); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java index 684de6bf93..cc1994eb6a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/CronExpressionCompletionProvider.java @@ -39,7 +39,7 @@ public class CronExpressionCompletionProvider implements AnnotationAttributeComp @Override - public Map getCompletionCandidatesWithLabels(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { return CRON_EXPRESSIONS_MAP; } } 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 fe51b80908..8c15107432 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,7 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.scope; -import java.util.List; +import java.util.Map; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -20,18 +20,18 @@ */ public class ScopeCompletionProcessor implements AnnotationAttributeCompletionProvider { - private static final List SCOPE_COMPLETIONS = List.of( - "application", - "globalSession", - "prototype", - "request", - "session", - "singleton", - "websocket" - ); + private static final Map SCOPE_COMPLETIONS = Map.of( + "application", "application", + "globalSession", "globalSession", + "prototype", "prototype", + "request", "request", + "session", "session", + "singleton", "singleton", + "websocket", "websocket" + ); @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { return SCOPE_COMPLETIONS; }