Skip to content

Commit

Permalink
Merge pull request #2349 from Haehnchen/feature/2334-qb-prefix
Browse files Browse the repository at this point in the history
#2334 optimize prefix matcher for Doctrine querybuilder parameter, to reduce conflicting PhpStorm implementation
  • Loading branch information
Haehnchen committed Apr 14, 2024
2 parents a12ead4 + 5073618 commit 7a38bc3
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.doctrine.DoctrineUtil;
import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelField;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContribution;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContributionType;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderPropertyAlias;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderRelation;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.processor.QueryBuilderChainProcessor;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.util.MatcherUtil;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.util.QueryBuilderUtil;
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -108,7 +112,7 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters
@Override
protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) {
PsiElement psiElement = completionParameters.getOriginalPosition();
if (!Symfony2ProjectComponent.isEnabled(psiElement) || !(psiElement.getContext() instanceof StringLiteralExpression)) {
if (!Symfony2ProjectComponent.isEnabled(psiElement) || !(psiElement.getContext() instanceof StringLiteralExpression parent)) {
return;
}

Expand All @@ -122,6 +126,14 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters

QueryBuilderMethodReferenceParser qb = getQueryBuilderParser(methodMatchParameter.getMethodReference());
QueryBuilderScopeContext collect = qb.collect();

String content = PsiElementUtils.getStringBeforeCursor(parent, completionParameters.getOffset());

// "test.test"
if (content != null && content.matches("^[\\w_.]+$")) {
completionResultSet = completionResultSet.withPrefixMatcher(content);
}

for (Map.Entry<String, List<QueryBuilderRelation>> parameter : collect.getRelationMap().entrySet()) {
for (QueryBuilderRelation relation : parameter.getValue()) {
LookupElementBuilder element = LookupElementBuilder
Expand Down Expand Up @@ -162,45 +174,19 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters

QueryBuilderMethodReferenceParser qb = getQueryBuilderParser(methodMatchParameter.getMethodReference());
QueryBuilderScopeContext collect = qb.collect();
buildLookupElements(completionResultSet, collect);

for (QueryBuilderCompletionContribution contribution : QueryBuilderUtil.guestCompletionContribution(content)) {
if (contribution.type() == QueryBuilderCompletionContributionType.FUNCTION) {
for (Map.Entry<String, String> entry : DoctrineUtil.getDoctrineOrmFunctions(project).entrySet()) {
LookupElementBuilder lookup = LookupElementBuilder.create(entry.getKey().toUpperCase())
.withTypeText("FunctionNode")
.withIcon(Symfony2Icons.DOCTRINE_WEAK);

// "test.test"
if (content == null || content.isBlank() || content.matches("^[\\w+.]+$")) {
buildLookupElements(completionResultSet, collect);
return;
}

// "foo test.test"
Matcher matcher = Pattern.compile("(\\w+)\\.(\\w+)$").matcher(content);
if (matcher.find()) {
String table = matcher.group(1);
String field = matcher.group(2);

buildLookupElements(completionResultSet.withPrefixMatcher(table + "." + field), collect);
}

// "foo test."
Matcher matcher2 = Pattern.compile("(\\w+)\\.$").matcher(content);
if (matcher2.find()) {
String prefix = matcher2.group(1) + ".";
buildLookupElements(completionResultSet.withPrefixMatcher(prefix), collect);
}

// "foo, test.test"
// "(test.test"
Matcher matcher3 = Pattern.compile("[(|,]\\s*(\\w+)$").matcher(content);
if (matcher3.find()) {
String prefix = matcher3.group(1);
buildLookupElements(completionResultSet.withPrefixMatcher(prefix), collect);
}

for (Map.Entry<String, String> entry : DoctrineUtil.getDoctrineOrmFunctions(project).entrySet()) {
LookupElementBuilder lookup = LookupElementBuilder.create(entry.getKey().toUpperCase())
.withTypeText("FunctionNode")
.withIcon(Symfony2Icons.DOCTRINE_WEAK);

completionResultSet.addElement(lookup);
completionResultSet.addElement(lookup);
}
} else if (contribution.type() == QueryBuilderCompletionContributionType.PROPERTY) {
buildLookupElements(completionResultSet.withPrefixMatcher(contribution.prefix()), collect);
}
}
}
});
Expand Down Expand Up @@ -230,44 +216,28 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters
QueryBuilderScopeContext collect = qb.collect();

String content = PsiElementUtils.getStringBeforeCursor(parent, completionParameters.getOffset());
if (content == null || content.isBlank() || content.matches("^[\\w+.]+$")) {
buildLookupElements(completionResultSet, collect);
return;
}

Matcher matcher = Pattern.compile("(\\w+)\\.(\\w+)$").matcher(content);
if (matcher.find()) {
String table = matcher.group(1);
String field = matcher.group(2);

buildLookupElements(completionResultSet.withPrefixMatcher(table + "." + field), collect);
}

Matcher matcher2 = Pattern.compile("(\\w+)\\.$").matcher(content);
if (matcher2.find()) {
String prefix = matcher2.group(1) + ".";
buildLookupElements(completionResultSet.withPrefixMatcher(prefix), collect);
for (QueryBuilderCompletionContribution contribution : QueryBuilderUtil.guestCompletionContribution(content)) {
if (contribution.type() == QueryBuilderCompletionContributionType.PROPERTY) {
buildLookupElements(completionResultSet.withPrefixMatcher(contribution.prefix()), collect);
}
}

Matcher matcher3 = Pattern.compile("(AND|OR|WHERE|NOT|[!=><]+)\\s+(\\w+)$").matcher(content);
if (matcher3.find()) {
String prefix = matcher3.group(2);
buildLookupElements(completionResultSet.withPrefixMatcher(prefix), collect);
if (content != null) {
// $qb->andWhere('foo.id = ":foo_id"')
addParameterNameCompletion(completionResultSet.withPrefixMatcher(""), content);
}

// $qb->andWhere('foo.id = ":foo_id"')
addParameterNameCompletion(completionResultSet.withPrefixMatcher(""), content);
}

private void addParameterNameCompletion(CompletionResultSet completionResultSet, String content) {
private void addParameterNameCompletion(@NotNull CompletionResultSet completionResultSet, @NotNull String content) {
// test.test = :
// test.test =
Matcher matcher = Pattern.compile("(\\w+)\\.(\\w+)[\\s+]*[!=><]+[\\s+]*(?<colon>:*)$").matcher(content);
Matcher matcher = Pattern.compile("([\\w_]+)\\.([\\w_]+)[\\s+]*[!=><]+[\\s+]*(?<colon>:*)$").matcher(content);
boolean hasMatch = matcher.find();

if (!hasMatch) {
// test.test = :a
matcher = Pattern.compile("(\\w+)\\.(\\w+)[\\s+]*[!=><]+[\\s+]*(?<colon>:)(?<ident>\\w+)$").matcher(content);
matcher = Pattern.compile("([\\w_]+)\\.(\\w+)[\\s+]*[!=><]+[\\s+]*(?<colon>:)(?<ident>\\w+)$").matcher(content);
hasMatch = matcher.find();
if (hasMatch) {
completionResultSet = completionResultSet.withPrefixMatcher(":" + matcher.group("ident"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict;

import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.jetbrains.annotations.NotNull;

/**
* @author Daniel Espendiller <[email protected]>
*/
public record QueryBuilderCompletionContribution(@NotNull QueryBuilderCompletionContributionType type, @NotNull String prefix) {
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(this.type)
.append(this.prefix)
.toHashCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict;

/**
* @author Daniel Espendiller <[email protected]>
*/
public enum QueryBuilderCompletionContributionType {
PROPERTY,
FUNCTION
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.util;

import fr.adrienbrault.idea.symfony2plugin.doctrine.ObjectRepositoryTypeProvider;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContribution;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContributionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -66,4 +69,64 @@ public static String getFieldString(@NotNull String content, int offset) {

return field;
}

public static Collection<QueryBuilderCompletionContribution> guestCompletionContribution(@Nullable String content) {
Collection<QueryBuilderCompletionContribution> contributions = new ArrayList<>();

if (content == null || content.isBlank()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, ""));
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.FUNCTION, ""));
}

if (content != null) {
if (content.matches("^[\\w+_.]+$")) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, content));
}

Matcher matcher = Pattern.compile("([\\w_]+)\\.([\\w_]+)$").matcher(content);
if (matcher.find()) {
String table = matcher.group(1);
String field = matcher.group(2);

contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, table + "." + field));
}

// "foo, test.test"
// "(test.test"
Matcher matcher3 = Pattern.compile("[(|,]\\s*([\\w_]+)$").matcher(content);
if (matcher3.find()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, matcher3.group(1)));
}

// "test"
// ", test"
Matcher matcher2 = Pattern.compile("[(|,]\\s*([\\w_]+)$").matcher(content);
if (matcher2.find()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.FUNCTION, matcher2.group(1)));
} else {
Matcher matcherX = Pattern.compile("^([\\w_]+)$").matcher(content);
if (matcherX.find()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.FUNCTION, matcherX.group(1)));
}
}

