Skip to content

Commit 578ca3e

Browse files
vudayanimartinlippert
authored andcommitted
Explain spel expressions and queries with copilot
1 parent 992a7c7 commit 578ca3e

File tree

15 files changed

+20412
-9
lines changed

15 files changed

+20412
-9
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
4141
import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider;
4242
import org.springframework.ide.vscode.boot.java.handlers.HoverProvider;
43+
import org.springframework.ide.vscode.boot.java.handlers.QueryCodeLensProvider;
4344
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
4445
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
4546
import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider;
@@ -173,7 +174,7 @@ public BootJavaLanguageServerComponents(ApplicationContext appContext) {
173174
Duration.ofSeconds(5),
174175
sourceLinks);
175176

176-
codeLensHandler = createCodeLensEngine(springSymbolIndex);
177+
codeLensHandler = createCodeLensEngine(springSymbolIndex, projectFinder, server);
177178

178179
highlightsEngine = createDocumentHighlightEngine(springSymbolIndex);
179180
documents.onDocumentHighlight(highlightsEngine);
@@ -304,9 +305,10 @@ protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server,
304305
return new BootJavaReferencesHandler(this, cuCache, projectFinder, providers);
305306
}
306307

307-
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index) {
308+
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index, JavaProjectFinder projectFinder, SimpleLanguageServer server) {
308309
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
309310
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(index));
311+
codeLensProvider.add(new QueryCodeLensProvider(projectFinder, server));
310312

311313
return new BootJavaCodeLensEngine(this, codeLensProvider);
312314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2017, 2024 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.handlers;
12+
13+
import java.util.Arrays;
14+
import java.util.List;
15+
import java.util.Optional;
16+
import java.util.concurrent.CompletableFuture;
17+
18+
import org.eclipse.jdt.core.dom.ASTVisitor;
19+
import org.eclipse.jdt.core.dom.Annotation;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.Expression;
22+
import org.eclipse.jdt.core.dom.MemberValuePair;
23+
import org.eclipse.jdt.core.dom.NormalAnnotation;
24+
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
25+
import org.eclipse.lsp4j.CodeLens;
26+
import org.eclipse.lsp4j.Command;
27+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
28+
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor;
29+
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
30+
import org.springframework.ide.vscode.commons.java.IJavaProject;
31+
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
32+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
33+
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
34+
import org.springframework.ide.vscode.commons.util.BadLocationException;
35+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
36+
37+
import com.google.common.collect.ImmutableList;
38+
import com.google.gson.JsonPrimitive;
39+
40+
/**
41+
* @author Udayani V
42+
*/
43+
public class QueryCodeLensProvider implements CodeLensProvider {
44+
45+
46+
public static final String CMD_ENABLE_COPILOT_FEATURES = "sts/enable/copilot/features";
47+
public static final String EXPLAIN_SPEL_TITLE = "Explain Spel Expression using Copilot";
48+
public static final String EXPLAIN_QUERY_TITLE = "Explain Query using Copilot";
49+
50+
private static final String QUERY = "Query";
51+
private static final String FQN_QUERY = "org.springframework.data.jpa.repository." + QUERY;
52+
private static final String SPEL_EXPRESSION_QUERY_PROMPT = "Explain the following SpEL Expression in detail: \n";
53+
private static final String JPQL_QUERY_PROMPT = "Explain the following JPQL query in detail. If the query contains any SpEL expressions, explain those parts as well: \n";
54+
private static final String HQL_QUERY_PROMPT = "Explain the following HQL query in detail. If the query contains any SpEL expressions, explain those parts as well: \n";
55+
private static final String DEFAULT_QUERY_PROMPT = "Explain the following query in detail: \n";
56+
private static final String CMD = "vscode-spring-boot.query.explain";
57+
58+
private final AnnotationParamSpelExtractor[] spelExtractors = AnnotationParamSpelExtractor.SPEL_EXTRACTORS;
59+
60+
private final JavaProjectFinder projectFinder;
61+
62+
private static boolean showCodeLenses;
63+
64+
public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server) {
65+
this.projectFinder = projectFinder;
66+
server.onCommand(CMD_ENABLE_COPILOT_FEATURES, params -> {
67+
if (params.getArguments().get(0) instanceof JsonPrimitive) {
68+
QueryCodeLensProvider.showCodeLenses = ((JsonPrimitive)params.getArguments().get(0)).getAsBoolean();
69+
}
70+
return CompletableFuture.completedFuture(showCodeLenses);
71+
});
72+
}
73+
74+
@Override
75+
public void provideCodeLenses(CancelChecker cancelToken, TextDocument document, CompilationUnit cu,
76+
List<CodeLens> resultAccumulator) {
77+
if(!showCodeLenses) {
78+
return;
79+
}
80+
cu.accept(new ASTVisitor() {
81+
82+
@Override
83+
public boolean visit(SingleMemberAnnotation node) {
84+
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
85+
.map(o -> o.get()).forEach(snippet -> {
86+
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, resultAccumulator);
87+
});
88+
89+
if (isQueryAnnotation(node)) {
90+
String queryPrompt = determineQueryPrompt(document);
91+
provideCodeLensForQuery(cancelToken, node, document, node.getValue(), queryPrompt, resultAccumulator);
92+
}
93+
94+
return super.visit(node);
95+
}
96+
97+
@Override
98+
public boolean visit(NormalAnnotation node) {
99+
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
100+
.map(o -> o.get()).forEach(snippet -> {
101+
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, resultAccumulator);
102+
});
103+
104+
if (isQueryAnnotation(node)) {
105+
String queryPrompt = determineQueryPrompt(document);
106+
for (Object value : node.values()) {
107+
if (value instanceof MemberValuePair) {
108+
MemberValuePair pair = (MemberValuePair) value;
109+
if ("value".equals(pair.getName().getIdentifier())) {
110+
provideCodeLensForQuery(cancelToken, node, document, pair.getValue(), queryPrompt, resultAccumulator);
111+
break;
112+
}
113+
}
114+
}
115+
}
116+
117+
return super.visit(node);
118+
}
119+
});
120+
}
121+
122+
protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annotation node, TextDocument document, Snippet snippet,
123+
List<CodeLens> resultAccumulator) {
124+
cancelToken.checkCanceled();
125+
126+
if (snippet != null) {
127+
try {
128+
129+
CodeLens codeLens = new CodeLens();
130+
codeLens.setRange(document.toRange(snippet.offset(), snippet.text().length()));
131+
132+
Command cmd = new Command();
133+
cmd.setTitle(EXPLAIN_SPEL_TITLE);
134+
cmd.setCommand(CMD);
135+
cmd.setArguments(ImmutableList.of(SPEL_EXPRESSION_QUERY_PROMPT + snippet.text()));
136+
codeLens.setCommand(cmd);
137+
138+
resultAccumulator.add(codeLens);
139+
} catch (BadLocationException e) {
140+
e.printStackTrace();
141+
}
142+
}
143+
}
144+
145+
protected void provideCodeLensForQuery(CancelChecker cancelToken, Annotation node, TextDocument document,
146+
Expression valueExp, String query, List<CodeLens> resultAccumulator) {
147+
cancelToken.checkCanceled();
148+
149+
if (valueExp != null) {
150+
try {
151+
152+
CodeLens codeLens = new CodeLens();
153+
codeLens.setRange(document.toRange(valueExp.getStartPosition(), valueExp.getLength()));
154+
155+
Command cmd = new Command();
156+
cmd.setTitle(EXPLAIN_QUERY_TITLE);
157+
cmd.setCommand(CMD);
158+
cmd.setArguments(ImmutableList.of(query + valueExp.toString()));
159+
codeLens.setCommand(cmd);
160+
161+
resultAccumulator.add(codeLens);
162+
} catch (BadLocationException e) {
163+
e.printStackTrace();
164+
}
165+
}
166+
}
167+
168+
private static boolean isQueryAnnotation(Annotation a) {
169+
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
170+
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
171+
}
172+
173+
private String determineQueryPrompt(TextDocument document) {
174+
Optional<IJavaProject> optProject = projectFinder.find(document.getId());
175+
if (optProject.isPresent()) {
176+
IJavaProject jp = optProject.get();
177+
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? HQL_QUERY_PROMPT : JPQL_QUERY_PROMPT;
178+
}
179+
return DEFAULT_QUERY_PROMPT;
180+
}
181+
182+
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import org.springframework.ide.vscode.boot.java.Annotations;
2222
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
2323

