completions for predicate keywords in Spring Data repositories

This commit is contained in:
danthe1st
2023-03-03 12:49:57 +01:00
committed by aboyko
parent a68aa38dda
commit 2e796921d5
4 changed files with 72 additions and 17 deletions

View File

@@ -33,9 +33,9 @@ record DataRepositoryMethodNameParseResult(
*/
boolean performFullCompletion,
/**
* the last entered word, which completion options should be used for completing the expression.
* the last entered word (if it is not a keyword), which completion options should be used for completing the expression.
*
* e.g. {@code First} in {@code findByFirst} which could be completed to {@code findByFirstName}
* e.g. {@code First} in {@code findByFirst} which could be completed to {@code findByFirstName} or {@code null} if it is a keyword or non-existent
*/
String lastWord,
/**

View File

@@ -30,9 +30,7 @@ class DataRepositoryMethodParser {
private static final Map<String, List<QueryPredicateKeywordInfo>> PREDICATE_KEYWORDS_GROUPED_BY_FIRST_WORD = QueryPredicateKeywordInfo.PREDICATE_KEYWORDS
.stream()
.collect(Collectors.groupingBy(info->{
return findFirstWord(info.keyword());
}));
.collect(Collectors.groupingBy(info -> findFirstWord(info.keyword())));
private final String prefix;
private final Map<String, List<DomainProperty>> propertiesGroupedByFirstWord;

View File

@@ -47,22 +47,55 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo
if (parseResult.lastWord() == null || !propertiesByName.containsKey(parseResult.lastWord())) {
addPropertyProposals(completions, offset, repoDef, parseResult);
}
addPredicateKeywordProposals(completions, offset, prefix, parseResult, propertiesByName);
}
}
private void addPredicateKeywordProposals(Collection<ICompletionProposal> completions, int offset, String prefix, DataRepositoryMethodNameParseResult parseResult, Map<String, DomainProperty> propertiesByName) {
String lastWord = findLastWordWithoutPrefixingProperty(parseResult, propertiesByName);
for (QueryPredicateKeywordInfo predicate : QueryPredicateKeywordInfo.PREDICATE_KEYWORDS){
if (parseResult.allowedKeywordTypes().contains(predicate.type())) {
createLastWordReplacementCompletion(completions, offset, parseResult, lastWord, predicate.keyword());
}
}
}
private String findLastWordWithoutPrefixingProperty(DataRepositoryMethodNameParseResult parseResult, Map<String, DomainProperty> propertiesByName) {
String lastWord = parseResult.lastWord();
if (lastWord == null) {
return "";
}
if (propertiesByName.containsKey(lastWord)) {
return "";
}
for (int i = lastWord.length() - 1; i >= 0; i--) {
if (Character.isUpperCase(lastWord.charAt(i))) {
String substring = lastWord.substring(0,i);
if (propertiesByName.containsKey(substring)) {
return lastWord.substring(i);
}
}
}
return lastWord;
}
private void addPropertyProposals(Collection<ICompletionProposal> completions, int offset, DataRepositoryDefinition repoDef, DataRepositoryMethodNameParseResult parseResult) {
for(DomainProperty property : repoDef.getDomainType().getProperties()){
String lastWord = parseResult.lastWord();
if (lastWord == null) {
lastWord = "";
}
if (property.getName().startsWith(lastWord)) {
DocumentEdits edits = new DocumentEdits(null, false);
edits.replace(offset - lastWord.length(), offset, property.getName());
DocumentEdits additionalEdits = new DocumentEdits(null, false);
ICompletionProposal proposal = new FindByCompletionProposal(property.getName(), CompletionItemKind.Text, edits, "property " + property.getName(), null, Optional.of(additionalEdits), lastWord);
completions.add(proposal);
}
String toReplace = property.getName();
createLastWordReplacementCompletion(completions, offset, parseResult, parseResult.lastWord(), toReplace);
}
}
private void createLastWordReplacementCompletion(Collection<ICompletionProposal> completions, int offset, DataRepositoryMethodNameParseResult parseResult, String lastWord, String toReplace) {
if (lastWord == null) {
lastWord = "";
}
if (toReplace.startsWith(lastWord)) {
DocumentEdits edits = new DocumentEdits(null, false);
edits.replace(offset - lastWord.length(), offset, toReplace);
DocumentEdits additionalEdits = new DocumentEdits(null, false);
ICompletionProposal proposal = new FindByCompletionProposal(toReplace, CompletionItemKind.Text, edits, "property " + toReplace, null, Optional.of(additionalEdits), lastWord);
completions.add(proposal);
}
}

View File

@@ -138,11 +138,35 @@ public class DataRepositoryCompletionProcessorTest {
}
@Test
void findByComplexExpression() throws Exception {
void testFindByComplexExpression() throws Exception {
checkCompletions("findByResponsibleEmployee", "List<Customer> findByResponsibleEmployee(Employee responsibleEmployee);");
checkCompletions("findByResponsibleEmployee_SocialSecurityNumber", "List<Customer> findByResponsibleEmployee_SocialSecurityNumber(Long responsibleEmployee_SocialSecurityNumber);");
}
@Test
void testAppendKeywords() throws Exception {
checkCompletions("findByFirstName",
"findByFirstNameAnd",
"findByFirstNameExists",
"findByFirstNameIgnoreCase",
"findByFirstNameIsLessThanEqual",
"findByFirstNameOr",
"findByFirstNameOrderBy");
}
@Test
void testAppendKeywordsWithPreviousKeyword() throws Exception {
checkCompletions("findByFirstNameAnd",
"findByFirstNameAndNot");
}
@Test
void testAppendKeywordsStartAlreadyPresent() throws Exception {
checkCompletions("findByFirstNameA",
"findByFirstNameAfter",
"findByFirstNameAnd");
}
private void checkCompletions(String alredyPresent, String... expectedCompletions) throws Exception {
prepareCase("{\n}", "{\n\t" + alredyPresent + "<*>");
assertContainsAnnotationCompletions(Arrays.stream(expectedCompletions).map(expected -> "\t" + expected + "<*>").toArray(String[]::new));