// "(test
// "( test
Matcher matcherU = Pattern.compile("\\(\\s*([\\w_]+)$").matcher(content);
if (matcherU.find()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, matcherU.group(1)));
}

// "AND test"
// "AND test."
// "> test"
// "> test."
Matcher matcherY = Pattern.compile("(AND|OR|WHERE|NOT|=|>|<)\\s+([\\w_]+[.]*)$").matcher(content);
if (matcherY.find()) {
contributions.add(new QueryBuilderCompletionContribution(QueryBuilderCompletionContributionType.PROPERTY, matcherY.group(2)));
}
}

return new HashSet<>(contributions);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fr.adrienbrault.idea.symfony2plugin.tests.doctrine.querybuilder;

import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.jetbrains.php.lang.PhpFileType;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.QueryBuilderCompletionContributor;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -42,8 +44,8 @@ public void testCompletionForSelect() {
public void testCompletionForSelectField() {
assertCompletionContains(
"test.php",
createQueryBuilderWrap("$qb->select('foobar, foobar.<caret>');"),
"foobar.id", "foobar.name"
createQueryBuilderWrap("$qb->select('foobar, foobar.na<caret>me');"),
"foobar.name"
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fr.adrienbrault.idea.symfony2plugin.tests.doctrine.querybuilder.util;

import com.intellij.openapi.util.Pair;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContribution;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.dict.QueryBuilderCompletionContributionType;
import fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.util.QueryBuilderUtil;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;

Expand Down Expand Up @@ -53,4 +55,51 @@ public void testGetFieldString() {
assertEquals(item.getSecond(), string);
}
}

public void testGuestCompletionContribution() {
Collection<QueryBuilderCompletionContribution> t1 = QueryBuilderUtil.guestCompletionContribution("test_foo.te");
assertEquals(1, t1.size());
assertTrue(t1.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.te".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t2 = QueryBuilderUtil.guestCompletionContribution("");
assertEquals(2, t2.size());
assertTrue(t2.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && c.prefix().isEmpty()));
assertTrue(t2.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.FUNCTION && c.prefix().isEmpty()));

Collection<QueryBuilderCompletionContribution> t3 = QueryBuilderUtil.guestCompletionContribution("test_foo.");
assertEquals(1, t3.size());
assertTrue(t3.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t4 = QueryBuilderUtil.guestCompletionContribution("foo, test_foo.test_foo");
assertEquals(1, t4.size());
assertTrue(t4.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.test_foo".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t5 = QueryBuilderUtil.guestCompletionContribution("(test_foo.test_foo");
assertEquals(1, t5.size());
assertTrue(t5.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.test_foo".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t6 = QueryBuilderUtil.guestCompletionContribution("test_foo");
assertEquals(2, t6.size());
assertTrue(t6.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.FUNCTION && "test_foo".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t7 = QueryBuilderUtil.guestCompletionContribution("= test_foo.a");
assertEquals(1, t7.size());
assertTrue(t7.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.a".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t8 = QueryBuilderUtil.guestCompletionContribution("= test_foo");
assertEquals(1, t8.size());
assertTrue(t8.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t9 = QueryBuilderUtil.guestCompletionContribution("AND test_foo.");
assertEquals(1, t9.size());
assertTrue(t9.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t10 = QueryBuilderUtil.guestCompletionContribution("FOO(test_foo.fo");
assertEquals(1, t10.size());
assertTrue(t10.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.fo".equals(c.prefix())));

Collection<QueryBuilderCompletionContribution> t11 = QueryBuilderUtil.guestCompletionContribution("FOO(test_foo.fo");
assertEquals(1, t11.size());
assertTrue(t11.stream().anyMatch(c -> c.type() == QueryBuilderCompletionContributionType.PROPERTY && "test_foo.fo".equals(c.prefix())));
}
}

0 comments on commit 7a38bc3

Please sign in to comment.