include method definition context to explain spel expressions

This commit is contained in:
vudayani
2024-08-23 11:49:26 +05:30
committed by Martin Lippert
parent 00636085ca
commit ada7d7f4be
6 changed files with 211 additions and 47 deletions

View File

@@ -56,6 +56,7 @@ import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolP
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxHandlerCodeLensProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteHighlightProdivder;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.utils.SpringLiveChangeDetectionWatchdog;
import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider;
@@ -118,6 +119,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private DocumentSymbolHandler docSymbolProvider;
private JdtSemanticTokensHandler semanticTokensHandler;
private JdtInlayHintsHandler inlayHintsHandler;
private SpelSemanticTokens spelSemanticTokens;
public BootJavaLanguageServerComponents(ApplicationContext appContext) {
this.server = appContext.getBean(SimpleLanguageServer.class);
@@ -175,8 +177,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
projectFinder,
Duration.ofSeconds(5),
sourceLinks);
spelSemanticTokens = appContext.getBean(SpelSemanticTokens.class);
codeLensHandler = createCodeLensEngine(springSymbolIndex, projectFinder, server);
codeLensHandler = createCodeLensEngine(springSymbolIndex, projectFinder, server, spelSemanticTokens);
highlightsEngine = createDocumentHighlightEngine(appContext);
documents.onDocumentHighlight(highlightsEngine);
@@ -312,10 +316,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
return new BootJavaReferencesHandler(this, cuCache, projectFinder, providers);
}
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index, JavaProjectFinder projectFinder, SimpleLanguageServer server) {
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index, JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(index));
codeLensProvider.add(new QueryCodeLensProvider(projectFinder, server));
codeLensProvider.add(new QueryCodeLensProvider(projectFinder, server, spelSemanticTokens));
return new BootJavaCodeLensEngine(this, codeLensProvider);
}

View File

