blob: 64252a3370de30a895b4fd9f9bfc9c97b36aeab3 [file]
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.grails;
import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.EqTailType;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.lookup.TailTypeDecorator;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.grails.util.GrailsArtifact;
import org.jetbrains.plugins.grails.util.GrailsPluginCondition;
import org.jetbrains.plugins.grails.util.GrailsUtils;
import org.jetbrains.plugins.grails.util.GrailsVersionCondition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.jetbrains.plugins.groovy.util.FieldInitializerTailTypes.EQ_ARRAY;
import static org.jetbrains.plugins.groovy.util.FieldInitializerTailTypes.EQ_CLOSURE;
import static org.jetbrains.plugins.groovy.util.FieldInitializerTailTypes.EQ_STRING;
import static org.jetbrains.plugins.groovy.util.FieldInitializerTailTypes.EQ_STRING_ARRAY;
public class GrailsStaticFieldCompletionProvider extends CompletionProvider<CompletionParameters> {
private record FieldCompletion(@NotNull String fieldName, @NotNull TailType tailType, @Nullable Condition<? super PsiClass> condition) {
}
private static final Map<GrailsArtifact, FieldCompletion[]> ALLOWED_STATIC_FIELDS = GrailsUtils.createEnumMap(
GrailsArtifact.DOMAIN, new FieldCompletion[]{
new FieldCompletion("mapWith", EQ_STRING, null),
new FieldCompletion("hasMany", EQ_ARRAY, null),
new FieldCompletion("hasOne", EQ_ARRAY, null),
new FieldCompletion("belongsTo", EQ_ARRAY, null),
new FieldCompletion("relatesToMany", EQ_ARRAY, null),
new FieldCompletion("constraints", EQ_CLOSURE, null),
new FieldCompletion("mappedBy", EQ_ARRAY, null),
new FieldCompletion("mapping", EQ_CLOSURE, null),
new FieldCompletion("embedded", EQ_STRING_ARRAY, null),
new FieldCompletion("transients", EQ_STRING_ARRAY, null),
new FieldCompletion("evanescent", EQ_STRING_ARRAY, null),
new FieldCompletion("optionals", EqTailType.INSTANCE, null),
new FieldCompletion("namedQueries", EQ_CLOSURE, null),
new FieldCompletion("fetchMode", EQ_ARRAY, null),
new FieldCompletion("searchable", EqTailType.INSTANCE, new GrailsPluginCondition("searchable")),
new FieldCompletion("auditable", EqTailType.INSTANCE, new GrailsPluginCondition("audit-logging"))
},
GrailsArtifact.JOB, new FieldCompletion[]{
new FieldCompletion("triggers", EQ_CLOSURE, null)
},
GrailsArtifact.CONTROLLER, new FieldCompletion[]{
new FieldCompletion("layout", EQ_STRING, null),
new FieldCompletion("defaultAction", EQ_STRING, null),
new FieldCompletion("allowedMethods", EqTailType.INSTANCE, null),
new FieldCompletion("accessControl", EQ_CLOSURE, Conditions.or(new GrailsPluginCondition("shiro"), new GrailsPluginCondition("jsecurity"))),
new FieldCompletion("namespace", EQ_STRING, new GrailsVersionCondition("2.3.0"))
},
GrailsArtifact.TAGLIB, new FieldCompletion[]{
new FieldCompletion("namespace", EQ_STRING, null),
new FieldCompletion("supportsController", EqTailType.INSTANCE, null),
new FieldCompletion("returnObjectForTags", EQ_STRING_ARRAY, null)
},
GrailsArtifact.SERVICE, new FieldCompletion[]{
new FieldCompletion("transactional", EqTailType.INSTANCE, null),
new FieldCompletion("scope", EqTailType.INSTANCE, null)
},
GrailsArtifact.URLMAPPINGS, new FieldCompletion[]{
new FieldCompletion("mappings", EQ_CLOSURE, null),
},
GrailsArtifact.REALM, new FieldCompletion[]{
new FieldCompletion("authTokenClass", EqTailType.INSTANCE, null)
}
);
private static final Map<GrailsArtifact, FieldCompletion[]> ALLOWED_FIELDS = GrailsUtils.createEnumMap(
GrailsArtifact.CONTROLLER, new FieldCompletion[]{
new FieldCompletion("beforeInterceptor", EqTailType.INSTANCE, null),
new FieldCompletion("afterInterceptor", EqTailType.INSTANCE, null),
new FieldCompletion("scaffold", EqTailType.INSTANCE, null)
},
GrailsArtifact.FILTER, new FieldCompletion[]{
new FieldCompletion("filters", EqTailType.INSTANCE, null),
new FieldCompletion("dependsOn", EqTailType.INSTANCE, null)
},
GrailsArtifact.JOB, new FieldCompletion[] {
new FieldCompletion("sessionRequired", EqTailType.INSTANCE, null),
new FieldCompletion("concurrent", EqTailType.INSTANCE, null),
}
);
private static final Set<String> ourFieldNameCache;
static {
Set<String> res = new HashSet<>();
for (FieldCompletion[] completions : ALLOWED_FIELDS.values()) {
for (FieldCompletion completion : completions) {
res.add(completion.fieldName);
}
}
for (FieldCompletion[] completions : ALLOWED_STATIC_FIELDS.values()) {
for (FieldCompletion completion : completions) {
res.add(completion.fieldName);
}
}
ourFieldNameCache = res;
}
private final Map<GrailsArtifact, FieldCompletion[]> myMap;
public GrailsStaticFieldCompletionProvider(boolean aStatic) {
myMap = aStatic ? ALLOWED_STATIC_FIELDS : ALLOWED_FIELDS;
}
public static boolean isGrailsField(@NotNull GrField field) {
String fieldName = field.getName();
if (!ourFieldNameCache.contains(fieldName)) return false;
PsiClass containingClass = field.getContainingClass();
GrailsArtifact grailsArtifact = GrailsArtifact.getType(containingClass);
if (grailsArtifact == null) return false;
Map<GrailsArtifact, FieldCompletion[]> map = field.hasModifierProperty(PsiModifier.STATIC) ? ALLOWED_STATIC_FIELDS : ALLOWED_FIELDS;
FieldCompletion[] trinities = map.get(grailsArtifact);
if (trinities == null) return false;
for (FieldCompletion trinity : trinities) {
if (trinity.fieldName.equals(fieldName) && (trinity.condition == null || trinity.condition.value(containingClass))) {
return true;
}
}
return false;
}
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet result) {
final PsiClass psiClass = PsiTreeUtil.getParentOfType(parameters.getPosition(), PsiClass.class);
if (psiClass == null) return;
GrailsArtifact artifact = GrailsUtils.calculateArtifactType(psiClass);
if (artifact == null) return;
FieldCompletion[] allowedFields = myMap.get(artifact);
if (allowedFields == null) return;
for (FieldCompletion completion : allowedFields) {
String fieldName = completion.fieldName;
if (psiClass.findFieldByName(fieldName, false) == null) {
if (completion.condition == null || completion.condition.value(psiClass)) {
result.addElement(TailTypeDecorator.withTail(LookupElementBuilder.create(fieldName), completion.tailType));
}
}
}
}
}