package com.jsonnetplugin; import com.intellij.codeInsight.completion.*; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.ProcessingContext; import com.jsonnetplugin.psi.*; import org.jetbrains.annotations.NotNull; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import static com.jsonnetplugin.JsonnetIdentifierReference.findIdentifierFromParams; import static com.jsonnetplugin.JsonnetIdentifierReference.isFunctionExpr; public class JsonnetCompletionContributor extends CompletionContributor { public JsonnetCompletionContributor() { extend(CompletionType.BASIC, PlatformPatterns.psiElement(JsonnetTypes.IDENTIFIER).withLanguage(JsonnetLanguage.INSTANCE), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) { PsiElement element = parameters.getPosition().getOriginalElement(); while (element != null) { if (element instanceof JsonnetSelect) { JsonnetObjinside[] resolved = resolveExprToObj((JsonnetExpr) element.getParent()); if (resolved != null) { for (JsonnetObjinside r : resolved) { addMembersFromObject(r, resultSet); } } // Do not show suggestions from outer space if the element // before the dot can be resolved. We are only interested in the fields. return; } else if (element instanceof JsonnetOuterlocal) { List<JsonnetBind> binds = JsonnetIdentifierReference.findBindInOuterLocal((JsonnetOuterlocal) element); for (JsonnetBind b : binds) { resultSet.addElement(LookupElementBuilder.create(b.getIdentifier0().getText())); } } else if (element instanceof JsonnetExpr0 && isFunctionExpr((JsonnetExpr0) element)) { List<JsonnetIdentifier0> identifiers = JsonnetIdentifierReference.findIdentifierFromFunctionExpr0((JsonnetExpr0) element); for (JsonnetIdentifier0 i : identifiers) { resultSet.addElement(LookupElementBuilder.create(i.getText())); } } else if (element instanceof JsonnetObjinside) { List<JsonnetObjlocal> locals = new ArrayList<>(((JsonnetObjinside) element).getObjlocalList()); JsonnetMembers members = ((JsonnetObjinside) element).getMembers(); if (members != null) { for (JsonnetMember m : members.getMemberList()) { if (m.getObjlocal() != null) { locals.add(m.getObjlocal()); } } } for (JsonnetObjlocal local : locals) { JsonnetBind b = local.getBind(); resultSet.addElement(LookupElementBuilder.create(b.getIdentifier0().getText())); } } else if (element.getParent() instanceof JsonnetBind && ((JsonnetBind) element.getParent()).getExpr() == element) { List<JsonnetIdentifier0> idents = findIdentifierFromParams(((JsonnetBind) element.getParent()).getParams()); for (JsonnetIdentifier0 ident : idents) { resultSet.addElement(LookupElementBuilder.create(ident.getText())); } } else if (element.getParent() instanceof JsonnetField && ((JsonnetField) element.getParent()).getExpr() == element) { List<JsonnetIdentifier0> idents = findIdentifierFromParams(((JsonnetField) element.getParent()).getParams()); for (JsonnetIdentifier0 ident : idents) { resultSet.addElement(LookupElementBuilder.create(ident.getText())); } } element = element.getParent(); } resultSet.addElement(LookupElementBuilder.create("null")); resultSet.addElement(LookupElementBuilder.create("true")); resultSet.addElement(LookupElementBuilder.create("false")); resultSet.addElement(LookupElementBuilder.create("local")); } } ); extend(CompletionType.BASIC, PlatformPatterns.psiElement(JsonnetTypes.DOUBLE_QUOTED_STRING).withLanguage(JsonnetLanguage.INSTANCE), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) { if (checkIfImport(parameters.getPosition())) { String text = parameters.getPosition().getText(); addFileCompletions(parameters.getOriginalFile(), text, resultSet); } } } ); } private static void addMembersFromObject(JsonnetObjinside obj, CompletionResultSet resultSet) { if (obj == null || obj.getMembers() == null) return; List<JsonnetMember> memberList = obj.getMembers().getMemberList(); for (JsonnetMember member : memberList) { if (member.getField() != null && member.getField().getFieldname().getIdentifier0() != null) { String fieldName = member.getField().getFieldname().getIdentifier0().getText(); resultSet.addElement(LookupElementBuilder.create(fieldName)); } } } private static JsonnetObjinside[] resolveExprToObj(JsonnetExpr expr) { return resolveExprToObj(expr, new ArrayList<>()); } /** * Resolves an expression of the form x.y.z.(dummy token) to an instance of JsonnetObj * if possible, otherwise returns null. * To avoid infinite loops, we keep track of the list of expressions visited along this * call chain.git p */ private static JsonnetObjinside[] resolveExprToObj(JsonnetExpr expr, List<JsonnetExpr> visited) { if (visited.contains(expr)) return null; // In the future we can give a warning here visited.add(expr); try { List<JsonnetIdentifier0> selectList = new ArrayList<>(); for (JsonnetSelect select : expr.getSelectList()) { if (!select.getIdentifier0().getText().endsWith(Constants.INTELLIJ_RULES.trim())) { selectList.add(select.getIdentifier0()); } else { break; } } return resolveExprToObj(expr, visited, selectList); } finally { visited.remove(expr); } } static JsonnetObjinside[] resolveExprToObj(JsonnetExpr expr, List<JsonnetExpr> visited, List<JsonnetIdentifier0> selectList) { JsonnetObjinside[] curr = resolveExprLhsToObj(expr, visited, selectList); List<JsonnetObjinside> extended = new java.util.ArrayList<>(); if (curr != null) { extended.addAll(Arrays.asList(curr)); } for (JsonnetObjextend x : expr.getObjextendList()) { extended.add(x.getObjinside()); } for (JsonnetBinsuffix x : expr.getBinsuffixList()) { JsonnetObjinside[] rhs = resolveExprToObj(x.getExpr(), visited); if (rhs != null) { extended.addAll(Arrays.asList(rhs)); } } if (extended.size() == 0) return null; else { JsonnetObjinside[] res = new JsonnetObjinside[extended.size()]; extended.toArray(res); return res; } } static JsonnetObjinside[] resolveExprLhsToObj(JsonnetExpr expr, List<JsonnetExpr> visited, List<JsonnetIdentifier0> selectList) { JsonnetExpr0 first = expr.getExpr0(); JsonnetObjinside[] curr = resolveExpr0ToObj(first, visited); for (JsonnetIdentifier0 select : selectList) { if (curr == null) return null; List<JsonnetExpr> fieldValues = Arrays.stream(curr) .map(c -> getField(c, select.getText())) .filter(Objects::nonNull) .collect(Collectors.toList()); if (fieldValues.isEmpty()) return null; List<JsonnetObjinside> resolvedList = fieldValues.stream() .map(f -> resolveExprToObj(f, visited)) .filter(Objects::nonNull) .flatMap(Arrays::stream) .collect(Collectors.toList()); curr = new JsonnetObjinside[resolvedList.size()]; resolvedList.toArray(curr); } return curr; } private static JsonnetObjinside[] resolveIdentifierToObj(JsonnetIdentifier0 id, List<JsonnetExpr> visited) { if (id.getReference() == null) return null; PsiElement resolved = id.getReference().resolve(); if (resolved instanceof JsonnetBind) { JsonnetExpr expr = ((JsonnetBind) resolved).getExpr(); return resolveExprToObj(expr, visited); } return null; } private static JsonnetExpr getField(JsonnetObjinside obj, String name) { if (obj == null || obj.getMembers() == null) return null; List<JsonnetMember> memberList = obj.getMembers().getMemberList(); for (JsonnetMember member : memberList) { if (member.getField() != null && member.getField().getFieldname().getIdentifier0() != null) { String fieldName = member.getField().getFieldname().getIdentifier0().getText(); if (fieldName.equals(name)) { return member.getField().getExpr(); } } } return null; } private static JsonnetObjinside[] resolveExpr0ToObj(JsonnetExpr0 expr0, List<JsonnetExpr> visited) { if (expr0.getIfelse() != null) { JsonnetIfelse ifelse = expr0.getIfelse(); ArrayList<JsonnetObjinside> output = new ArrayList<>(); JsonnetObjinside[] lhsRes = resolveExprToObj(ifelse.getExprList().get(1), visited); if (lhsRes != null) { output.addAll(Arrays.asList(lhsRes)); } if (ifelse.getExprList().size() == 3) { JsonnetObjinside[] rhsRes = resolveExprToObj(ifelse.getExprList().get(2), visited); if (rhsRes != null) { output.addAll(Arrays.asList(rhsRes)); } } return output.toArray(new JsonnetObjinside[0]); } if (expr0.getExpr() != null) { return resolveExprToObj(expr0.getExpr(), visited); } if (expr0.getOuterlocal() != null) { return resolveExprToObj(expr0.getOuterlocal().getExpr(), visited); } if (expr0.getObj() != null && expr0.getObj().getObjinside() != null) { return new JsonnetObjinside[]{expr0.getObj().getObjinside()}; } if (expr0.getText().equals("self")) { return new JsonnetObjinside[]{findSelfObject(expr0)}; } if (expr0.getText().equals("$")) { return new JsonnetObjinside[]{findOuterObject(expr0)}; } if (expr0.getImportop() != null) { JsonnetImportop importop = expr0.getImportop(); if (importop.getReference() == null) { return null; } PsiFile file = (PsiFile) importop.getReference().resolve(); if (file == null) { // The imported file does not exist return null; } for (PsiElement c : file.getChildren()) { // Apparently children can be line comments and other unwanted rubbish if (c instanceof JsonnetExpr) { JsonnetObjinside[] res = resolveExprToObj((JsonnetExpr) c, visited); if (res != null) return res; } } } if (expr0.getIdentifier0() != null) { return resolveIdentifierToObj(expr0.getIdentifier0(), visited); } return null; } private static JsonnetObjinside findSelfObject(PsiElement elem) { PsiElement curr = elem; while (curr != null && !(curr instanceof JsonnetObj)) { curr = curr.getParent(); } return ((JsonnetObj) curr).getObjinside(); } private static JsonnetObjinside findOuterObject(PsiElement elem) { JsonnetObj obj = null; PsiElement curr = elem; while (curr != null) { if (curr instanceof JsonnetObj) { obj = (JsonnetObj) curr; } curr = curr.getParent(); } return obj.getObjinside(); } private static boolean checkIfImport(PsiElement position) { return position.getPrevSibling() != null && position.getPrevSibling().getPrevSibling().getNode().getElementType().equals(JsonnetTypes.IMPORT); } private static void addFileCompletions(PsiFile file, String current, CompletionResultSet set) { // current always begins with a " String cleanedCurrent = current.substring(1); if (cleanedCurrent.endsWith("\"")) { cleanedCurrent = cleanedCurrent.substring(0, cleanedCurrent.length() - 1); } if (!cleanedCurrent.endsWith(Constants.INTELLIJ_RULES)) { return; } Path currentPath = Paths.get(file.getContainingDirectory().getVirtualFile().getPath()); String stripped = cleanedCurrent.replace(Constants.INTELLIJ_RULES, ""); Path strippedPath = Paths.get(stripped); int strippedPathCount = strippedPath.getNameCount(); File prefixFile; String input; if (stripped.endsWith("/")) { prefixFile = currentPath.resolve(Paths.get(stripped)).toFile(); input = ""; } else if (strippedPathCount == 1) { prefixFile = currentPath.toFile(); input = stripped; } else { prefixFile = currentPath.resolve(strippedPath.subpath(0, strippedPathCount - 1)).toFile(); input = strippedPath.subpath(strippedPathCount - 1, strippedPathCount).toString(); } CompletionResultSet replaceSet = set.withPrefixMatcher(stripped); if (prefixFile.isDirectory()) { File[] files = prefixFile.listFiles((dir, name) -> name.startsWith(input)); if (files != null) { for (File f : files) { String result = stripped + f.getName().substring(input.length()); replaceSet.addElement(LookupElementBuilder.create(result)); } } } } }