blob: 3b1b87e149e4ee192cf82ef8de570ae071b2dc09 [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.javascript2.editor;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.Model;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.ModelUtils;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.*;
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.css.indexing.api.CssIndex;
import org.netbeans.modules.html.editor.lib.api.HtmlVersion;
import org.netbeans.modules.html.editor.lib.api.model.HtmlModel;
import org.netbeans.modules.html.editor.lib.api.model.HtmlModelFactory;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTag;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTagAttribute;
import org.netbeans.modules.javascript2.editor.spi.CompletionContext;
import org.netbeans.modules.javascript2.editor.JsCompletionItem.CompletionRequest;
import org.netbeans.modules.javascript2.editor.doc.JsDocumentationCodeCompletion;
import org.netbeans.modules.javascript2.editor.doc.JsDocumentationElement;
import org.netbeans.modules.javascript2.model.api.IndexedElement;
import org.netbeans.modules.javascript2.lexer.api.JsDocumentationTokenId;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.editor.options.OptionsUtils;
import org.netbeans.modules.javascript2.editor.parser.JsParserResult;
import static org.netbeans.modules.javascript2.editor.spi.CompletionContext.EXPRESSION;
import static org.netbeans.modules.javascript2.editor.spi.CompletionContext.OBJECT_MEMBERS;
import static org.netbeans.modules.javascript2.editor.spi.CompletionContext.OBJECT_PROPERTY;
import org.netbeans.modules.javascript2.editor.spi.CompletionProvider;
import org.netbeans.modules.javascript2.model.api.Index;
import org.netbeans.modules.javascript2.types.api.Identifier;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.indexing.support.IndexResult;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
/**
*
* @author Petr Pisl
*/
class JsCodeCompletion implements CodeCompletionHandler2 {
private static final Logger LOGGER = Logger.getLogger(JsCodeCompletion.class.getName());
private static final List<String> WINDOW_EXPRESSION_CHAIN = Arrays.<String>asList("window", "@pro"); //NOI18N
private boolean caseSensitive;
private static final String CHARS_NO_AUTO_COMPLETE = ";,/+-\\:={}[]()"; //NOI18N
@Override
public CodeCompletionResult complete(CodeCompletionContext ccContext) {
final CancelSupport cancelSupport = CancelSupport.getDefault();
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
long start = System.currentTimeMillis();
BaseDocument doc = (BaseDocument) ccContext.getParserResult().getSnapshot().getSource().getDocument(false);
if (doc == null) {
return CodeCompletionResult.NONE;
}
this.caseSensitive = ccContext.isCaseSensitive();
ParserResult info = ccContext.getParserResult();
int caretOffset = ccContext.getParserResult().getSnapshot().getEmbeddedOffset(ccContext.getCaretOffset());
FileObject fileObject = ccContext.getParserResult().getSnapshot().getSource().getFileObject();
JsParserResult jsParserResult = (JsParserResult)info;
CompletionContext context = CompletionContextFinder.findCompletionContext(info, caretOffset);
LOGGER.log(Level.FINE, String.format("CC context: %s", context.toString()));
JsCompletionItem.CompletionRequest request = new JsCompletionItem.CompletionRequest();
String pref = ccContext.getPrefix();
//pref = pref == null ? "" : pref;
request.anchor = pref == null ? caretOffset : caretOffset
// can't just use 'prefix.getLength()' here cos it might have been calculated with
// the 'upToOffset' flag set to false
- pref.length();
request.result = jsParserResult;
request.info = info;
request.prefix = pref;
request.completionContext = context;
request.addHtmlTagAttributes = false;
request.cancelSupport = cancelSupport;
Model.getModel(jsParserResult, false).resolve();
final List<CompletionProposal> resultList = new ArrayList<CompletionProposal>();
HashMap<String, List<JsElement>> added = new HashMap<String, List<JsElement>>();
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
if (ccContext.getQueryType() == QueryType.ALL_COMPLETION) {
switch (context) {
case GLOBAL:
addGlobalObjectsFromIndex(request, added);
break;
case EXPRESSION:
completeKeywords(request, resultList);
completeExpression(request, added);
break;
case OBJECT_PROPERTY:
completeObjectProperty(request, added);
break;
case OBJECT_MEMBERS:
completeObjectMember(request, added);
break;
default:
break;
}
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
if ((context == CompletionContext.EXPRESSION || context == CompletionContext.OBJECT_MEMBERS || context == CompletionContext.OBJECT_PROPERTY) && !request.prefix.isEmpty()) {
Collection<? extends IndexResult> indexResults = Index.get(fileObject).query(Index.FIELD_BASE_NAME, request.prefix, QuerySupport.Kind.PREFIX, Index.TERMS_BASIC_INFO);
for (IndexResult indexResult : indexResults) {
IndexedElement indexElement = IndexedElement.create(indexResult);
addPropertyToMap(request, added, indexElement);
}
}
} else {
switch (context) {
case IN_STRING:
//XXX should be treated in the getPrefix method, but now
// there is hardcoded behavior for jQuery
if (request.prefix.startsWith(".")) {
request.prefix = request.prefix.substring(1);
request.anchor = request.anchor + 1;
}
List<String> expression = resolveExpressionChainFromString(request);
Map<String, List<JsElement>> toAdd = getCompletionFromExpressionChain(request, expression);
// create code completion results
JsCompletionItem.Factory.create(toAdd, request, resultList);
break;
case STRING_ELEMENTS_BY_ID:
completeTagIds(request, resultList);
break;
case STRING_ELEMENTS_BY_CLASS_NAME:
completeCSSClassNames(request, resultList);
break;
case IMPORT_EXPORT_SPECIAL_TOKENS:
addImportExportKeywords(request, resultList);
break;
case IMPORT_EXPORT_MODULE:
completeJsModuleNames(request, resultList);
break;
case GLOBAL:
HashMap<String, List<JsElement>> addedProperties = new HashMap<String, List<JsElement>>();
addedProperties.putAll(getDomCompletionResults(request));
for (JsObject libGlobal : ModelUtils.getExtendingGlobalObjects(fileObject)) {
for (JsObject object : libGlobal.getProperties().values()) {
addPropertyToMap(request, addedProperties, object);
}
}
for (JsObject object : Model.getModel(request.result, false).getVariables(caretOffset)) {
if (!(object instanceof JsFunction && ((JsFunction) object).isAnonymous())) {
addPropertyToMap(request, addedProperties, object);
}
}
completeKeywords(request, resultList);
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
addGlobalObjectsFromIndex(request, addedProperties);
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
completeInWith(request, addedProperties);
JsCompletionItem.Factory.create(addedProperties, request, resultList);
break;
case CALL_ARGUMENT:
completeCallArguments(request, resultList);
case EXPRESSION:
completeKeywords(request, resultList);
completeExpression(request, added);
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
completeObjectProperty(request, added);
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
completeInWith(request, added);
added.remove(ModelUtils.PROTOTYPE);
break;
case OBJECT_PROPERTY:
completeObjectProperty(request, added);
break;
case OBJECT_MEMBERS:
completeObjectMember(request, added);
break;
case DOCUMENTATION:
JsDocumentationCodeCompletion.complete(request, resultList);
break;
case OBJECT_PROPERTY_NAME:
completeObjectPropertyName(request, added);
break;
case NUMBER:
completeNumberProperties(request, added);
break;
case STRING:
completeStringProperties(request, added);
break;
case REGEXP:
completeRegExpProperties(request, added);
break;
default:
break;
}
}
JsCompletionItem.Factory.create(added, request, resultList);
if (request.addHtmlTagAttributes) {
completeTagAttributes(request, resultList);
}
long end = System.currentTimeMillis();
LOGGER.log(Level.FINE, "Counting JS CC took {0}ms ", (end - start));
for (CompletionProvider interceptor : EditorExtender.getDefault().getCompletionProviders()) {
resultList.addAll(interceptor.complete(ccContext, context, pref));
}
if (!resultList.isEmpty()) {
return new DefaultCompletionResult(resultList, false);
}
return CodeCompletionResult.NONE;
}
private void addGlobalObjectsFromIndex(CompletionRequest request, HashMap<String, List<JsElement>> addedProperties) {
FileObject fileObject = request.result.getSnapshot().getSource().getFileObject();
if (fileObject != null) {
Index jsIndex = Index.get(fileObject);
Collection<IndexedElement> fromIndex = jsIndex.getGlobalVar(request.prefix);
for (IndexedElement indexElement : fromIndex) {
addPropertyToMap(request, addedProperties, indexElement);
}
fromIndex = jsIndex.getPropertiesWithPrefix("window", request.prefix);
for (IndexedElement indexElement : fromIndex) {
addPropertyToMap(request, addedProperties, indexElement);
}
}
}
@Override
public String document(ParserResult info, 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 Documentation documentElement(ParserResult info, ElementHandle element, Callable<Boolean> cancel) {
if (element == null) {
return null;
}
if (element instanceof IndexedElement) {
final Documentation[] result = new Documentation[1];
final IndexedElement indexedElement = (IndexedElement)element;
FileObject nextFo = indexedElement.getFileObject();
if (nextFo != null) {
try {
ParserManager.parse(Collections.singleton(Source.create(nextFo)), new UserTask () {
@Override
public void run(ResultIterator resultIterator) throws Exception {
Result parserResult = resultIterator.getParserResult();
if (parserResult instanceof JsParserResult) {
JsParserResult jsInfo = (JsParserResult)parserResult;
String fqn = indexedElement.getFQN();
JsObject jsObjectGlobal = Model.getModel(jsInfo, false).getGlobalObject();
JsObject property = ModelUtils.findJsObjectByName(jsObjectGlobal, fqn);
if (property != null) {
Documentation doc = property.getDocumentation();
result[0] = doc;
}
} else {
LOGGER.log(Level.INFO, "Not instance of JsParserResult: {0}", parserResult);
}
}
});
} catch (ParseException ex) {
LOGGER.log(Level.WARNING, null, ex);
}
}
if (result[0] != null) {
return result[0];
}
} else if (element instanceof JsObject) {
JsObject jsObject = (JsObject) element;
if (jsObject.getDocumentation() != null) {
return jsObject.getDocumentation();
}
}
for (CompletionProvider interceptor : EditorExtender.getDefault().getCompletionProviders()) {
String doc = interceptor.getHelpDocumentation(info, element);
if (doc != null && !doc.isEmpty()) {
return Documentation.create(doc);
}
}
if (element instanceof JsDocumentationElement) {
String documentation = ((JsDocumentationElement) element).getDocumentation();
return documentation != null ? Documentation.create(documentation) : null;
}
if (element instanceof JsCompletionItem.SimpleDocElement) {
String documentation = ((JsCompletionItem.SimpleDocElement) element).getDocumentation();
return documentation != null ? Documentation.create(documentation) : null;
}
if (OffsetRange.NONE.equals(element.getOffsetRange(info))) {
return Documentation.create(NbBundle.getMessage(JsCodeCompletion.class, "MSG_ItemFromUsageDoc"));
}
return Documentation.create(NbBundle.getMessage(JsCodeCompletion.class, "MSG_DocNotAvailable"));
}
@Override
public ElementHandle resolveLink(String link, ElementHandle originalHandle) {
return null;
}
@Override
public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) {
String prefix = "";
BaseDocument doc = (BaseDocument) info.getSnapshot().getSource().getDocument(false);
if (doc == null) {
return null;
}
//caretOffset = info.getSnapshot().getEmbeddedOffset(caretOffset);
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(info.getSnapshot(), caretOffset);
if (ts == null) {
return null;
}
int offset = info.getSnapshot().getEmbeddedOffset(caretOffset);
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
if (ts.offset() == offset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
ts.movePrevious();
}
Token<? extends JsTokenId> token = ts.token();
if (token != null && token.id() != JsTokenId.EOL) {
JsTokenId id = token.id();
if (id == JsTokenId.STRING_END && ts.movePrevious()) {
if (ts.token().id() == JsTokenId.STRING_BEGIN) {
return "";
} else {
ts.moveNext();
}
}
if (id == JsTokenId.STRING) {
prefix = token.text().toString();
if (upToOffset) {
int end = offset - ts.offset();
int prefixIndex = getPrefixIndexFromSequence(prefix.substring(0, end));
prefix = prefix.substring(prefixIndex, end);
}
}
if (id == JsTokenId.IDENTIFIER || id.isKeyword()) {
prefix = token.text().toString();
if (upToOffset) {
int end = offset - ts.offset();
if (end >= 0) {
prefix = prefix.substring(0, Math.min(end, prefix.length()));
}
}
}
if (id == JsTokenId.DOC_COMMENT) {
TokenSequence<? extends JsDocumentationTokenId> docTokenSeq =
LexUtilities.getJsDocumentationTokenSequence(info.getSnapshot(), offset);
if (docTokenSeq == null) {
return null;
}
docTokenSeq.move(offset);
// initialize moved token
if (!docTokenSeq.moveNext() && !docTokenSeq.movePrevious()) {
return null;
}
if (docTokenSeq.token().id() == JsDocumentationTokenId.KEYWORD) {
// inside the keyword tag
prefix = docTokenSeq.token().text().toString();
if (upToOffset) {
prefix = prefix.substring(0, offset - docTokenSeq.offset());
}
} else {
// get the token before
docTokenSeq.movePrevious();
prefix = docTokenSeq.token().text().toString();
}
}
if (id.isError()) {
prefix = token.text().toString();
//if (upToOffset) {
prefix = prefix.substring(0, offset - ts.offset());
//}
}
}
LOGGER.log(Level.FINE, String.format("Prefix for cc: %s", prefix));
return prefix;
}
@Override
public QueryType getAutoQuery(JTextComponent component, String typedText) {
if (typedText.length() == 0) {
return QueryType.NONE;
}
int offset = component.getCaretPosition();
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(component.getDocument(), offset);
if (ts != null) {
int diff = ts.move(offset);
TokenId currentTokenId = null;
if (diff == 0 && ts.movePrevious() || ts.moveNext()) {
currentTokenId = ts.token().id();
}
char lastChar = typedText.charAt(typedText.length() - 1);
if (currentTokenId == JsTokenId.BLOCK_COMMENT || currentTokenId == JsTokenId.DOC_COMMENT
|| currentTokenId == JsTokenId.LINE_COMMENT) {
if (lastChar == '@') { //NOI18N
return QueryType.COMPLETION;
}
} else if (currentTokenId == JsTokenId.STRING && lastChar == '/') {
return QueryType.COMPLETION;
} else {
switch (lastChar) {
case '.': //NOI18N
if (OptionsUtils.forLanguage(JsTokenId.javascriptLanguage()).autoCompletionAfterDot()) {
return QueryType.COMPLETION;
}
break;
default:
if (OptionsUtils.forLanguage(JsTokenId.javascriptLanguage()).autoCompletionFull()) {
if (!Character.isWhitespace(lastChar) && CHARS_NO_AUTO_COMPLETE.indexOf(lastChar) == -1) {
return QueryType.COMPLETION;
}
}
return QueryType.NONE;
}
}
}
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) {
// must return null - CSL reasons, see #217101 for more information
return null;
}
@Override
public ParameterInfo parameters(ParserResult info, int caretOffset, CompletionProposal proposal) {
// TODO needs to be implemented.
return ParameterInfo.NONE;
}
private void completeExpression(CompletionRequest request, HashMap <String, List<JsElement>> addedItems) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
addedItems.putAll(getDomCompletionResults(request));
// from index
Index index = Index.get(fo);
Collection<IndexedElement> fromIndex = index.getGlobalVar(request.prefix);
for (IndexedElement indexedElement : fromIndex) {
addPropertyToMap(request, addedItems, indexedElement);
}
// from libraries
for (JsObject libGlobal : ModelUtils.getExtendingGlobalObjects(fo)) {
for (JsObject object : libGlobal.getProperties().values()) {
addPropertyToMap(request, addedItems, object);
}
}
// from model
//int offset = request.info.getSnapshot().getEmbeddedOffset(request.anchor);
for(JsObject object : Model.getModel(request.result, false).getVariables(request.anchor)) {
if (!(object instanceof JsFunction && ((JsFunction) object).isAnonymous())) {
addPropertyToMap(request, addedItems, object);
}
}
// addedItems.putAll(getWithCompletionResults(request, null));
}
private int checkRecursion;
private void completeObjectProperty(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
List<String> expChain = ModelUtils.resolveExpressionChain(request.result.getSnapshot(), request.anchor, false);
if (!expChain.isEmpty()) {
Map<String, List<JsElement>> toAdd = getCompletionFromExpressionChain(request, expChain);
if (request.cancelSupport.isCancelled()) {
return;
}
FileObject fo = request.result.getSnapshot().getSource().getFileObject();
if (fo != null) {
long start = System.currentTimeMillis();
Collection<IndexedElement> fromUsages = Index.get(request.result.getSnapshot().getSource().getFileObject()).getUsagesFromExpression(expChain);
for (IndexedElement indexedElement : fromUsages) {
if (!fo.equals(indexedElement.getFileObject()) || !indexedElement.getName().equals(request.prefix)) {
addPropertyToMap(request, addedItems, indexedElement);
}
}
long end = System.currentTimeMillis();
LOGGER.log(Level.FINE, String.format("Counting cc based on usages took: %dms", (end - start)));
}
addedItems.putAll(toAdd);
}
}
private Map<String, List<JsElement>> getCompletionFromExpressionChain(CompletionRequest request, List<String> expChain) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
Collection<TypeUsage> resolveTypeFromExpression = new ArrayList<TypeUsage>();
HashMap<String, List<JsElement>> addedProperties = new HashMap<String, List<JsElement>>();
resolveTypeFromExpression.addAll(ModelUtils.resolveTypeFromExpression(Model.getModel(request.result, false), jsIndex, expChain, request.anchor, true));
if (request.cancelSupport.isCancelled()) {
return addedProperties;
}
resolveTypeFromExpression = ModelUtils.resolveTypes(resolveTypeFromExpression, Model.getModel(request.result, false), jsIndex, true);
// try to map window property
Collection<String> windowProp = new ArrayList<String>();
for (TypeUsage typeUsage : resolveTypeFromExpression) {
if (typeUsage.isResolved() && !typeUsage.getType().startsWith("window")) {
windowProp.add("window." + typeUsage.getType());
}
}
Collection<String> prototypeChain = new ArrayList<String>();
for (TypeUsage typeUsage : resolveTypeFromExpression) {
prototypeChain.addAll(ModelUtils.findPrototypeChain(typeUsage.getType(), jsIndex));
}
for (String string : windowProp) {
resolveTypeFromExpression.add(new TypeUsage(string));
}
for (String string : prototypeChain) {
resolveTypeFromExpression.add(new TypeUsage(string));
}
if (request.cancelSupport.isCancelled()) {
return addedProperties;
}
boolean isFunction = false; // addding Function to the prototype chain?
List<JsObject> lastResolvedObjects = new ArrayList<JsObject>();
for (TypeUsage typeUsage : resolveTypeFromExpression) {
checkRecursion = 0;
boolean addFunctionProp = processTypeInModel(request, Model.getModel(request.result, false), typeUsage, lastResolvedObjects, expChain.get(1).equals("@pro"), jsIndex, addedProperties);
isFunction = isFunction || addFunctionProp;
if (typeUsage.isResolved()) {
addObjectPropertiesFromIndex(typeUsage.getType(), jsIndex, request, addedProperties, true);
}
}
boolean isPublic = lastResolvedObjects.isEmpty();
for (JsObject resolved : lastResolvedObjects) {
if(!isFunction && resolved.getJSKind().isFunction()) {
isFunction = true;
}
addObjectPropertiesToCC(resolved, request, addedProperties);
if (!resolved.isDeclared()) {
// if the object is not defined here, look to the index as well
addObjectPropertiesFromIndex(resolved.getFullyQualifiedName(), jsIndex, request, addedProperties, true);
isPublic = true;
} else {
if (!resolved.getModifiers().contains(Modifier.PRIVATE)) {
isPublic = true;
}
}
}
if (isFunction) {
addObjectPropertiesFromIndex("Function", jsIndex, request, addedProperties, true); //NOI18N
}
if (request.cancelSupport.isCancelled()) {
return addedProperties;
}
addObjectPropertiesFromIndex("Object", jsIndex, request, addedProperties, true); //NOI18N
if (isPublic) {
// now look to the index again for declared item outside
StringBuilder fqn = new StringBuilder();
for (int i = expChain.size() - 1; i > -1; i--) {
fqn.append(expChain.get(--i));
fqn.append('.');
}
if (fqn.length() > 0) {
Collection<IndexedElement> indexResults = jsIndex.getPropertiesWithPrefix(fqn.toString().substring(0, fqn.length() - 1), request.prefix);
for (IndexedElement indexedElement : indexResults) {
if (!indexedElement.isAnonymous()
&& indexedElement.getModifiers().contains(Modifier.PUBLIC)) {
addPropertyToMap(request, addedProperties, indexedElement);
}
}
}
}
return addedProperties;
}
private Identifier findNameOfFunctionCall (CompletionRequest request) {
// is an argument of a function call?
TokenHierarchy<?> th = request.result.getSnapshot().getTokenHierarchy();
if (th == null) {
return null;
}
TokenSequence<JsTokenId> ts = th.tokenSequence(JsTokenId.javascriptLanguage());
if (ts == null) {
return null;
}
ts.move(request.anchor);
if (!ts.moveNext() && !ts.movePrevious()){
return null;
}
int curlyDeep = 0;
Token<? extends JsTokenId> token = ts.token();
JsTokenId tokenId = token.id();
while (ts.movePrevious() && tokenId != JsTokenId.BRACKET_LEFT_PAREN
&& tokenId != JsTokenId.OPERATOR_SEMICOLON) {
if (tokenId == JsTokenId.BRACKET_LEFT_CURLY) {
curlyDeep++;
}
token = ts.token();
tokenId = token.id();
}
if (tokenId == JsTokenId.BRACKET_LEFT_PAREN) {
token = LexUtilities.findPreviousNonWsNonComment(ts);
if (token != null && token.id() == JsTokenId.IDENTIFIER) {
String functionName = token.text().toString();
return new Identifier(functionName, new OffsetRange(ts.offset(), ts.offset() + functionName.length()));
}
}
return null;
}
private List<IndexedElement.FunctionIndexedElement> findFunctionInIndex(Identifier functionName, CompletionRequest request) {
List<IndexedElement.FunctionIndexedElement> result = new ArrayList<IndexedElement.FunctionIndexedElement>();
List<String> expChain = ModelUtils.resolveExpressionChain(request.result.getSnapshot(), functionName.getOffsetRange().getStart() - 1, false);
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
if (fo != null) {
Index jsIndex = Index.get(fo);
if (expChain.isEmpty()) {
// global space
Collection<IndexedElement> globalVars = jsIndex.getGlobalVar(functionName.getName());
for (IndexedElement globalVar : globalVars) {
if (globalVar.getName().equals(functionName.getName()) && globalVar.getJSKind().isFunction()) {
result.add((IndexedElement.FunctionIndexedElement)globalVar);
}
}
} else {
// the expression needs to be resolved
Collection<TypeUsage> types = ModelUtils.resolveTypeFromExpression(Model.getModel(request.result, false), jsIndex, expChain, request.anchor, false);
for (TypeUsage type : types) {
Collection<IndexedElement> properties = jsIndex.getPropertiesWithPrefix(type.getType(), functionName.getName());
properties.addAll(jsIndex.getPropertiesWithPrefix(type.getType() + "." + ModelUtils.PROTOTYPE, functionName.getName()));
for (IndexedElement property : properties) {
if (property.getName().equals(functionName.getName()) && property.getJSKind().isFunction()) {
IndexedElement.FunctionIndexedElement function = (IndexedElement.FunctionIndexedElement)property;
result.add(function);
}
}
}
}
}
return result;
}
private List<TypeUsage> findPossibleCallArgTypes(CompletionRequest request) {
Identifier functionName = findNameOfFunctionCall(request);
if (functionName == null) {
// probably not in a call
return null;
}
List<TypeUsage> result = new ArrayList<TypeUsage>();
List<IndexedElement.FunctionIndexedElement> functions = findFunctionInIndex(functionName, request);
for (IndexedElement.FunctionIndexedElement function : functions) {
LinkedHashMap<String, Collection<String>> parameters = function.getParameters();
for (Collection<String> assignments: parameters.values()) {
if (!assignments.isEmpty()) {
for (String assignment : assignments) {
result.add(new TypeUsage(assignment));
}
}
}
}
return result;
}
private void completeObjectPropertyName(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
// is an argument of the function call?
TokenHierarchy<?> th = request.result.getSnapshot().getTokenHierarchy();
if (th == null) {
return;
}
TokenSequence<JsTokenId> ts = th.tokenSequence(JsTokenId.javascriptLanguage());
if (ts == null) {
return;
}
ts.move(request.anchor);
if (!ts.moveNext() && !ts.movePrevious()){
return;
}
int curlyDeep = 0;
Token<? extends JsTokenId> token = ts.token();
JsTokenId tokenId = token.id();
while (ts.movePrevious() && tokenId != JsTokenId.BRACKET_LEFT_PAREN
&& tokenId != JsTokenId.OPERATOR_SEMICOLON) {
if (tokenId == JsTokenId.BRACKET_LEFT_CURLY) {
curlyDeep++;
}
token = ts.token();
tokenId = token.id();
}
// what is the function?
if (curlyDeep == 1 && tokenId == JsTokenId.BRACKET_LEFT_PAREN) {
token = LexUtilities.findPreviousNonWsNonComment(ts);
if (token != null && token.id() == JsTokenId.IDENTIFIER) {
String functionName = token.text().toString();
List<String> expChain = ModelUtils.resolveExpressionChain(request.result.getSnapshot(), ts.offset() - 1, false);
List<TypeUsage> possibleTypes = new ArrayList<TypeUsage>();
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
if (expChain.isEmpty()) {
// global space
Collection<? extends JsObject> variables = ModelUtils.getVariables(Model.getModel(request.result, false), request.anchor);
for (JsObject variable : variables) {
if (variable.getName().equals(functionName) && variable.getJSKind().isFunction()) {
// do we now the tape of the argument?
JsFunction function = (JsFunction)variable;
Collection<? extends JsObject> parameters = function.getParameters();
for (JsObject parameter: parameters) {
if (!parameter.getAssignments().isEmpty()) {
possibleTypes.addAll(parameter.getAssignments());
}
}
break;
}
}
Collection<IndexedElement> globalVars = jsIndex.getGlobalVar(functionName);
for (IndexedElement globalVar : globalVars) {
if (globalVar.getName().equals(functionName) && globalVar.getJSKind().isFunction()) {
IndexedElement.FunctionIndexedElement function = (IndexedElement.FunctionIndexedElement)globalVar;
LinkedHashMap<String, Collection<String>> parameters = function.getParameters();
for (Collection<String> assignments: parameters.values()) {
if (!assignments.isEmpty()) {
for (String type : assignments) {
possibleTypes.add(new TypeUsage(type));
}
}
}
}
}
} else {
Collection<TypeUsage> types = ModelUtils.resolveTypeFromExpression(Model.getModel(request.result, false), jsIndex, expChain, request.anchor, false);
for (TypeUsage type : types) {
Collection<IndexedElement> properties = jsIndex.getPropertiesWithPrefix(type.getType(), functionName);
properties.addAll(jsIndex.getPropertiesWithPrefix(type.getType() + "." + ModelUtils.PROTOTYPE, functionName));
for (IndexedElement property : properties) {
if (property.getName().equals(functionName) && property.getJSKind().isFunction()) {
IndexedElement.FunctionIndexedElement function = (IndexedElement.FunctionIndexedElement)property;
LinkedHashMap<String, Collection<String>> parameters = function.getParameters();
for (Collection<String> assignments: parameters.values()) {
if (!assignments.isEmpty()) {
for (String assignment : assignments) {
possibleTypes.add(new TypeUsage(assignment));
}
}
}
}
}
}
}
if (!possibleTypes.isEmpty()) {
for (TypeUsage type : possibleTypes) {
addObjectPropertiesFromIndex(type.getType(), jsIndex, request, addedItems, true);
}
}
}
}
}
private void completeNumberProperties(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
addObjectPropertiesFromIndex("Number", jsIndex, request, addedItems, false); // NOI18N
addObjectPropertiesFromIndex("Object", jsIndex, request, addedItems, false); // NOI18N
}
private void completeStringProperties(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
addObjectPropertiesFromIndex("String", jsIndex, request, addedItems, false); // NOI18N
addObjectPropertiesFromIndex("Object", jsIndex, request, addedItems, false); // NOI18N
}
private void completeRegExpProperties(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
addObjectPropertiesFromIndex("RegExp", jsIndex, request, addedItems, false); // NOI18N
addObjectPropertiesFromIndex("Object", jsIndex, request, addedItems, false); // NOI18N
}
private List<String> resolveExpressionChainFromString(CompletionRequest request) {
TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy();
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(th, request.anchor);
if (ts == null) {
return Collections.<String>emptyList();
}
int offset = request.info.getSnapshot().getEmbeddedOffset(request.anchor);
ts.move(offset);
String text = null;
if (ts.moveNext()) {
if (ts.token().id() == JsTokenId.STRING_END) {
if (ts.movePrevious() && ts.token().id() == JsTokenId.STRING) {
text = ts.token().text().toString();
}
} else if (ts.token().id() == JsTokenId.STRING) {
text = ts.token().text().toString().substring(0, offset - ts.offset());
}
}
if (text != null && !text.isEmpty()) {
int index = text.length() - 1;
List<String> exp = new ArrayList<String>();
int parenBalancer = 0;
boolean methodCall = false;
char ch = text.charAt(index);
String part = "";
while (index > -1 && ch != ' ' && ch != '\n' && ch != ';' && ch != '}'
&& ch != '{' && ch != '(' && ch != '=' && ch != '+' && ch != '[') {
if (ch == '.') {
if (!part.isEmpty()) {
exp.add(part);
part = "";
if (methodCall) {
exp.add("@mtd");
methodCall = false;
} else {
exp.add("@pro");
}
}
} else {
if (ch == ')') {
parenBalancer++;
methodCall = true;
while (parenBalancer > 0 && --index > -1) {
ch = text.charAt(index);
if (ch == ')') {
parenBalancer++;
} else if (ch == '(') {
parenBalancer--;
}
}
} else {
part = ch + part;
}
}
if (--index > -1) {
ch = text.charAt(index);
}
}
if (!part.isEmpty()) {
exp.add(part);
if (methodCall) {
exp.add("@mtd");
} else {
exp.add("@pro");
}
}
return exp;
}
return Collections.<String>emptyList();
}
/**
*
* @param request
* @param offset offset where the expression should be resolved
* @param lookBefore if yes, looks for the beginning of the expression before the offset,
* if no, it can be in a middle of expression
* @return
*/
private List<String> resolveExpressionChain(CompletionRequest request, int offset, boolean lookBefore) {
TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy();
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(th, offset);
if (ts == null) {
return Collections.<String>emptyList();
}
ts.move(offset);
if (ts.movePrevious() && (ts.moveNext() || ((ts.offset() + ts.token().length()) == request.result.getSnapshot().getText().length()))) {
if (!lookBefore && ts.token().id() != JsTokenId.OPERATOR_DOT) {
ts.movePrevious();
}
Token<? extends JsTokenId> token = lookBefore ? LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.EOL)) : ts.token();
int parenBalancer = 0;
// 1 - method call, 0 - property, 2 - array
int partType = 0;
boolean wasLastDot = lookBefore;
int offsetFirstRightParen = -1;
List<String> exp = new ArrayList();
while (token.id() != JsTokenId.WHITESPACE && token.id() != JsTokenId.OPERATOR_SEMICOLON
&& token.id() != JsTokenId.BRACKET_RIGHT_CURLY && token.id() != JsTokenId.BRACKET_LEFT_CURLY
&& token.id() != JsTokenId.BRACKET_LEFT_PAREN
&& token.id() != JsTokenId.BLOCK_COMMENT
&& token.id() != JsTokenId.LINE_COMMENT
&& token.id() != JsTokenId.OPERATOR_ASSIGNMENT
&& token.id() != JsTokenId.OPERATOR_PLUS) {
if (token.id() != JsTokenId.EOL) {
if (token.id() != JsTokenId.OPERATOR_DOT) {
if (token.id() == JsTokenId.BRACKET_RIGHT_PAREN) {
parenBalancer++;
partType = 1;
if (offsetFirstRightParen == -1) {
offsetFirstRightParen = ts.offset();
}
while (parenBalancer > 0 && ts.movePrevious()) {
token = ts.token();
if (token.id() == JsTokenId.BRACKET_RIGHT_PAREN) {
parenBalancer++;
} else {
if (token.id() == JsTokenId.BRACKET_LEFT_PAREN) {
parenBalancer--;
}
}
}
} else if (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET) {
parenBalancer++;
partType = 2;
while (parenBalancer > 0 && ts.movePrevious()) {
token = ts.token();
if (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET) {
parenBalancer++;
} else {
if (token.id() == JsTokenId.BRACKET_LEFT_BRACKET) {
parenBalancer--;
}
}
}
} else if (parenBalancer == 0 && "operator".equals(token.id().primaryCategory())) { // NOI18N
return exp;
} else {
exp.add(token.text().toString());
switch (partType) {
case 0:
exp.add("@pro"); // NOI18N
break;
case 1:
exp.add("@mtd"); // NOI18N
offsetFirstRightParen = -1;
break;
case 2:
exp.add("@arr"); // NOI18N
break;
default:
break;
}
partType = 0;
wasLastDot = false;
}
} else {
wasLastDot = true;
}
} else {
if (!wasLastDot && ts.movePrevious()) {
// check whether it's continuatino of previous line
token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.LINE_COMMENT));
if (token.id() != JsTokenId.OPERATOR_DOT) {
// the dot was not found => it's not continuation of expression
break;
}
}
}
if (!ts.movePrevious()) {
break;
}
token = ts.token();
}
if (token.id() == JsTokenId.WHITESPACE) {
if (ts.movePrevious()) {
token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.BLOCK_COMMENT, JsTokenId.EOL));
if (token.id() == JsTokenId.KEYWORD_NEW && !exp.isEmpty()) {
exp.remove(exp.size() - 1);
exp.add("@pro"); // NOI18N
} else if (!lookBefore && offsetFirstRightParen > -1) {
// in the case when the expression is like ( new Object()).someMethod
exp.addAll(resolveExpressionChain(request, offsetFirstRightParen - 1, true));
}
}
} else if (exp.isEmpty() && !lookBefore && offsetFirstRightParen > -1) {
// in the case when the expression is like ( new Object()).someMethod
exp.addAll(resolveExpressionChain(request, offsetFirstRightParen - 1, true));
}
return exp;
}
return Collections.<String>emptyList();
}
private void completeObjectMember(CompletionRequest request, Map<String, List<JsElement>> addedItems) {
JsParserResult result = (JsParserResult)request.info;
JsObject jsObject = (JsObject)ModelUtils.getDeclarationScope(Model.getModel(result, false), request.anchor);
if (jsObject.getJSKind() == JsElement.Kind.METHOD) {
jsObject = jsObject.getParent();
}
boolean startThis = startWithThis(request);
completeObjectMembers(jsObject, request, addedItems, !startThis);
if (ModelUtils.PROTOTYPE.equals(jsObject.getName())) { //NOI18N
completeObjectMembers(jsObject.getParent(), request, addedItems, !startThis);
}
}
private void completeObjectMembers(JsObject jsObject, CompletionRequest request, Map<String, List<JsElement>> properties, boolean includePrivate) {
if (jsObject.getJSKind() == JsElement.Kind.OBJECT || jsObject.getJSKind() == JsElement.Kind.CONSTRUCTOR
|| jsObject.getJSKind() == JsElement.Kind.OBJECT_LITERAL) {
for (JsObject property : jsObject.getProperties().values()) {
if(!(request.completionContext == OBJECT_MEMBERS && property.getModifiers().contains(Modifier.PRIVATE) && !includePrivate && property.getModifiers().size() == 1) && !property.isAnonymous()) {
addPropertyToMap(request, properties, property);
}
}
}
String fqn = jsObject.getFullyQualifiedName();
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Collection<IndexedElement> indexedProperties = Index.get(fo).getProperties(fqn);
for (IndexedElement indexedElement : indexedProperties) {
addPropertyToMap(request, properties, indexedElement);
}
}
private boolean startWithThis(CompletionRequest request) {
boolean result = false;
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(request.info.getSnapshot(), request.anchor);
if (ts == null) {
return result;
}
ts.move(request.info.getSnapshot().getEmbeddedOffset(request.anchor));
if (ts.movePrevious()) {
Token<? extends JsTokenId> token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.IDENTIFIER, JsTokenId.OPERATOR_DOT));
if (token != null && JsTokenId.KEYWORD_THIS == token.id()) {
result = true;
}
}
return result;
}
private void completeInWith (CompletionRequest request,HashMap <String, List<JsElement>> addedItems) {
int offset = request.anchor;
Collection<? extends TypeUsage> typesFromWith = ModelUtils.getTypeFromWith(Model.getModel(request.result, false), offset);
if (!typesFromWith.isEmpty()) {
FileObject fo = request.info.getSnapshot().getSource().getFileObject();
Index jsIndex = Index.get(fo);
Collection<TypeUsage> resolveTypes = ModelUtils.resolveTypes(typesFromWith, Model.getModel(request.result, false), jsIndex, true);
for (TypeUsage type : resolveTypes) {
JsObject localObject = ModelUtils.findJsObjectByName(Model.getModel(request.result, false), type.getType());
if (localObject != null) {
addObjectPropertiesToCC(localObject, request, addedItems);
}
addObjectPropertiesFromIndex(type.getType(), jsIndex, request, addedItems, true);
}
}
}
private void completeCallArguments(CompletionRequest request, List<CompletionProposal> resultList) {
// find (if exist) the function which is called.
List<TypeUsage> types = findPossibleCallArgTypes(request);
FileObject fo = request.result.getSnapshot().getSource().getFileObject();
if (types != null && fo != null && !types.isEmpty()) {
Index jsIndex = Index.get(fo);
for (TypeUsage type: types) {
Collection<? extends IndexResult> fromIndex = jsIndex.findByFqn(type.getType(), Index.TERMS_BASIC_INFO);
for (IndexResult indexResult: fromIndex) {
IndexedElement indexElement = IndexedElement.create(indexResult);
if (indexElement.getJSKind() == JsElement.Kind.CALLBACK) {
resultList.add(new JsCompletionItem.JsCallbackCompletionItem((IndexedElement.FunctionIndexedElement)indexElement, request));
}
}
}
}
}
private void completeKeywords(CompletionRequest request, List<CompletionProposal> resultList) {
for (Map.Entry<String, JsKeywords.CompletionDescription> entry : JsKeywords.KEYWORDS.entrySet()) {
if (startsWith(entry.getKey(), request.prefix)) {
resultList.add(new JsCompletionItem.KeywordItem(entry.getKey(), entry.getValue(), request));
}
}
}
private void addImportExportKeywords(CompletionRequest request, List<CompletionProposal> resultList) {
for (Map.Entry<String, JsKeywords.CompletionDescription> entry : JsKeywords.SPECIAL_KEYWORDS_IMPORTEXPORT.entrySet()) {
if (startsWith(entry.getKey(), request.prefix)) {
resultList.add(new JsCompletionItem.KeywordItem(entry.getKey(), entry.getValue(), request));
}
}
}
private void completeJsModuleNames(CompletionRequest request, List<CompletionProposal> resultList) {
final Snapshot snapshot = request.info.getSnapshot();
TokenHierarchy<?> th = snapshot.getTokenHierarchy();
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(th, request.anchor);
if (ts == null) {
return;
}
int offset = snapshot.getEmbeddedOffset(request.anchor);
ts.move(offset);
final String prefix = request.prefix;
String writtenPath = request.prefix;
if (ts.moveNext() && (ts.token().id() == JsTokenId.STRING_END || ts.token().id() == JsTokenId.STRING)) {
if (ts.token().id() == JsTokenId.STRING_END) {
ts.movePrevious();
}
if (ts.token().id() == JsTokenId.STRING) {
String text = ts.token().text().toString();
// this is needed, because from JS the prefix is split with '.' and '/'
writtenPath = text.substring(0, offset - ts.offset());
// writtenPath = text;
// offset = ts.offset();
}
}
FileObject fo = snapshot.getSource().getFileObject();
try {
List<CompletionProposal> relativeFiles = FileUtils.computeRelativeItems(Collections.singletonList(fo), writtenPath, offset, false, false, new FileUtils.FileObjectFilter() {
@Override
public boolean accept(FileObject file) {
return file.isFolder() || ("js".equals(file.getExt().toLowerCase()) && file.getName().startsWith(prefix)); //NOI18N
}
});
resultList.addAll(relativeFiles);
} catch (IOException ex) {
LOGGER.log(Level.INFO, ex, null);
}
}
private void completeTagAttributes(CompletionRequest request, List<CompletionProposal> resultList) {
for(HtmlTagAttribute attribute: getAllAttributes()) {
if (attribute.getName().startsWith(request.prefix)) {
resultList.add(new JsCompletionItem.JsHtmlAttributeItem(attribute, request));
}
}
}
private void completeTagIds(CompletionRequest request, List<CompletionProposal> resultList) {
FileObject fo = request.result.getSnapshot().getSource().getFileObject();
if (fo == null) {
return;
}
Project project = FileOwnerQuery.getOwner(fo);
HashSet<String> unique = new HashSet<String>();
try {
CssIndex cssIndex = CssIndex.create(project);
Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findIdsByPrefix(request.prefix);
for (Collection<String> ids : findIdsByPrefix.values()) {
for (String id : ids) {
if (!id.isEmpty()) {
unique.add(id);
}
}
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (!unique.isEmpty()) {
for (Iterator<String> iterator = unique.iterator(); iterator.hasNext();) {
resultList.add(new JsCompletionItem.CssCompletionItem(iterator.next(), request));
}
}
}
private void completeCSSClassNames(CompletionRequest request, List<CompletionProposal> resultList) {
FileObject fo = request.result.getSnapshot().getSource().getFileObject();
if(fo == null) {
return;
}
Project project = FileOwnerQuery.getOwner(fo);
HashSet<String> unique = new HashSet<String>();
try {
CssIndex cssIndex = CssIndex.create(project);
Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findClassesByPrefix(request.prefix);
for (Collection<String> ids : findIdsByPrefix.values()) {
for (String id : ids) {
if (!id.isEmpty()) {
unique.add(id);
}
}
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (!unique.isEmpty()) {
for (Iterator<String> iterator = unique.iterator(); iterator.hasNext();) {
resultList.add(new JsCompletionItem.CssCompletionItem(iterator.next(), request));
}
}
}
private boolean startsWith(String theString, String prefix) {
if (prefix == null || prefix.length() == 0) {
return true;
}
return caseSensitive ? theString.startsWith(prefix)
: theString.toLowerCase().startsWith(prefix.toLowerCase());
}
private boolean processTypeInModel(CompletionRequest request, Model model, TypeUsage type, List<JsObject> lastResolvedObjects, boolean prop, Index index, Map<String, List<JsElement>> addedProperties) {
if (++checkRecursion > 10) {
return false;
}
boolean isFunction = false;
// at first try to find the type in the model
JsObject jsObject = ModelUtils.findJsObjectByName(model, type.getType());
if (jsObject != null) {
lastResolvedObjects.add(jsObject);
}
for (JsObject libGlobal : ModelUtils.getExtendingGlobalObjects(request.result.getSnapshot().getSource().getFileObject())) {
JsObject found = ModelUtils.findJsObjectByName(libGlobal, type.getType());
if (found != null && found != libGlobal) {
jsObject = found;
lastResolvedObjects.add(jsObject);
break;
}
}
if (jsObject == null || !jsObject.isDeclared()) {
boolean isObject = type.getType().equals("Object"); //NOI18N
if (prop && !isObject) {
for (IndexResult indexResult : index.findByFqn(type.getType(), Index.FIELD_FLAG)) {
JsElement.Kind kind = IndexedElement.Flag.getJsKind(Integer.parseInt(indexResult.getValue(Index.FIELD_FLAG)));
if (kind.isFunction()) {
isFunction = true;
}
}
}
if (!isObject) {
addObjectPropertiesFromIndex(type.getType(), index, request, addedProperties, true);
}
} else if (jsObject.getDeclarationName() != null) {
Collection<? extends TypeUsage> assignments = jsObject.getAssignmentForOffset(jsObject.getDeclarationName().getOffsetRange().getEnd());
for (TypeUsage assignment : assignments) {
boolean isFun = processTypeInModel(request, model, assignment, lastResolvedObjects, prop, index, addedProperties);
isFunction = isFunction ? true : isFun;
}
}
return isFunction;
}
private void addObjectPropertiesToCC(JsObject jsObject, CompletionRequest request, Map<String, List<JsElement>> addedProperties) {
JsObject prototype = jsObject.getProperty(ModelUtils.PROTOTYPE); // NOI18N
if (prototype != null) {
// at first add all prototype properties
// if the same property is declared in the project directly, then this is replaced.
addObjectPropertiesToCC(prototype, request, addedProperties);
}
for (JsObject property : jsObject.getProperties().values()) {
if (!(property instanceof JsFunction && ((JsFunction) property).isAnonymous())
&& !(ModelUtils.getDisplayName(property.getName()).isEmpty())
&& !property.getModifiers().contains(Modifier.PRIVATE)
&& !property.getJSKind().isPropertyGetterSetter()) {
addPropertyToMap(request, addedProperties, property);
}
}
}
private void addObjectPropertiesFromIndex(String fqn, Index jsIndex, CompletionRequest request, Map<String, List<JsElement>> addedProperties, boolean includeStatic) {
Collection<IndexedElement> properties = jsIndex.getProperties(fqn);
for (IndexedElement indexedElement : properties) {
if(includeStatic || (!includeStatic && !indexedElement.getModifiers().contains(Modifier.STATIC))) {
addPropertyToMap(request, addedProperties, indexedElement);
if (ModelUtils.PROTOTYPE.equals(indexedElement.getName())) {
Collection<IndexedElement> protoProperties = jsIndex.getProperties(indexedElement.getFQN());
for (IndexedElement protoProperty : protoProperties) {
addPropertyToMap(request, addedProperties, protoProperty);
}
}
}
}
if (fqn.equals("Element")) {
request.addHtmlTagAttributes = true;
}
}
private void addPropertyToMap(CompletionRequest request, Map<String, List<JsElement>> addedProperties, JsElement property) {
String name = property.getName();
if (startsWith(name, request.prefix) && !(ModelUtils.getDisplayName(property.getName()).isEmpty())
&& property.getJSKind() != JsElement.Kind.CALLBACK) {
if (!(name.equals(request.prefix) && !property.isDeclared() && request.anchor == property.getOffset())) { // don't include just the prefix
List<JsElement> elements = addedProperties.get(name);
if (!ModelUtils.PROTOTYPE.equals(name)) {
if (elements == null || elements.isEmpty()) {
List<JsElement> properties = new ArrayList<JsElement>(1);
properties.add(property);
addedProperties.put(name, properties);
} else {
if (property.isDeclared()) {
boolean addAsNew = true;
if (!elements.isEmpty()) {
for (int i = 0; i < elements.size(); i++) {
JsElement element = elements.get(i);
FileObject fo = element.getFileObject();
if (!element.isDeclared() || (fo != null && fo.equals(property.getFileObject()))) {
if (!element.isDeclared() || (element.getOffsetRange() == OffsetRange.NONE && property.getOffsetRange() != OffsetRange.NONE)) {
elements.remove(i);
elements.add(property);
addAsNew = false;
break;
} else if (fo != null && fo.equals(property.getFileObject())) {
addAsNew = false;
break;
}
} else if (element.isPlatform() && property.isPlatform()) {
addAsNew = false;
break;
}
}
}
if (addAsNew) {
// expect that all items are declaration -> so just add the next declaraiton
elements.add(property);
}
}
}
} else {
if (elements == null && property.isPlatform()) {
List<JsElement> properties = new ArrayList<JsElement>(1);
properties.add(property);
addedProperties.put(name, properties);
}
}
}
}
}
private Map<String, List<JsElement>> getDomCompletionResults(CompletionRequest request) {
Map<String, List<JsElement>> result = new HashMap<String, List<JsElement>>(1);
// default window object
result.putAll(getCompletionFromExpressionChain(request, WINDOW_EXPRESSION_CHAIN));
return result;
}
/** XXX - Once the JS framework support becomes plugable, should be moved to jQueryCompletionHandler getPrefix() */
private static int getPrefixIndexFromSequence(String prefix) {
int spaceIndex = prefix.lastIndexOf(" ") + 1; //NOI18N
int dotIndex = prefix.lastIndexOf(".") + 1; //NOI18N
int hashIndex = prefix.lastIndexOf("#") + 1; //NOI18N
int bracketIndex = prefix.lastIndexOf("[") + 1; //NOI18N
int columnIndex = prefix.lastIndexOf(":") + 1; //NOI18N
int parenIndex = prefix.lastIndexOf("(") + 1; //NOI18N
// for file code completion
int slashIndex = prefix.lastIndexOf('/') + 1; //NOI18N
return (Math.max(0, Math.max(hashIndex, Math.max(dotIndex, Math.max(parenIndex,Math.max(columnIndex, Math.max(bracketIndex, Math.max(spaceIndex, slashIndex))))))));
}
private Collection<HtmlTagAttribute> getAllAttributes() {
HtmlModel htmlModel = HtmlModelFactory.getModel(HtmlVersion.HTML5);
Map<String, HtmlTagAttribute> result = new HashMap<String, HtmlTagAttribute>();
for (HtmlTag htmlTag : htmlModel.getAllTags()) {
for (HtmlTagAttribute htmlTagAttribute : htmlTag.getAttributes()) {
// attributes can probably differ per tag so we can just offer some of them,
// at least for the CC purposes it should be complete list of attributes for unknown tag
if (!result.containsKey(htmlTagAttribute.getName())) {
result.put(htmlTagAttribute.getName(), htmlTagAttribute);
}
}
}
return result.values();
}
}