blob: 983f2ba7186bbb41964c7c12a17d67c90cec77b5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.web.el.completion;
import com.sun.el.parser.AstAssign;
import com.sun.el.parser.AstBracketSuffix;
import com.sun.el.parser.AstDeferredExpression;
import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstDynamicExpression;
import com.sun.el.parser.AstFunction;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstInteger;
import com.sun.el.parser.AstLambdaExpression;
import com.sun.el.parser.AstListData;
import com.sun.el.parser.AstMapData;
import com.sun.el.parser.AstMethodArguments;
import com.sun.el.parser.AstSemiColon;
import com.sun.el.parser.AstString;
import com.sun.el.parser.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CodeCompletionHandler;
import org.netbeans.modules.csl.api.CodeCompletionHandler2;
import org.netbeans.modules.csl.api.CodeCompletionResult;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.Documentation;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ParameterInfo;
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.el.lexer.api.ELTokenId;
import org.netbeans.modules.web.el.AstPath;
import org.netbeans.modules.web.el.CompilationContext;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.ELParserResult;
import org.netbeans.modules.web.el.ELTypeUtilities;
import org.netbeans.modules.web.el.ELVariableResolvers;
import org.netbeans.modules.web.el.NodeUtil;
import org.netbeans.modules.web.el.ResourceBundles;
import org.netbeans.modules.web.el.refactoring.RefactoringUtil;
import org.netbeans.modules.web.el.spi.ELPlugin;
import org.netbeans.modules.web.el.spi.ELVariableResolver.VariableInfo;
import org.netbeans.modules.web.el.spi.Function;
import org.netbeans.modules.web.el.spi.ResourceBundle;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
/**
* Code completer for Expression Language.
*
* @author Erno Mononen
*/
public final class ELCodeCompletionHandler implements CodeCompletionHandler2 {
private static Set<String> keywordFixedTexts = null;
/**
* Gets Set of {@link ELTokenId#fixedText()} values with {@link ELTokenId.ELTokenCategories.KEYWORDS} category.<br/>
* Result is resolved and stored on first method call into static field {@link #keywordFixedTexts}.
* On next call is returned the same Set instance.
* @return Set of {@link ELTokenId#fixedText()} values with {@link ELTokenId.ELTokenCategories.KEYWORDS} category.
*/
private static synchronized Set<String> getKeywordFixedTexts() {
if (keywordFixedTexts == null) {
keywordFixedTexts = new HashSet<>();
for (ELTokenId elTokenId : ELTokenId.values()) {
if (ELTokenId.ELTokenCategories.KEYWORDS.hasCategory(elTokenId)) {
keywordFixedTexts.add(elTokenId.fixedText());
}
}
}
return keywordFixedTexts;
}
@Override
public CodeCompletionResult complete(final CodeCompletionContext context) {
final List<CompletionProposal> proposals = new ArrayList<>(50);
CodeCompletionResult result = new DefaultCompletionResult(proposals, false);
final ELElement element = getElementAt(context.getParserResult(), context.getCaretOffset());
if (element == null || !element.isValid()) {
return CodeCompletionResult.NONE;
}
final Node target = getTargetNode(element, context.getCaretOffset());
if(target == null) {
//completion called outside of the EL content, resp. inside the
//delimiters #{ or } area
return CodeCompletionResult.NONE;
}
AstPath path = new AstPath(element.getNode());
final List<Node> rootToNode = path.rootToNode(target);
if (rootToNode.isEmpty()) {
return result;
}
final PrefixMatcher prefixMatcher = PrefixMatcher.create(target, context);
if (prefixMatcher == null) {
return CodeCompletionResult.NONE;
}
// see if it is bundle key in the array notation and if so complete them
if (target instanceof AstString) {
ResourceBundles bundle = ResourceBundles.get(getFileObject(context));
String bundleIdentifier = bundle.findResourceBundleIdentifier(path);
if (bundleIdentifier != null) {
proposeBundleKeysInArrayNotation(context, prefixMatcher, element, bundleIdentifier, (AstString) target, proposals);
return proposals.isEmpty() ? CodeCompletionResult.NONE : result;
}
}
final Node nodeToResolve = getNodeToResolve(target, rootToNode);
final Map<AstIdentifier, Node> assignments = getAssignments(context.getParserResult(), context.getCaretOffset());
final FileObject file = context.getParserResult().getSnapshot().getSource().getFileObject();
JavaSource jsource = JavaSource.create(ELTypeUtilities.getElimplExtendedCPI(file));
try {
jsource.runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController info) throws Exception {
info.toPhase(JavaSource.Phase.RESOLVED);
CompilationContext ccontext = CompilationContext.create(file, info);
// assignments to resolve
Node node = nodeToResolve instanceof AstIdentifier && assignments.containsKey((AstIdentifier) nodeToResolve) ?
assignments.get((AstIdentifier) nodeToResolve) : nodeToResolve;
// fetch information from the JSF editor for resolving beans if necessary for cc:interface elements
List<VariableInfo> attrsObjects = Collections.<VariableInfo>emptyList();
if (ELTypeUtilities.isRawObjectReference(ccontext, node, false)) {
attrsObjects = ELVariableResolvers.getRawObjectProperties(ccontext, "attrs", context.getParserResult().getSnapshot()); //NOI18N
}
Element resolved = null;
if (!isInLambda(rootToNode)) {
// resolve the element
resolved = ELTypeUtilities.resolveElement(ccontext, element, nodeToResolve, assignments, attrsObjects);
}
if (ELTypeUtilities.isStaticIterableElement(ccontext, node)) {
proposeStream(ccontext, context, prefixMatcher, proposals);
} else if (ELTypeUtilities.isRawObjectReference(ccontext, node, true)) {
proposeRawObjectProperties(ccontext, context, prefixMatcher, node, proposals);
} else if (ELTypeUtilities.isScopeObject(ccontext, node)) {
// seems to be something like "sessionScope.^", so complete beans from the scope
proposeBeansFromScope(ccontext, context, prefixMatcher, element, node, proposals);
} else if (ELTypeUtilities.isResourceBundleVar(ccontext, node)) {
proposeBundleKeysInDotNotation(context, prefixMatcher, element, node, proposals);
} else if (resolved == null) {
if (target instanceof AstDotSuffix == false && node instanceof AstFunction == false) {
proposeFunctions(ccontext, context, prefixMatcher, element, proposals);
proposeManagedBeans(ccontext, context, prefixMatcher, element, proposals);
proposeBundles(ccontext, context, prefixMatcher, element, proposals);
proposeVariables(ccontext, context, prefixMatcher, element, proposals);
proposeImpicitObjects(ccontext, context, prefixMatcher, proposals);
proposeKeywords(context, prefixMatcher, proposals);
proposeAssignements(context, prefixMatcher, assignments, proposals);
}
if (ELStreamCompletionItem.STREAM_METHOD.equals(node.getImage())) {
proposeOperators(ccontext, context, prefixMatcher, element, proposals, rootToNode, isBracketProperty(target, rootToNode));
}
ELJavaCompletion.propose(ccontext, context, element, target, proposals);
} else {
proposeMethods(ccontext, context, resolved, prefixMatcher, element, proposals, rootToNode, isBracketProperty(target, rootToNode));
if (ELTypeUtilities.isIterableElement(ccontext, resolved)) {
proposeStream(ccontext, context, prefixMatcher, proposals);
}
}
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return proposals.isEmpty() ? CodeCompletionResult.NONE : result;
}
private static boolean isBracketProperty(Node target, List<Node> rootToNode) {
Node previous = rootToNode.get(rootToNode.size() - 1);
return (target instanceof AstString && previous instanceof AstBracketSuffix);
}
private static Node getNodeToResolve(Node target, List<Node> rootToNode) {
Node previous = rootToNode.get(rootToNode.size() - 1);
// due to the ast structure in the case of identifiers we need to try to resolve the type of the identifier,
// otherwise the type of the preceding node.
if (target instanceof AstString && previous instanceof AstBracketSuffix) {
return rootToNode.get(rootToNode.size() - 2);
} else if (target instanceof AstIdentifier
&& ((previous instanceof AstIdentifier || previous instanceof AstDotSuffix || NodeUtil.isMethodCall(previous))
|| target.jjtGetParent() instanceof AstSemiColon)) {
return target;
} else {
for (int i = rootToNode.size() - 1; i >= 0; i--) {
Node node = rootToNode.get(i);
if (isArrayIndexCall(rootToNode, i)) {
// array item call - bean.myArray[0].
return node;
} else if (node instanceof AstMethodArguments) {
// prvious node was method call
return rootToNode.get(i - 1);
} else if (node instanceof AstListData || node instanceof AstMapData) {
return node;
} else if (node.jjtGetParent() instanceof AstSemiColon) {
return previous;
} else if (node instanceof AstIdentifier || node instanceof AstDotSuffix) {
return node;
}
}
return previous;
}
}
private static boolean isArrayIndexCall(List<Node> rootToNode, int nodeIndex) {
for (int i = nodeIndex; i >= 0; i--) {
if (rootToNode.get(nodeIndex) instanceof AstInteger && rootToNode.get(i) instanceof AstBracketSuffix) {
return true;
}
}
return false;
}
private static ELElement getElementAt(ParserResult parserResult, int offset) {
ELParserResult elParserResult = (ELParserResult) parserResult;
ELElement result = elParserResult.getElementAt(offset);
if (result == null || result.isValid()) {
return result;
}
// try to sanitize
ELSanitizer sanitizer = new ELSanitizer(result, offset - result.getOriginalOffset().getStart());
return sanitizer.sanitized();
}
private Map<AstIdentifier, Node> getAssignments(ParserResult parserResult, int offset) {
Map<AstIdentifier, Node> result = new HashMap<>();
ELParserResult elParserResult = (ELParserResult) parserResult;
for (ELElement elElement : elParserResult.getElementsTo(offset)) {
if (elElement.getError() != null) {
elElement = getElementAt(parserResult, offset);
}
if (elElement.getNode() == null) {
continue;
}
AstPath astPath = new AstPath(elElement.getNode());
for (Node node : astPath.rootToLeaf()) {
if (node instanceof AstAssign) {
Node leftSide = node.jjtGetChild(0);
Node leaf = getLastAssigneableLeaf(elElement.getNode());
Node targetNode = getTargetNode(elElement, elElement.getOriginalOffset().getStart() + leaf.endOffset());
Node rightSide = getNodeToResolve(targetNode, astPath.rootToNode(targetNode, true));
if (leftSide instanceof AstIdentifier && rightSide instanceof Node) {
result.put((AstIdentifier) leftSide, rightSide);
}
}
}
}
return result;
}
private static boolean isInLambda(List<Node> rootToNode) {
boolean inLambda = false;
for (int i = rootToNode.size() - 1; i >= 0; i--) {
Node node = rootToNode.get(i);
if (node instanceof AstDotSuffix) {
break;
} else if (node instanceof AstLambdaExpression) {
inLambda = true;
}
}
return inLambda;
}
private static Node getLastAssigneableLeaf(Node root) {
AstPath astPath = new AstPath(root);
for (Node node : astPath.rootToLeaf()) {
if (node instanceof AstSemiColon) {
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
Node childNode = node.jjtGetChild(i);
AstPath path = new AstPath(childNode);
return getNodeToResolve(childNode, path.rootToLeaf());
}
}
}
return getNodeToResolve(root, astPath.rootToLeaf());
}
/**
* @param offset swing <b>document</b> offset
*/
private Node getTargetNode(ELElement element, int offset) {
Node result = element.findNodeAt(offset);
// in EL AST for example #{foo.bar^} the caret is at a deferred expression node, whereas
// for code completion we need the "bar" property node; the code below tries to accomplish
// that
if (result instanceof AstDeferredExpression || result instanceof AstDynamicExpression) {
//ensure we do not jump out from the expression && do not offer EL stuff in delimiters
int relativeOffset = offset - element.getOriginalOffset().getStart();
assert relativeOffset >= 0;
//do the workaround only if the completion called at or after
//the opening EL delimiter #{ or ${, not before or inside it
if(relativeOffset >= 2) {
Node realTarget = element.findNodeAt(offset - 1);
if (realTarget != null) {
result = realTarget;
}
} else {
//ensure no EL completion before or inside the delimiters
return null;
}
}
return result;
}
private FileObject getFileObject(CodeCompletionContext context) {
return context.getParserResult().getSnapshot().getSource().getFileObject();
}
private void proposeOperators(CompilationContext ccontext, CodeCompletionContext context,
PrefixMatcher prefixMatcher, ELElement element, List<CompletionProposal> proposals, List<Node> rootToNode,
boolean isBracketProperty) {
TypeElement streamElement = ccontext.info().getElements().getTypeElement("com.sun.el.stream.Stream"); //NOI18N
if (streamElement != null) {
proposeJavaMethodsForElements(ccontext, context, prefixMatcher, element,
Arrays.<Element>asList(streamElement), isBracketProperty, proposals);
}
}
private void proposeMethods(CompilationContext info, CodeCompletionContext context, Element resolved,
PrefixMatcher prefix, ELElement elElement,List<CompletionProposal> proposals, List<Node> rootToNode,
boolean isBracketProperty) {
List<Element> allTypes = ELTypeUtilities.getSuperTypesFor(info, resolved, elElement, rootToNode);
proposeJavaMethodsForElements(info, context, prefix, elElement, allTypes, isBracketProperty, proposals);
}
private void proposeJavaMethodsForElements(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, List<Element> elements, boolean isBracketCall,
List<CompletionProposal> proposals) {
for(Element element : elements) {
for (ExecutableElement enclosed : ElementFilter.methodsIn(element.getEnclosedElements())) {
//do not propose Object's members
if(element.getSimpleName().contentEquals("Object")) { //NOI18N
//XXX hmm, not an ideal non-fqn check
continue;
}
if (!enclosed.getModifiers().contains(Modifier.PUBLIC) ||
enclosed.getModifiers().contains(Modifier.STATIC)) {
continue;
}
boolean hasParameters = !enclosed.getParameters().isEmpty();
String methodName = enclosed.getSimpleName().toString();
String propertyName = RefactoringUtil.getPropertyName(methodName, enclosed.getReturnType(), true);
if (hasParameters) {
propertyName = methodName;
}
if (!prefix.matches(propertyName)) {
continue;
}
// Now check methodName or propertyName is EL keyword.
if (getKeywordFixedTexts().contains(propertyName) ||
getKeywordFixedTexts().contains(methodName) ) {
continue;
}
if (ELPlugin.Query.isValidProperty(enclosed, context.getParserResult().getSnapshot().getSource(), info, context)) {
ELVariableCompletionItem completionItem = new ELVariableCompletionItem(propertyName, ELTypeUtilities.getTypeNameFor(info, enclosed));
completionItem.setSmart(true);
completionItem.setAnchorOffset(context.getCaretOffset() - prefix.length());
if (!contains(proposals, propertyName)) {
proposals.add(completionItem);
}
} else {
ELJavaCompletionItem completionItem;
if (!contains(proposals, propertyName)) {
if (!hasParameters) {
completionItem = new ELJavaCompletionItem(info, enclosed, elElement, isBracketCall); //
} else {
completionItem = new ELJavaCompletionItem(info, enclosed, methodName, elElement, isBracketCall);
}
completionItem.setSmart(false);
completionItem.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(completionItem);
}
}
}
}
}
private void proposeStream(CompilationContext info, CodeCompletionContext context, PrefixMatcher prefix, List<CompletionProposal> proposals) {
if (prefix.matches(ELStreamCompletionItem.STREAM_METHOD)) {
proposals.add(new ELStreamCompletionItem(context.getCaretOffset() - prefix.length()));
}
}
private boolean contains(List<CompletionProposal> completionProposals, String proposalName) {
if (proposalName == null || proposalName.isEmpty()) {
return true;
}
for (CompletionProposal completionProposal : completionProposals) {
if (proposalName.equals(completionProposal.getName())) {
return true;
}
}
return false;
}
private void proposeImpicitObjects(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, List<CompletionProposal> proposals) {
for (org.netbeans.modules.web.el.spi.ImplicitObject implicitObject : ELTypeUtilities.getImplicitObjects(info)) {
if (prefix.matches(implicitObject.getName())) {
ELImplictObjectCompletionItem item = new ELImplictObjectCompletionItem(implicitObject.getName(), implicitObject.getClazz());
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
proposals.add(item);
}
}
}
private void proposeKeywords(CodeCompletionContext context,
PrefixMatcher prefix, List<CompletionProposal> proposals) {
for (ELTokenId elToken : ELTokenId.values()) {
if (!ELTokenId.ELTokenCategories.KEYWORDS.hasCategory(elToken)) {
continue;
}
if (elToken.fixedText() == null) {
continue;
}
if (prefix.matches(elToken.fixedText())) {
ELKeywordCompletionItem item = new ELKeywordCompletionItem(elToken.fixedText());
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
}
private void proposeAssignements(CodeCompletionContext context, PrefixMatcher prefix,
Map<AstIdentifier, Node> assignments, List<CompletionProposal> proposals) {
for (Map.Entry<AstIdentifier, Node> entry : assignments.entrySet()) {
AstIdentifier variable = entry.getKey();
if (prefix.matches(variable.getImage())) {
ELAssignedVariableCompletionItem item = new ELAssignedVariableCompletionItem(variable.getImage());
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
}
private void proposeManagedBeans(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {
for (VariableInfo bean : ELVariableResolvers.getManagedBeans(info, getFileObject(context))) {
if (!prefix.matches(bean.name)) {
continue;
}
Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
if(element == null) {
continue; //unresolvable bean class name
}
ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, bean.name, elElement);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
if (!contains(proposals, bean.name)) {
proposals.add(item);
}
}
}
private void proposeBeansFromScope(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, Node scopeNode, List<CompletionProposal> proposals) {
String scope = scopeNode.getImage();
// this is ugly, but in the JSF model beans
// are stored to "session", "application" etc (instead of "sessionScope").
// see org.netbeans.modules.web.jsf.api.facesmodel.ManagedBean.Scope
final String scopeString = "Scope";//NOI18N
if (scope.endsWith(scopeString)) {
scope = scope.substring(0, scope.length() - scopeString.length());
}
for (VariableInfo bean : ELVariableResolvers.getBeansInScope(info, scope, context.getParserResult().getSnapshot())) {
if (!prefix.matches(bean.name)) {
continue;
}
Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, elElement);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
proposals.add(item);
}
}
private void proposeRawObjectProperties(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, Node scopeNode, List<CompletionProposal> proposals) {
for (VariableInfo property : ELVariableResolvers.getRawObjectProperties(info, scopeNode.getImage(), context.getParserResult().getSnapshot())) {
if (!prefix.matches(property.name)) {
continue;
}
ELRawObjectPropertyCompletionItem item = new ELRawObjectPropertyCompletionItem(property.name);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
proposals.add(item);
}
}
private void proposeVariables(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {
for (VariableInfo bean : ELVariableResolvers.getVariables(info, context.getParserResult().getSnapshot(), context.getCaretOffset())) {
if (!prefix.matches(bean.name)) {
continue;
}
if(bean.clazz == null) {
//probably a refered (w/o type) variable, just show it in the completion w/o type
ELVariableCompletionItem item = new ELVariableCompletionItem(bean.name, bean.expression);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
proposals.add(item);
} else {
//resolved variable
Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
if (element == null) {
continue;
}
ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, elElement);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
item.setSmart(true);
proposals.add(item);
}
}
}
private void proposeBundles(CompilationContext ccontext, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {
ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
if (!resourceBundles.canHaveBundles()) {
return;
}
for (ResourceBundle bundle : resourceBundles.getBundles(ccontext.context())) {
if (!prefix.matches(bundle.getVar())) {
continue;
}
ELResourceBundleCompletionItem item = new ELResourceBundleCompletionItem(ccontext.file(), bundle, resourceBundles);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
private void proposeBundleKeysInArrayNotation(CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, String bundleKey, AstString target, List<CompletionProposal> proposals) {
if (target.getImage().isEmpty()
|| elElement.getOriginalOffset(target).getStart() >= context.getCaretOffset()) {
return;
}
ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
if (!resourceBundles.canHaveBundles()) {
return;
}
for (Map.Entry<String, String> entry : resourceBundles.getEntries(bundleKey).entrySet()) {
if (!prefix.matches(entry.getKey())) {
continue;
}
ELResourceBundleKeyCompletionItem item = new ELResourceBundleKeyCompletionItem(entry.getKey(), entry.getValue(), elElement);
item.setSmart(true);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
// "msg.key" notation
private void proposeBundleKeysInDotNotation(CodeCompletionContext context,
PrefixMatcher prefix,
ELElement elElement,
Node baseObjectNode,
List<CompletionProposal> proposals) {
String bundleKey = baseObjectNode.getImage();
ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
if (!resourceBundles.canHaveBundles()) {
return;
}
for (Map.Entry<String, String> entry : resourceBundles.getEntries(bundleKey).entrySet()) {
if (!prefix.matches(entry.getKey())) {
continue;
}
ELResourceBundleKeyCompletionItem item = new ELResourceBundleKeyCompletionItem(entry.getKey(), entry.getValue(), elElement);
item.setSmart(true);
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
private void proposeFunctions(CompilationContext info, CodeCompletionContext context,
PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {
for (Function function : ELTypeUtilities.getELFunctions(info)) {
if (!prefix.matches(function.getName())) {
continue;
}
ELFunctionCompletionItem item = new ELFunctionCompletionItem(
function.getName(),
function.getReturnType(),
function.getParameters(),
function.getDescription());
item.setAnchorOffset(context.getCaretOffset() - prefix.length());
proposals.add(item);
}
}
@Override
public String document(ParserResult info, final ElementHandle element) {
Documentation doc = documentElement(info, element, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return false;
}
});
if (doc != null) {
return doc.getContent();
}
return null;
}
@Override
public ElementHandle resolveLink(String link, ElementHandle originalHandle) {
return null;
}
@Override
public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) {
ELElement element = getElementAt(info, caretOffset);
if (element == null) {
return null;
}
Node node = element.findNodeAt(caretOffset);
// get the prefix for bundle keys
if (node instanceof AstString) {
int startOffset = element.getOriginalOffset(node).getStart();
int end = caretOffset - startOffset;
String image = node.getImage();
if (end > 0 && !image.isEmpty()) {
// index 0 in AstString#image is either ' or ",
// so start from 1
return image.substring(1, end);
}
}
// use the default CSL behavior for getting the prefix
return null;
}
@Override
public QueryType getAutoQuery(JTextComponent component, String typedText) {
assert typedText.length() > 0;
char last = typedText.charAt(typedText.length() - 1);
switch(last) {
case '.':
return QueryType.COMPLETION;
}
return QueryType.NONE;
}
@Override
public String resolveTemplateVariable(String variable, ParserResult info, int caretOffset, String name, Map parameters) {
return null;
}
@Override
public Set<String> getApplicableTemplates(Document doc, int selectionBegin, int selectionEnd) {
return Collections.emptySet();
}
@Override
public ParameterInfo parameters(ParserResult info, int caretOffset, CompletionProposal proposal) {
return ParameterInfo.NONE;
}
@Override
public Documentation documentElement(ParserResult info, ElementHandle element, Callable<Boolean> cancel) {
if (!(element instanceof ELElementHandle)) {
return null;
}
return ((ELElementHandle) element).document(info, cancel);
}
/*package*/ static class PrefixMatcher {
private final String prefix;
private final boolean exact;
private final boolean caseSensitive;
private PrefixMatcher(String value, boolean exact, boolean caseSensitive) {
this.prefix = value;
this.exact = exact;
this.caseSensitive = caseSensitive;
}
static PrefixMatcher create(Node target, CodeCompletionContext context) {
String prefix = context.getPrefix() != null ? context.getPrefix() : "";
boolean isDoc = context.getQueryType() == QueryType.DOCUMENTATION;
if (isDoc) {
prefix = getPrefixForDocumentation(target);
}
// for documentation we need full prefix
if (isDoc && prefix.isEmpty()) {
return null;
}
return new PrefixMatcher(prefix, isDoc, context.isCaseSensitive());
}
static PrefixMatcher create(String prefix, CodeCompletionContext context) {
return new PrefixMatcher(prefix, false, context.isCaseSensitive());
}
private static String getPrefixForDocumentation(Node target) {
if (target instanceof AstString) {
return ((AstString) target).getString();
}
return target.getImage() == null ? "" : target.getImage();
}
boolean matches(String str) {
if (exact) {
return prefix.equals(str);
} else if (caseSensitive) {
return str.startsWith(prefix);
} else {
return str.toLowerCase().startsWith(prefix.toLowerCase());
}
}
int length() {
return prefix.length();
}
public String getPrefix() {
return prefix;
}
}
}