This commit is contained in:
aboyko
2024-09-29 11:17:07 -04:00
22 changed files with 717 additions and 83 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

@@ -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;
@@ -114,34 +115,34 @@ 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);
AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get(attributeName);
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());
double score = filteredCandidates.size();
for (String candidate : filteredCandidates) {
Map<String, String> proposals = completionProvider.getCompletionCandidates(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, (u, v) -> u, LinkedHashMap::new));
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, candidate, null, score--);
AnnotationAttributeCompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits,
candidate, entry.getValue(), null, score--);
completions.add(proposal);
}
}
}
//
// internal computation of the right positions, prefixes, etc.
//

View File

@@ -10,12 +10,12 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.annotations;
import java.util.List;
import java.util.Map;
import org.springframework.ide.vscode.commons.java.IJavaProject;
public interface AnnotationAttributeCompletionProvider {
List<String> getCompletionCandidates(IJavaProject project);
Map<String, String> getCompletionCandidates(IJavaProject project);
}

View File

@@ -11,7 +11,9 @@
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
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 +237,11 @@ public class DependsOnCompletionProcessor implements AnnotationAttributeCompleti
// }
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
return Arrays.stream(this.springIndex.getBeansOfProject(project.getElementName()))
.map(bean -> bean.getName())
.distinct()
.toList();
.collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new));
}

View File

@@ -11,7 +11,9 @@
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
@@ -32,7 +34,7 @@ public class NamedCompletionProvider implements AnnotationAttributeCompletionPro
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
@@ -40,7 +42,7 @@ public class NamedCompletionProvider implements AnnotationAttributeCompletionPro
findAllNamedValues(beans),
Arrays.stream(beans).map(bean -> bean.getName()))
.distinct()
.toList();
.collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new));
}
private Stream<String> findAllNamedValues(Bean[] beans) {

View File

@@ -11,7 +11,9 @@
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
@@ -32,13 +34,13 @@ public class ProfileCompletionProvider implements AnnotationAttributeCompletionP
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
return findAllProfiles(beans)
.distinct()
.toList();
.collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new));
}
private Stream<String> findAllProfiles(Bean[] beans) {

View File

@@ -11,7 +11,9 @@
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
@@ -32,7 +34,7 @@ public class QualifierCompletionProvider implements AnnotationAttributeCompletio
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
@@ -40,7 +42,7 @@ public class QualifierCompletionProvider implements AnnotationAttributeCompletio
findAllQualifiers(beans),
Arrays.stream(beans).map(bean -> bean.getName()))
.distinct()
.toList();
.collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new));
}
private Stream<String> findAllQualifiers(Bean[] beans) {

View File

@@ -11,7 +11,9 @@
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
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 +32,13 @@ public class ResourceCompletionProvider implements AnnotationAttributeCompletion
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> 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, (u, v) -> u, LinkedHashMap::new));
}
}

View File

@@ -12,7 +12,9 @@ package org.springframework.ide.vscode.boot.java.conditionalonresource;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List;
import java.util.LinkedHashMap;
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 +25,8 @@ import org.springframework.ide.vscode.commons.java.IJavaProject;
*/
public class ConditionalOnResourceCompletionProcessor implements AnnotationAttributeCompletionProvider {
private List<String> findResources(IJavaProject project) {
List<String> resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
private Map<String, String> findResources(IJavaProject project) {
Map<String, String> resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
.distinct()
.sorted(new Comparator<String>() {
@Override
@@ -34,13 +36,13 @@ public class ConditionalOnResourceCompletionProcessor implements AnnotationAttri
})
.map(r -> r.replaceAll("\\\\", "/"))
.map(r -> "classpath:" + r)
.toList();
.collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new));
return resources;
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
return findResources(project);
}

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 * * * *", "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");
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> getCompletionCandidates(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

@@ -10,7 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.scope;
import java.util.List;
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;
@@ -20,18 +21,20 @@ import org.springframework.ide.vscode.commons.java.IJavaProject;
*/
public class ScopeCompletionProcessor implements AnnotationAttributeCompletionProvider {
private static final List<String> SCOPE_COMPLETIONS = List.of(
"application",
"globalSession",
"prototype",
"request",
"session",
"singleton",
"websocket"
);
private static final Map<String, String> 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 List<String> getCompletionCandidates(IJavaProject project) {
public Map<String, String> getCompletionCandidates(IJavaProject project) {
return SCOPE_COMPLETIONS;
}

View File

@@ -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<Match<PropertyInfo>> 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<ICompletionProposal> 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<ICompletionProposal> completions, int offset, TextDocument doc) {
String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition());
@@ -253,15 +232,15 @@ public class ValueCompletionProcessor implements CompletionProvider {
private List<Match<PropertyInfo>> findMatches(String prefix, IDocument doc) {
FuzzyMap<PropertyInfo> index = indexProvider.getIndex(doc).getProperties();
List<Match<PropertyInfo>> matches =index.find(camelCaseToHyphens(prefix));
List<Match<PropertyInfo>> matches = index.find(camelCaseToHyphens(prefix));
//First the 'real' properties.
// First the 'real' properties.
Set<String> suggestedKeys = new HashSet<>();
for (Match<PropertyInfo> 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<IJavaProject> 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<ICompletionProposal> 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()

View File

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

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/CronScheduler.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

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

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