| /* |
| * 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(); |
| } |
| |
| } |