From 46778a7d8acea7de1c6d8a9417b5ed30dc26d098 Mon Sep 17 00:00:00 2001 From: vudayani Date: Tue, 24 Sep 2024 20:56:25 +0530 Subject: [PATCH 1/6] 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 | 51 ++++-- ...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, 624 insertions(+), 19 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 f0e2de13d..b33c81ab5 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.Hover; 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 class LanguageServerHarness { 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 220955856..e20e3770f 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 eb9c1bc0b..50b739412 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.NamedCompletionProvider; 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.snippets.JavaSnippetManager; 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 @@ public class BootJavaCompletionEngineConfigurer { 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 70f66a815..646883083 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 @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; 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; @@ -139,6 +140,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 304c3a2dd..27d157374 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.Name; 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 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide /** * 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 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide if (completionProvider != null) { List candidates = completionProvider.getCompletionCandidates(project); - 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()); + 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); - 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); + } + } 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) { + + 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 2f96e6c69..16212224e 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 000000000..684de6bf9 --- /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 000000000..ef926b1fb --- /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 000000000..bdc85f8df --- /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 000000000..0968364ce --- /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 000000000..d4f525213 --- /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 000000000..a02a56cf4 --- /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 4a63ffd5e71757f15bfa5128c19e736621266279 Mon Sep 17 00:00:00 2001 From: vudayani Date: Wed, 25 Sep 2024 21:08:24 +0530 Subject: [PATCH 2/6] 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 27d157374..53399807c 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.Name; 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 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide 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); + 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); - } - } 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) { - - 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 16212224e..5a0b6d331 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 10a4539a1..13bcd3215 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 class DependsOnCompletionProcessor implements AnnotationAttributeCompleti // } @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 895e27099..7d864ba43 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 class NamedCompletionProvider implements AnnotationAttributeCompletionPro } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); @@ -40,7 +41,7 @@ public class NamedCompletionProvider implements AnnotationAttributeCompletionPro 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 85cc4d0dc..47d6860e4 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 class ProfileCompletionProvider implements AnnotationAttributeCompletionP } @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 edd49d7ec..bc6daf4f2 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 class QualifierCompletionProvider implements AnnotationAttributeCompletio } @Override - public List getCompletionCandidates(IJavaProject project) { + public Map getCompletionCandidates(IJavaProject project) { Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); @@ -40,7 +41,7 @@ public class QualifierCompletionProvider implements AnnotationAttributeCompletio 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 d9f51a576..d16f8b63b 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 class ResourceCompletionProvider implements AnnotationAttributeCompletion } @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 b18d8760e..0fb39b9b2 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 @@ package org.springframework.ide.vscode.boot.java.conditionalonresource; 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 @@ import org.springframework.ide.vscode.commons.java.IJavaProject; */ 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 class ConditionalOnResourceCompletionProcessor implements AnnotationAttri }) .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 684de6bf9..cc1994eb6 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 fe51b8090..8c1510743 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 @@ import org.springframework.ide.vscode.commons.java.IJavaProject; */ 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; } From cfd1857d6a9e286f3d112ffe4dea397a7dbc205e Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 26 Sep 2024 12:17:53 +0200 Subject: [PATCH 3/6] fix typo in test source file name --- .../main/java/org/test/{CronSchedular.java => CronScheduler.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/{CronSchedular.java => CronScheduler.java} (100%) 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/CronScheduler.java similarity index 100% rename from headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronSchedular.java rename to headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/CronScheduler.java From fa7128171182fdd69444711eb12686a0b903bb9d Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 26 Sep 2024 12:20:35 +0200 Subject: [PATCH 4/6] fix typo in test source file name --- .../boot/java/cron/CronExpressionsInlayHintsProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0968364ce..ac78ff5f6 100644 --- 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 @@ -74,7 +74,7 @@ public class CronExpressionsInlayHintsProviderTest { @Test public void test_cronExpresionInlayHints() throws Exception { - String docUri = directory.toPath().resolve("src/main/java/org/test/CronSchedular.java").toUri().toString(); + String docUri = directory.toPath().resolve("src/main/java/org/test/CronScheduler.java").toUri().toString(); TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId()); TextDocumentInfo openedDoc = harness.openDocument(doc); From 8544efa73b8fd85ac0919c8320ffa90ee0d2a557 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 26 Sep 2024 13:42:25 +0200 Subject: [PATCH 5/6] bringing back an order in completion proposals, got lost due to random hashmap ordering --- ...nnotationAttributeCompletionProcessor.java | 3 ++- .../beans/DependsOnCompletionProcessor.java | 3 ++- .../java/beans/NamedCompletionProvider.java | 3 ++- .../java/beans/ProfileCompletionProvider.java | 3 ++- .../beans/QualifierCompletionProvider.java | 3 ++- .../beans/ResourceCompletionProvider.java | 3 ++- ...ditionalOnResourceCompletionProcessor.java | 3 ++- .../CronExpressionCompletionProvider.java | 8 +++---- .../java/scope/ScopeCompletionProcessor.java | 21 +++++++++++-------- 9 files changed, 30 insertions(+), 20 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 53399807c..712558bf7 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 @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.annotations; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -127,7 +128,7 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide 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)); + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (u, v) -> u, LinkedHashMap::new)); double score = filteredProposals.size(); for (Map.Entry entry : filteredProposals.entrySet()) { String candidate = entry.getKey(); 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 13bcd3215..2bd3e2894 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,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -240,7 +241,7 @@ public class DependsOnCompletionProcessor implements AnnotationAttributeCompleti return Arrays.stream(this.springIndex.getBeansOfProject(project.getElementName())) .map(bean -> bean.getName()) .distinct() - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); } 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 7d864ba43..b3f2a6fd5 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,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,7 +42,7 @@ public class NamedCompletionProvider implements AnnotationAttributeCompletionPro findAllNamedValues(beans), Arrays.stream(beans).map(bean -> bean.getName())) .distinct() - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); } 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 47d6860e4..37a004309 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,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,7 +40,7 @@ public class ProfileCompletionProvider implements AnnotationAttributeCompletionP return findAllProfiles(beans) .distinct() - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); } 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 bc6daf4f2..c51718df1 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,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,7 +42,7 @@ public class QualifierCompletionProvider implements AnnotationAttributeCompletio findAllQualifiers(beans), Arrays.stream(beans).map(bean -> bean.getName())) .distinct() - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); } 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 d16f8b63b..b8a006fe6 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,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -37,7 +38,7 @@ public class ResourceCompletionProvider implements AnnotationAttributeCompletion return Arrays.stream(beans).map(bean -> bean.getName()) .distinct() - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); } } 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 0fb39b9b2..9905ab524 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,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.conditionalonresource; import java.nio.file.Paths; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -35,7 +36,7 @@ public class ConditionalOnResourceCompletionProcessor implements AnnotationAttri }) .map(r -> r.replaceAll("\\\\", "/")) .map(r -> "classpath:" + r) - .collect(Collectors.toMap(key -> key, value -> value)); + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); return resources; } 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 cc1994eb6..e521a29eb 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 @@ -11,11 +11,11 @@ public class CronExpressionCompletionProvider implements AnnotationAttributeComp 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 * * * 1-5", "every hour every day between Monday and Friday"); + CRON_EXPRESSIONS_MAP.put("0 * * * * *", "every minute"); + CRON_EXPRESSIONS_MAP.put("0 */5 * * * *", "every 5 minutes"); + CRON_EXPRESSIONS_MAP.put("0 0 */6 * * *", "every 6 hours at minute 0"); 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"); 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 8c1510743..fb11a9b7a 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,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.scope; +import java.util.LinkedHashMap; import java.util.Map; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; @@ -20,15 +21,17 @@ import org.springframework.ide.vscode.commons.java.IJavaProject; */ public class ScopeCompletionProcessor implements AnnotationAttributeCompletionProvider { - private static final Map SCOPE_COMPLETIONS = Map.of( - "application", "application", - "globalSession", "globalSession", - "prototype", "prototype", - "request", "request", - "session", "session", - "singleton", "singleton", - "websocket", "websocket" - ); + private static final Map SCOPE_COMPLETIONS = new LinkedHashMap<>(); + + static { + SCOPE_COMPLETIONS.put("application", "application"); + SCOPE_COMPLETIONS.put("globalSession", "globalSession"); + SCOPE_COMPLETIONS.put("prototype", "prototype"); + SCOPE_COMPLETIONS.put("request", "request"); + SCOPE_COMPLETIONS.put("session", "session"); + SCOPE_COMPLETIONS.put("singleton", "singleton"); + SCOPE_COMPLETIONS.put("websocket", "websocket"); + } @Override public Map getCompletionCandidates(IJavaProject project) { From 99d703aff2b2509fd3b2ea98d038cd68b7e09534 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Fri, 27 Sep 2024 13:55:03 +0200 Subject: [PATCH 6/6] added non-working test cases for future reference and some code polish --- .../java/value/ValueCompletionProcessor.java | 68 ++++++++++--------- .../java/value/ValuePropertyKeyProposal.java | 1 + .../java/value/test/ValueCompletionTest.java | 39 +++++++++++ 3 files changed, 75 insertions(+), 33 deletions(-) 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 7f39dbe8c..a696ff416 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 @@ -24,6 +24,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.MemberValuePair; +import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.StringLiteral; @@ -75,6 +76,11 @@ public class ValueCompletionProcessor implements CompletionProvider { IJavaProject project = optionalProject.get(); + // in case the node is embedded in an qualified name, e.g. "file.txt", use the fully qualified node instead just a part + if (node instanceof Name && node.getParent() instanceof QualifiedName) { + node = node.getParent(); + } + // case: @Value(<*>) if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { List> matches = findMatches("", doc); @@ -95,20 +101,16 @@ public class ValueCompletionProcessor implements CompletionProvider { addClasspathResourceProposals(project, doc, offset, offset, "", true, completions); } // case: @Value(prefix<*>) - else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { + else if (node instanceof Name && node.getParent() instanceof Annotation) { computeProposalsForSimpleName(project, node, completions, offset, doc); } - // case: @Value(file.ext<*>) - the "." causes a QualifierNode to be generated - else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof Annotation) { - computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc); - } // case: @Value(value=<*>) - else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair + else if (node instanceof Name && node.getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { computeProposalsForSimpleName(project, node, completions, offset, doc); } // case: @Value(value=<*>) - else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair + else if (node instanceof Name && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair && "value".equals(((MemberValuePair)node.getParent().getParent()).getName().toString())) { computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc); } @@ -131,29 +133,6 @@ public class ValueCompletionProcessor implements CompletionProvider { } } - private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection completions) { - String[] resources = findResources(project, prefix); - - double score = resources.length + 1000; - for (String resource : resources) { - - DocumentEdits edits = new DocumentEdits(doc, false); - - if (includeQuotes) { - edits.replace(startOffset, endOffset, "\"classpath:" + resource + "\""); - } - else { - edits.replace(startOffset, endOffset, "classpath:" + resource); - } - - String label = "classpath:" + resource; - - ICompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, label, label, null, score--); - completions.add(proposal); - } - - } - private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) { String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); @@ -253,15 +232,15 @@ public class ValueCompletionProcessor implements CompletionProvider { private List> findMatches(String prefix, IDocument doc) { FuzzyMap index = indexProvider.getIndex(doc).getProperties(); - List> matches =index.find(camelCaseToHyphens(prefix)); + List> matches = index.find(camelCaseToHyphens(prefix)); - //First the 'real' properties. + // First the 'real' properties. Set suggestedKeys = new HashSet<>(); for (Match m : matches) { suggestedKeys.add(m.data.getId()); } - //Then also add 'ad-hoc' properties (see https://www.pivotaltracker.com/story/show/153107266). + // Then also add 'ad-hoc' properties Optional p = projectFinder.find(new TextDocumentIdentifier(doc.getUri())); if (p.isPresent()) { index = adHocIndexProvider.getIndex(p.get()); @@ -274,6 +253,29 @@ public class ValueCompletionProcessor implements CompletionProvider { return matches; } + private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection completions) { + String[] resources = findResources(project, prefix); + + double score = resources.length + 1000; + for (String resource : resources) { + + DocumentEdits edits = new DocumentEdits(doc, false); + + if (includeQuotes) { + edits.replace(startOffset, endOffset, "\"classpath:" + resource + "\""); + } + else { + edits.replace(startOffset, endOffset, "classpath:" + resource); + } + + String label = "classpath:" + resource; + + ICompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, label, label, null, score--); + completions.add(proposal); + } + + } + private String[] findResources(IJavaProject project, String prefix) { String[] resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream() .distinct() diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java index 2d90a8753..bcb742cb9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyKeyProposal.java @@ -24,6 +24,7 @@ import org.springframework.ide.vscode.commons.util.Renderable; public class ValuePropertyKeyProposal extends ScoreableProposal { private static final String EMPTY_DETAIL = ""; + private DocumentEdits edits; private String label; private String detail; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java index 79916d86c..bd06f3107 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java @@ -294,6 +294,20 @@ public class ValueCompletionTest { assertClasspathCompletions(); } +// The parser removes the (spring.) piece from the AST in this case, so there is no way +// to clearly identify this case +// +// @Test +// void testComplexPrefixCompletionParamNameCursorRightAfterDot() throws Exception { +// prepareCase("@Value(\"onField\")", "@Value(spring.<*>)"); +// prepareDefaultIndexData(); +// +// assertPropertyCompletions( +// "@Value(\"${spring.prop1}\"<*>)"); +// +// assertClasspathCompletions(); +// } + @Test void testComplexPrefixCompletion() throws Exception { prepareCase("@Value(\"onField\")", "@Value(spring.pr<*>)"); @@ -316,6 +330,20 @@ public class ValueCompletionTest { assertClasspathCompletions(); } +// The parser removes the (spring.) piece from the AST in this case, so there is no way +// to clearly identify this case +// +// @Test +// void testPrefixCompletionWithParamNameCursorRightAfterDot() throws Exception { +// prepareCase("@Value(\"onField\")", "@Value(value=spring.<*>)"); +// prepareDefaultIndexData(); +// +// assertPropertyCompletions( +// "@Value(value=\"${spring.prop1}\"<*>)"); +// +// assertClasspathCompletions(); +// } + @Test void testComplexPrefixCompletionWithParamName() throws Exception { prepareCase("@Value(\"onField\")", "@Value(value=spring.pr<*>)"); @@ -419,6 +447,17 @@ public class ValueCompletionTest { assertClasspathCompletions(); } + @Test + void testComplexPrefixCompletionWithQuotesAndDotRightAfterPrefix() throws Exception { + prepareCase("@Value(\"onField\")", "@Value(\"spring.<*>\")"); + prepareDefaultIndexData(); + + assertPropertyCompletions( + "@Value(\"${spring.prop1}<*>\")"); + + assertClasspathCompletions(); + } + @Test void testComplexPrefixCompletionWithQuotes() throws Exception { prepareCase("@Value(\"onField\")", "@Value(\"spring.pr<*>\")");