From 2e796921d585d04883345bd05d8585d6ab45e8be Mon Sep 17 00:00:00 2001 From: danthe1st Date: Fri, 3 Mar 2023 12:49:57 +0100 Subject: [PATCH] completions for predicate keywords in Spring Data repositories --- .../DataRepositoryMethodNameParseResult.java | 4 +- .../DataRepositoryMethodParser.java | 4 +- ...toryPrefixSensitiveCompletionProvider.java | 55 +++++++++++++++---- ...DataRepositoryCompletionProcessorTest.java | 26 ++++++++- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodNameParseResult.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodNameParseResult.java index 3048c4daf..48f3766ce 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodNameParseResult.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodNameParseResult.java @@ -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, /** diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodParser.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodParser.java index c4c55ddc1..aedf38643 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodParser.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryMethodParser.java @@ -30,9 +30,7 @@ class DataRepositoryMethodParser { private static final Map> 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> propertiesGroupedByFirstWord; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java index 95d4d0de5..1f0499b3d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java @@ -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 completions, int offset, String prefix, DataRepositoryMethodNameParseResult parseResult, Map 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 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 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 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); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java index 2a088cf10..3f807429a 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java @@ -138,11 +138,35 @@ public class DataRepositoryCompletionProcessorTest { } @Test - void findByComplexExpression() throws Exception { + void testFindByComplexExpression() throws Exception { checkCompletions("findByResponsibleEmployee", "List findByResponsibleEmployee(Employee responsibleEmployee);"); checkCompletions("findByResponsibleEmployee_SocialSecurityNumber", "List 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));