24-
final class AnnotationParamSpelExtractor {
24+
public final class AnnotationParamSpelExtractor {
2525

2626
private static final String SPRING_CACHEABLE = "org.springframework.cache.annotation.Cacheable";
2727
private static final String SPRING_CACHE_EVICT = "org.springframework.cache.annotation.CacheEvict";
@@ -35,7 +35,7 @@ final class AnnotationParamSpelExtractor {
3535

3636
private static final String SPRING_CONDITIONAL_ON_EXPRESSION = "org.springframework.boot.autoconfigure.condition.ConditionalOnExpression";
3737

38-
final static AnnotationParamSpelExtractor[] SPEL_EXTRACTORS = new AnnotationParamSpelExtractor[] {
38+
public final static AnnotationParamSpelExtractor[] SPEL_EXTRACTORS = new AnnotationParamSpelExtractor[] {
3939
new AnnotationParamSpelExtractor(Annotations.VALUE, null, "#{", "}"),
4040
new AnnotationParamSpelExtractor(Annotations.VALUE, "value", "#{", "}"),
4141

@@ -62,7 +62,7 @@ final class AnnotationParamSpelExtractor {
6262
};
6363

6464

65-
record Snippet(String text, int offset) {}
65+
public record Snippet(String text, int offset) {}
6666

6767
private final String annotationType;
6868
private final String paramName;
@@ -77,7 +77,7 @@ public AnnotationParamSpelExtractor(String annotationType, String paramName, Str
7777
this.paramValuePostfix = paramValuePostfix;
7878
}
7979

80-
Optional<Snippet> getSpelRegion(NormalAnnotation a) {
80+
public Optional<Snippet> getSpelRegion(NormalAnnotation a) {
8181
if (paramName == null) {
8282
return Optional.empty();
8383
}
@@ -104,7 +104,7 @@ Optional<Snippet> getSpelRegion(NormalAnnotation a) {
104104
return Optional.empty();
105105
}
106106

107-
Optional<Snippet> getSpelRegion(SingleMemberAnnotation a) {
107+
public Optional<Snippet> getSpelRegion(SingleMemberAnnotation a) {
108108
if (this.paramName != null) {
109109
return Optional.empty();
110110
}

0 commit comments

Comments
 (0)