GH-1323, GH-1324: Cron Expressions completion proposals and inlay hints

This commit is contained in:
vudayani
2024-09-24 20:56:25 +05:30
committed by Martin Lippert
parent 3dd95fa0e0
commit 46778a7d8a
12 changed files with 624 additions and 19 deletions

View File

@@ -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<InlayHint> getInlayHints(TextDocumentInfo document) throws Exception {
InlayHintParams params = new InlayHintParams();
params.setTextDocument(document.getId());
return getServer().getTextDocumentService().inlayHint(params).get();
}
public List<? extends DocumentHighlight> getDocumentHighlights(TextDocumentIdentifier docId, Position cursor) throws InterruptedException, ExecutionException {
return getServer().getTextDocumentService().documentHighlight(new DocumentHighlightParams(docId, cursor)).get();

View File

@@ -194,6 +194,13 @@
<artifactId>commons-util</artifactId>
<version>1.58.0-SNAPSHOT</version>
</dependency>
<!-- Cron expression descriptor library -->
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.0</version>
</dependency>
</dependencies>
<profiles>

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<ICompletionProposal> completions, int startOffset, int endOffset,
String filterPrefix, Function<String, String> createReplacementText) {
private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, String attributeName,
Collection<ICompletionProposal> completions, int startOffset, int endOffset, String filterPrefix,
Function<String, String> createReplacementText) {
Set<String> alreadyMentionedValues = alreadyMentionedValues(node);
@@ -123,25 +125,42 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide
if (completionProvider != null) {
List<String> candidates = completionProvider.getCompletionCandidates(project);
List<String> 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<String, String> proposals = completionProvider.getCompletionCandidatesWithLabels(project);
Map<String, String> 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<String, String> 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<String> 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.
//

View File

@@ -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<String> getCompletionCandidates(IJavaProject project);
default List<String> getCompletionCandidates(IJavaProject project) {
return new ArrayList<>();
}
default Map<String, String> getCompletionCandidatesWithLabels(IJavaProject project) {
return new HashMap<>();
}
}

View File

@@ -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<String, String> 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<String, String> getCompletionCandidatesWithLabels(IJavaProject project) {
return CRON_EXPRESSIONS_MAP;
}
}

View File

@@ -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<InlayHint> 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<InlayHint> 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());
}
}

View File

@@ -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<Void> 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<CompletionItem> 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());
}
}
}

View File

@@ -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<Void> 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<InlayHint> 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<InlayHint> 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;
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}