@@ -10,26 +10,35 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.handlers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor;
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
@@ -42,39 +51,37 @@ import com.google.gson.JsonPrimitive;
*/
public class QueryCodeLensProvider implements CodeLensProvider {
protected static Logger logger = LoggerFactory.getLogger(QueryCodeLensProvider.class);
public static final String CMD_ENABLE_COPILOT_FEATURES = "sts/enable/copilot/features";
public static final String EXPLAIN_SPEL_TITLE = "Explain Spel Expression using Copilot";
public static final String EXPLAIN_QUERY_TITLE = "Explain Query using Copilot";
private static final String QUERY = "Query";
private static final String FQN_QUERY = "org.springframework.data.jpa.repository." + QUERY;
private static final String SPEL_EXPRESSION_QUERY_PROMPT = "Explain the following SpEL Expression in detail: \n";
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";
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";
private static final String DEFAULT_QUERY_PROMPT = "Explain the following query in detail: \n";
private static final String CMD = "vscode-spring-boot.query.explain";
private static final String CMD = "vscode-spring-boot.query.explain";
private final AnnotationParamSpelExtractor[] spelExtractors = AnnotationParamSpelExtractor.SPEL_EXTRACTORS;
private final JavaProjectFinder projectFinder;
private SpelSemanticTokens spelSemanticTokens;
private static boolean showCodeLenses;
public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server) {
public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
this.projectFinder = projectFinder;
this.spelSemanticTokens = spelSemanticTokens;
server.onCommand(CMD_ENABLE_COPILOT_FEATURES, params -> {
if (params.getArguments().get(0) instanceof JsonPrimitive) {
QueryCodeLensProvider.showCodeLenses = ((JsonPrimitive)params.getArguments().get(0)).getAsBoolean();
QueryCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
}
return CompletableFuture.completedFuture(showCodeLenses);
return CompletableFuture.completedFuture(showCodeLenses);
});
}
@Override
public void provideCodeLenses(CancelChecker cancelToken, TextDocument document, CompilationUnit cu,
List<CodeLens> resultAccumulator) {
if(!showCodeLenses) {
if (!showCodeLenses) {
return;
}
cu.accept(new ASTVisitor() {
@@ -83,12 +90,15 @@ public class QueryCodeLensProvider implements CodeLensProvider {
public boolean visit(SingleMemberAnnotation node) {
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
.map(o -> o.get()).forEach(snippet -> {
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, resultAccumulator);
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
provideCodeLensForSpelExpression(cancelToken, node, document, snippet,
additionalContext, resultAccumulator);
});
if (isQueryAnnotation(node)) {
String queryPrompt = determineQueryPrompt(document);
provideCodeLensForQuery(cancelToken, node, document, node.getValue(), queryPrompt, resultAccumulator);
provideCodeLensForQuery(cancelToken, node, document, node.getValue(), queryPrompt,
resultAccumulator);
}
return super.visit(node);
@@ -96,9 +106,13 @@ public class QueryCodeLensProvider implements CodeLensProvider {
@Override
public boolean visit(NormalAnnotation node) {
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
.map(o -> o.get()).forEach(snippet -> {
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, resultAccumulator);
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext,
resultAccumulator);
});
if (isQueryAnnotation(node)) {
@@ -107,7 +121,8 @@ public class QueryCodeLensProvider implements CodeLensProvider {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
if ("value".equals(pair.getName().getIdentifier())) {
provideCodeLensForQuery(cancelToken, node, document, pair.getValue(), queryPrompt, resultAccumulator);
provideCodeLensForQuery(cancelToken, node, document, pair.getValue(), queryPrompt,
resultAccumulator);
break;
}
}
@@ -119,20 +134,26 @@ public class QueryCodeLensProvider implements CodeLensProvider {
});
}
protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annotation node, TextDocument document, Snippet snippet,
List<CodeLens> resultAccumulator) {
protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annotation node, TextDocument document,
Snippet snippet, String additionalContext, List<CodeLens> resultAccumulator) {
cancelToken.checkCanceled();
if (snippet != null) {
try {
String context = additionalContext != null && !additionalContext.isEmpty() ? String.format(
"""
Finally, provide a brief summary of what the following method does, focusing on its role within the SpEL expression.
The summary should mention key criteria the method checks but avoid detailed implementation steps.
Please include this summary as an appendix to the main explanation, and avoid repeating information covered earlier.\n\n%s
""",additionalContext) : "";
CodeLens codeLens = new CodeLens();
codeLens.setRange(document.toRange(snippet.offset(), snippet.text().length()));
Command cmd = new Command();
cmd.setTitle(EXPLAIN_SPEL_TITLE);
cmd.setTitle(QueryType.SPEL.getTitle());
cmd.setCommand(CMD);
cmd.setArguments(ImmutableList.of(SPEL_EXPRESSION_QUERY_PROMPT + snippet.text()));
cmd.setArguments(ImmutableList.of(QueryType.SPEL.getPrompt() + snippet.text() + "\n\n" + context));
codeLens.setCommand(cmd);
resultAccumulator.add(codeLens);
@@ -153,14 +174,14 @@ public class QueryCodeLensProvider implements CodeLensProvider {
codeLens.setRange(document.toRange(valueExp.getStartPosition(), valueExp.getLength()));
Command cmd = new Command();
cmd.setTitle(EXPLAIN_QUERY_TITLE);
cmd.setTitle(QueryType.DEFAULT.getTitle());
cmd.setCommand(CMD);
cmd.setArguments(ImmutableList.of(query + valueExp.toString()));
codeLens.setCommand(cmd);
resultAccumulator.add(codeLens);
} catch (BadLocationException e) {
e.printStackTrace();
logger.error("Error providing code lens: " + e.getMessage());
}
}
}
@@ -169,14 +190,52 @@ public class QueryCodeLensProvider implements CodeLensProvider {
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
}
private String determineQueryPrompt(TextDocument document) {
Optional<IJavaProject> optProject = projectFinder.find(document.getId());
if (optProject.isPresent()) {
IJavaProject jp = optProject.get();
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? HQL_QUERY_PROMPT : JPQL_QUERY_PROMPT;
}
return DEFAULT_QUERY_PROMPT;
Optional<IJavaProject> optProject = projectFinder.find(document.getId());
if (optProject.isPresent()) {
IJavaProject jp = optProject.get();
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? QueryType.HQL.getPrompt()
: QueryType.JPQL.getPrompt();
}
return QueryType.DEFAULT.getPrompt();
}
private String parseSpelAndFetchContext(CompilationUnit cu, String spelExpression) {
List<SemanticTokenData> tokens = parseSpelExpression(spelExpression);
Set<String> methodNames = extractMethodNames(tokens, spelExpression);
List<String> context = collectMethodContexts(methodNames, cu);
return String.join("\n", context);
}
private List<SemanticTokenData> parseSpelExpression(String spelText) {
try {
return spelSemanticTokens.computeTokens(spelText, 0);
} catch (Exception e) {
logger.error("Error computing tokens: " + e.getMessage());
return Collections.emptyList();
}
}
private static Set<String> extractMethodNames(List<SemanticTokenData> tokens, String spelText) {
return tokens.stream().filter(token -> "method".equals(token.type()))
.map(token -> spelText.substring(token.start(), token.end())).collect(Collectors.toSet());
}
private List<String> collectMethodContexts(Set<String> methodNames, CompilationUnit cu) {
List<String> methodContext = new ArrayList<>();
for (String methodName : methodNames) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
if (node.getName().getIdentifier().equals(methodName)) {
methodContext.add(node.toString());
}
return super.visit(node);
}
});
}
return methodContext;
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.ide.vscode.boot.java.handlers;
public enum QueryType {
SPEL("Explain SpEL Expression using Copilot", "Explain the following SpEL Expression with a clear summary first, followed by a breakdown of the expression with details: \n\n"),
JPQL("Explain Query using Copilot", "Explain the following JPQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
HQL("Explain Query using Copilot", "Explain the following HQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
DEFAULT("Explain Query using Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
private final String title;
private final String prompt;
QueryType(String title, String prompt) {
this.title = title;
this.prompt = prompt;
}
public String getTitle() {
return title;
}
public String getPrompt() {
return prompt;
}
}

View File

@@ -39,6 +39,8 @@ 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.boot.java.handlers.QueryCodeLensProvider;
import org.springframework.ide.vscode.boot.java.handlers.QueryType;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.ExecuteCommandHandler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
@@ -66,6 +68,9 @@ public class QueryCodeLensProviderTest {
@Autowired
private SpringSymbolIndex indexer;
private SimpleLanguageServer server;
@Autowired
private SpelSemanticTokens spelSemanticTokens;
private ArgumentCaptor<ExecuteCommandHandler> commandHandlerCaptor;
private QueryCodeLensProvider queryCodeLensProvider;
@@ -78,7 +83,7 @@ public class QueryCodeLensProviderTest {
String projectDir = directory.toURI().toString();
server = mock(SimpleLanguageServer.class);
commandHandlerCaptor = ArgumentCaptor.forClass(ExecuteCommandHandler.class);
queryCodeLensProvider = new QueryCodeLensProvider(projectFinder, server);
queryCodeLensProvider = new QueryCodeLensProvider(projectFinder, server, spelSemanticTokens);
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
@@ -102,9 +107,9 @@ public class QueryCodeLensProviderTest {
assertEquals(3, codeLenses.size());
assertTrue(containsCodeLens(codeLenses.get(0), QueryCodeLensProvider.EXPLAIN_QUERY_TITLE, 9, 8, 9, 108));
assertTrue(containsCodeLens(codeLenses.get(1), QueryCodeLensProvider.EXPLAIN_QUERY_TITLE, 13, 8, 13, 39));
assertTrue(containsCodeLens(codeLenses.get(2), QueryCodeLensProvider.EXPLAIN_QUERY_TITLE, 17, 14, 17, 92));
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.DEFAULT.getTitle(), 9, 8, 9, 108));
assertTrue(containsCodeLens(codeLenses.get(1), QueryType.DEFAULT.getTitle(), 13, 8, 13, 39));
assertTrue(containsCodeLens(codeLenses.get(2), QueryType.DEFAULT.getTitle(), 17, 14, 17, 92));
}
@Test
@@ -118,11 +123,75 @@ public class QueryCodeLensProviderTest {
TextDocumentInfo openedDoc = harness.openDocument(doc);
List<? extends CodeLens> codeLenses = harness.getCodeLenses(openedDoc);
String expectedPrompt = """
Explain the following SpEL Expression with a clear summary first, followed by a breakdown of the expression with details: \n
T(org.test.SpelController).isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'
assertEquals(2, codeLenses.size());
Finally, provide a brief summary of what the following method does, focusing on its role within the SpEL expression.
The summary should mention key criteria the method checks but avoid detailed implementation steps.
Please include this summary as an appendix to the main explanation, and avoid repeating information covered earlier.
assertTrue(containsCodeLens(codeLenses.get(0), QueryCodeLensProvider.EXPLAIN_SPEL_TITLE, 13, 17, 13, 111));
assertTrue(containsCodeLens(codeLenses.get(1), QueryCodeLensProvider.EXPLAIN_SPEL_TITLE, 16, 11, 16, 142));
public static boolean isValidVersion(String version){
if (version.matches("\\\\d+\\\\.\\\\d+\\\\.\\\\d+")) {
String[] parts=version.split("\\\\.");
int major=Integer.parseInt(parts[0]);
int minor=Integer.parseInt(parts[1]);
int patch=Integer.parseInt(parts[2]);
return (major > 3) || (major == 3 && (minor > 0 || (minor == 0 && patch >= 0)));
}
return false;
}
""";
assertEquals(3, codeLenses.size());
String actualPrompt = codeLenses.get(1).getCommand().getArguments().get(0).toString();
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.SPEL.getTitle(), 13, 17, 13, 111));
assertTrue(containsCodeLens(codeLenses.get(1), QueryType.SPEL.getTitle(), 16, 11, 16, 107));
assertEquals(expectedPrompt, actualPrompt);
}
@Test
public void testShowCodeLensesTrueForSpelWithMultipleMethods() throws Exception {
// Simulate the command execution with true parameter
setCommandParamsHandler(true);
String docUri = directory.toPath().resolve("src/main/java/org/test/SpelController.java").toUri().toString();
TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId());
TextDocumentInfo openedDoc = harness.openDocument(doc);
List<? extends CodeLens> codeLenses = harness.getCodeLenses(openedDoc);
String expectedPrompt = """
Explain the following SpEL Expression with a clear summary first, followed by a breakdown of the expression with details: \n
T(org.test.SpelController).toUpperCase('hello') + ' ' + T(org.test.SpelController).concat('world', '!')
Finally, provide a brief summary of what the following method does, focusing on its role within the SpEL expression.
The summary should mention key criteria the method checks but avoid detailed implementation steps.
Please include this summary as an appendix to the main explanation, and avoid repeating information covered earlier.
public static String toUpperCase(String input){
return input.toUpperCase();
}
public static String concat(String str1,String str2){
return str1 + str2;
}
""";
assertEquals(3, codeLenses.size());
String actualPrompt = codeLenses.get(2).getCommand().getArguments().get(0).toString();
assertTrue(containsCodeLens(codeLenses.get(2), QueryType.SPEL.getTitle(), 19, 11, 19, 114));
assertEquals(expectedPrompt, actualPrompt);
}
@Test

View File

@@ -14,8 +14,11 @@ public class SpelController {
@Value(value="#{'${app.version}' matches '\\\\d+\\\\.\\\\d+\\\\.\\\\d+' ? '${app.version}' : 'Invalid Version'}")
private String version;
@Value("#{T(org.springframework.samples.petclinic.owner.SpelController).isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'}")
@Value("#{T(org.test.SpelController).isValidVersion('${app.version}') ? 'Valid Version' :'Invalid Version'}")
private String versionValidity;
@Value("#{T(org.test.SpelController).toUpperCase('hello') + ' ' + T(org.test.SpelController).concat('world', '!')}")
private String greeting;
@GetMapping("/version")
@ResponseBody
@@ -39,5 +42,13 @@ public class SpelController {
}
return false;
}
public static String toUpperCase(String input) {
return input.toUpperCase();
}
public static String concat(String str1, String str2) {
return str1 + str2;
}
}

View File

@@ -92,9 +92,6 @@ async function updateConfiguration(value: boolean) {
async function explainQueryWithCopilot() {
commands.registerCommand('vscode-spring-boot.query.explain', async (userPrompt) => {
console.log('spel.explain: ' + userPrompt);
console.log('messages: ' + userPrompt);
await commands.executeCommand('workbench.action.chat.open', { query: userPrompt });
})
}