Skip to content

Commit

Permalink
GH-1323, GH-1324: Cron Expressions completion proposals and inlay hints
Browse files Browse the repository at this point in the history
  • Loading branch information
vudayani authored and martinlippert committed Sep 26, 2024
1 parent 3dd95fa commit 46778a7
Show file tree
Hide file tree
Showing 12 changed files with 623 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InlayHint;
import org.eclipse.lsp4j.InlayHintParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MarkupContent;
Expand Down Expand Up @@ -681,6 +683,12 @@ public List<? extends CodeLens> getCodeLenses(TextDocumentInfo document) throws
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();
Expand Down
7 changes: 7 additions & 0 deletions headless-services/spring-boot-language-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider;
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceCompletionProcessor;
import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor;
import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
Expand All @@ -40,8 +43,6 @@
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor;
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceCompletionProcessor;
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
Expand Down Expand Up @@ -132,6 +133,8 @@ BootJavaCompletionEngine javaCompletionEngine(

providers.put(Annotations.NAMED_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex))));
providers.put(Annotations.NAMED_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex))));

providers.put(Annotations.SCHEDULED, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("cron", new CronExpressionCompletionProvider())));

return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
Expand Down Expand Up @@ -114,34 +115,52 @@ else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotat
/**
* create the concrete completion proposal
*/
private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, String attributeName, Collection<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);

AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get(attributeName);
if (completionProvider != null) {
List<String> candidates = completionProvider.getCompletionCandidates(project);

List<String> filteredCandidates = candidates.stream()
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);

}
} 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());
.filter(candidate -> candidate.toLowerCase().contains(filterPrefix.toLowerCase()))
.filter(candidate -> !alreadyMentionedValues.contains(candidate)).collect(Collectors.toList());
double score = filteredCandidates.size();
for (String candidate : filteredCandidates) {

double score = filteredCandidates.size();
for (String candidate : filteredCandidates) {

DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, createReplacementText.apply(candidate));

AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, candidate, candidate, null, score--);
completions.add(proposal);
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, createReplacementText.apply(candidate));

AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits,
candidate, candidate, null, score--);
completions.add(proposal);
}
}
}
}


//
// internal computation of the right positions, prefixes, etc.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<>();
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading

0 comments on commit 46778a7

Please sign in to comment.