| /* |
| * 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.php.editor.completion; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.Callable; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.ImageIcon; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Document; |
| import javax.swing.text.JTextComponent; |
| import org.netbeans.api.annotations.common.CheckForNull; |
| import org.netbeans.api.editor.document.LineDocumentUtils; |
| 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.lexer.TokenUtilities; |
| import org.netbeans.editor.BaseDocument; |
| import org.netbeans.modules.csl.api.CodeCompletionContext; |
| import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType; |
| import org.netbeans.modules.csl.api.CodeCompletionHandler2; |
| import org.netbeans.modules.csl.api.CodeCompletionResult; |
| import org.netbeans.modules.csl.api.CompletionProposal; |
| import org.netbeans.modules.csl.api.Documentation; |
| import org.netbeans.modules.csl.api.ElementHandle; |
| import org.netbeans.modules.csl.api.ParameterInfo; |
| import org.netbeans.modules.csl.spi.ParserResult; |
| import org.netbeans.modules.csl.spi.support.CancelSupport; |
| import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport; |
| import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport.Kind; |
| import org.netbeans.modules.php.api.PhpVersion; |
| import org.netbeans.modules.php.editor.CodeUtils; |
| import org.netbeans.modules.php.editor.NavUtils; |
| import org.netbeans.modules.php.editor.PredefinedSymbols; |
| import org.netbeans.modules.php.editor.api.AliasedName; |
| import org.netbeans.modules.php.editor.api.ElementQueryFactory; |
| import org.netbeans.modules.php.editor.api.NameKind; |
| import org.netbeans.modules.php.editor.api.NameKind.CaseInsensitivePrefix; |
| import org.netbeans.modules.php.editor.api.PhpElementKind; |
| import org.netbeans.modules.php.editor.api.QualifiedName; |
| import org.netbeans.modules.php.editor.api.QualifiedNameKind; |
| import org.netbeans.modules.php.editor.api.elements.AliasedElement.Trait; |
| import org.netbeans.modules.php.editor.api.elements.ClassElement; |
| import org.netbeans.modules.php.editor.api.elements.ConstantElement; |
| import org.netbeans.modules.php.editor.api.elements.ElementFilter; |
| import org.netbeans.modules.php.editor.api.elements.FieldElement; |
| import org.netbeans.modules.php.editor.api.elements.FunctionElement; |
| import org.netbeans.modules.php.editor.api.elements.InterfaceElement; |
| import org.netbeans.modules.php.editor.api.elements.MethodElement; |
| import org.netbeans.modules.php.editor.api.elements.NamespaceElement; |
| import org.netbeans.modules.php.editor.api.elements.ParameterElement; |
| import org.netbeans.modules.php.editor.api.elements.PhpElement; |
| import org.netbeans.modules.php.editor.api.elements.TraitElement; |
| import org.netbeans.modules.php.editor.api.elements.TypeConstantElement; |
| import org.netbeans.modules.php.editor.api.elements.TypeElement; |
| import org.netbeans.modules.php.editor.api.elements.TypeMemberElement; |
| import org.netbeans.modules.php.editor.api.elements.VariableElement; |
| import org.netbeans.modules.php.editor.completion.CompletionContextFinder.CompletionContext; |
| import org.netbeans.modules.php.editor.completion.CompletionContextFinder.KeywordCompletionType; |
| import static org.netbeans.modules.php.editor.completion.CompletionContextFinder.lexerToASTOffset; |
| import org.netbeans.modules.php.editor.completion.PHPCompletionItem.CompletionRequest; |
| import org.netbeans.modules.php.editor.completion.PHPCompletionItem.FieldItem; |
| import org.netbeans.modules.php.editor.completion.PHPCompletionItem.MethodElementItem; |
| import org.netbeans.modules.php.editor.completion.PHPCompletionItem.TypeConstantItem; |
| import org.netbeans.modules.php.editor.elements.TypeResolverImpl; |
| import org.netbeans.modules.php.editor.elements.VariableElementImpl; |
| import org.netbeans.modules.php.editor.indent.CodeStyle; |
| import org.netbeans.modules.php.editor.lexer.LexUtilities; |
| import org.netbeans.modules.php.editor.lexer.PHPTokenId; |
| import org.netbeans.modules.php.editor.model.ArrowFunctionScope; |
| import org.netbeans.modules.php.editor.model.FunctionScope; |
| import org.netbeans.modules.php.editor.model.Model; |
| import org.netbeans.modules.php.editor.model.ModelElement; |
| import org.netbeans.modules.php.editor.model.ModelUtils; |
| import org.netbeans.modules.php.editor.model.NamespaceScope; |
| import org.netbeans.modules.php.editor.model.ParameterInfoSupport; |
| import org.netbeans.modules.php.editor.model.Scope; |
| import org.netbeans.modules.php.editor.model.TypeScope; |
| import org.netbeans.modules.php.editor.model.VariableName; |
| import org.netbeans.modules.php.editor.model.VariableScope; |
| import org.netbeans.modules.php.editor.model.impl.Type; |
| import org.netbeans.modules.php.editor.model.impl.VariousUtils; |
| import org.netbeans.modules.php.editor.options.CodeCompletionPanel.VariablesScope; |
| import org.netbeans.modules.php.editor.options.OptionsUtils; |
| import org.netbeans.modules.php.editor.parser.PHPParseResult; |
| import org.netbeans.modules.php.editor.parser.astnodes.ASTNode; |
| import org.netbeans.modules.php.editor.parser.astnodes.Block; |
| import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration; |
| import org.netbeans.modules.php.editor.parser.astnodes.ClassInstanceCreation; |
| import org.netbeans.modules.php.editor.parser.astnodes.Expression; |
| import org.netbeans.modules.php.editor.parser.astnodes.TraitDeclaration; |
| import org.netbeans.modules.php.editor.parser.astnodes.TypeDeclaration; |
| import org.openide.filesystems.FileObject; |
| import org.openide.util.Pair; |
| |
| /** |
| * |
| * @author Tomasz.Slota@Sun.COM |
| */ |
| public class PHPCodeCompletion implements CodeCompletionHandler2 { |
| |
| // for unit tests |
| static volatile PhpVersion PHP_VERSION = null; |
| private static final Logger LOGGER = Logger.getLogger(PHPCodeCompletion.class.getName()); |
| |
| private static enum UseType { |
| TYPE, |
| CONST, |
| FUNCTION, |
| }; |
| |
| static final Map<String, KeywordCompletionType> PHP_KEYWORDS = new HashMap<>(); |
| |
| static { |
| PHP_KEYWORDS.put("use", KeywordCompletionType.SIMPLE); //NOI18N |
| PHP_KEYWORDS.put("namespace", KeywordCompletionType.SIMPLE); //NOI18N |
| PHP_KEYWORDS.put("class", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("const", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("continue", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("function", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("fn", KeywordCompletionType.SIMPLE); // NOI18N PHP 7.4 |
| PHP_KEYWORDS.put("new", KeywordCompletionType.SIMPLE); //NOI18N |
| PHP_KEYWORDS.put("static", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("var", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("final", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("interface", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("instanceof", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("implements", KeywordCompletionType.SIMPLE); //NOI18N |
| PHP_KEYWORDS.put("extends", KeywordCompletionType.SIMPLE); //NOI18N |
| PHP_KEYWORDS.put("public", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("private", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("protected", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("abstract", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("readonly", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("clone", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("global", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("goto", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("throw", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("if", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("switch", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("match", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("for", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("array", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("foreach", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("while", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("catch", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("try", KeywordCompletionType.ENDS_WITH_CURLY_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("default", KeywordCompletionType.ENDS_WITH_COLON); //NOI18N |
| PHP_KEYWORDS.put("default =>", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N PHP 8.0 match expression |
| PHP_KEYWORDS.put("break", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("endif", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("endfor", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("endforeach", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("endwhile", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("endswitch", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("case", KeywordCompletionType.ENDS_WITH_COLON); //NOI18N |
| PHP_KEYWORDS.put("and", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("as", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("declare", KeywordCompletionType.CURSOR_INSIDE_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("do", KeywordCompletionType.ENDS_WITH_CURLY_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("else", KeywordCompletionType.ENDS_WITH_CURLY_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("elseif", KeywordCompletionType.ENDS_WITH_BRACKETS_AND_CURLY_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("enddeclare", KeywordCompletionType.ENDS_WITH_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("or", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("xor", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| PHP_KEYWORDS.put("finally", KeywordCompletionType.ENDS_WITH_CURLY_BRACKETS); //NOI18N |
| PHP_KEYWORDS.put("yield", KeywordCompletionType.CURSOR_BEFORE_ENDING_SEMICOLON); //NOI18N |
| PHP_KEYWORDS.put("yield from", KeywordCompletionType.ENDS_WITH_SPACE); //NOI18N |
| } |
| private static final String[] PHP_LANGUAGE_CONSTRUCTS_WITH_QUOTES = { |
| "echo", "include", "include_once", "require", "require_once", "print" // NOI18N |
| }; |
| private static final String[] PHP_LANGUAGE_CONSTRUCTS_WITH_PARENTHESES = { |
| "die", "eval", "exit", "empty", "isset", "list", "unset" // NOI18N |
| }; |
| private static final String[] PHP_LANGUAGE_CONSTRUCTS_WITH_SEMICOLON = { |
| "return" // NOI18N |
| }; |
| static final String PHP_CLASS_KEYWORD_THIS = "$this->"; //NOI18N |
| static final String[] PHP_CLASS_KEYWORDS = { |
| PHP_CLASS_KEYWORD_THIS, "self::", "parent::", "static::" //NOI18N |
| }; |
| static final String[] PHP_STATIC_CLASS_KEYWORDS = { |
| "self::", "parent::", "static::" //NOI18N |
| }; |
| static final List<String> PHP_GLOBAL_CONST_KEYWORDS = Arrays.asList( |
| "array", // NOI18N |
| "new" // NOI18N |
| ); |
| static final List<String> PHP_CLASS_CONST_KEYWORDS = Arrays.asList( |
| "array", // NOI18N |
| "self::", // NOI18N |
| "parent::" // NOI18N |
| ); |
| private static final List<String> PHP_MATCH_EXPRESSION_KEYWORDS = Arrays.asList( |
| "function", // NOI18N |
| "fn", // NOI18N |
| "new", // NOI18N |
| "static", // NOI18N |
| "instanceof", // NOI18N |
| "clone", // NOI18N |
| "throw", // NOI18N |
| "match", // NOI18N |
| "array", // NOI18N |
| "default =>", // NOI18N |
| "and", // NOI18N |
| "or", // NOI18N |
| "xor" // NOI18N |
| ); |
| private static final List<String> PHP_VISIBILITY_KEYWORDS = Arrays.asList( |
| "public", // NOI18N |
| "protected", // NOI18N |
| "private" // NOI18N |
| ); |
| private static final Collection<Character> AUTOPOPUP_STOP_CHARS = new TreeSet<>( |
| Arrays.asList('=', ';', '+', '-', '*', '/', |
| '%', '(', ')', '[', ']', '{', '}', '?')); |
| private static final Collection<PHPTokenId> TOKENS_TRIGGERING_AUTOPUP_TYPES_WS = |
| Arrays.asList(PHPTokenId.PHP_NEW, PHPTokenId.PHP_EXTENDS, PHPTokenId.PHP_IMPLEMENTS, PHPTokenId.PHP_INSTANCEOF); |
| private static final List<String> INVALID_PROPOSALS_FOR_CLS_MEMBERS = |
| Arrays.asList(new String[]{"__construct", "__destruct", "__call", "__callStatic", |
| "__clone", "__get", "__invoke", "__isset", "__set", "__set_state", |
| "__sleep", "__toString", "__unset", "__wakeup"}); //NOI18N |
| private static final List<String> CLASS_CONTEXT_KEYWORD_PROPOSAL = |
| Arrays.asList(new String[]{"abstract", "const", "function", "private", "final", |
| "protected", "public", "static", "var", "readonly"}); //NOI18N |
| private static final List<String> INTERFACE_CONTEXT_KEYWORD_PROPOSAL = |
| Arrays.asList(new String[]{"const", "function", "public", "static"}); //NOI18N |
| private static final List<String> INHERITANCE_KEYWORDS = |
| Arrays.asList(new String[]{"extends", "implements"}); //NOI18N |
| private static final String EXCEPTION_CLASS_NAME = "\\Exception"; // NOI18N |
| private static final List<PHPTokenId> VALID_UNION_TYPE_TOKENS = Arrays.asList( |
| PHPTokenId.WHITESPACE, PHPTokenId.PHP_STRING, PHPTokenId.PHP_NS_SEPARATOR, |
| PHPTokenId.PHP_TYPE_BOOL, PHPTokenId.PHP_TYPE_FLOAT, PHPTokenId.PHP_TYPE_INT, PHPTokenId.PHP_TYPE_STRING, PHPTokenId.PHP_TYPE_VOID, |
| PHPTokenId.PHP_TYPE_OBJECT, PHPTokenId.PHP_TYPE_MIXED, PHPTokenId.PHP_SELF, PHPTokenId.PHP_PARENT, PHPTokenId.PHP_STATIC, |
| PHPTokenId.PHP_NULL, PHPTokenId.PHP_FALSE, PHPTokenId.PHP_ARRAY, PHPTokenId.PHP_ITERABLE, PHPTokenId.PHP_CALLABLE, |
| PHPTokenId.PHPDOC_COMMENT_START, PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, |
| PHPTokenId.PHP_COMMENT_START, PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END |
| ); |
| private boolean caseSensitive; |
| private QuerySupport.Kind nameKind; |
| |
| @Override |
| public CodeCompletionResult complete(CodeCompletionContext completionContext) { |
| long startTime = 0; |
| if (LOGGER.isLoggable(Level.FINE)) { |
| startTime = System.currentTimeMillis(); |
| } |
| |
| BaseDocument doc = (BaseDocument) completionContext.getParserResult().getSnapshot().getSource().getDocument(false); |
| if (doc == null) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| // TODO: separate the code that uses informatiom from lexer |
| // and avoid running the index/ast analysis under read lock |
| // in order to improve responsiveness |
| // doc.readLock(); //TODO: use token hierarchy from snapshot and not use read lock in CC #171702 |
| |
| final PHPCompletionResult completionResult = new PHPCompletionResult(completionContext); |
| ParserResult info = completionContext.getParserResult(); |
| final int caretOffset = completionContext.getCaretOffset(); |
| |
| this.caseSensitive = completionContext.isCaseSensitive(); |
| this.nameKind = caseSensitive ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.CASE_INSENSITIVE_PREFIX; |
| |
| PHPParseResult result = (PHPParseResult) info; |
| |
| if (result.getProgram() == null) { |
| return CodeCompletionResult.NONE; |
| } |
| final FileObject fileObject = result.getSnapshot().getSource().getFileObject(); |
| if (fileObject == null) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| CompletionContext context = CompletionContextFinder.findCompletionContext(info, caretOffset); |
| LOGGER.log(Level.FINE, "CC context: {0}", context); |
| |
| if (context == CompletionContext.NONE) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| PHPCompletionItem.CompletionRequest request = new PHPCompletionItem.CompletionRequest(); |
| request.context = context; |
| String prefix = getPrefix(info, caretOffset, true, PrefixBreaker.WITH_NS_PARTS); |
| if (prefix == null) { |
| return CodeCompletionResult.NONE; |
| } |
| prefix = prefix.trim().isEmpty() ? completionContext.getPrefix() : prefix; |
| // prefix for index search (used for group use, equals to the base NS (before curly open)) |
| String searchPrefix; |
| switch (context) { |
| case GROUP_USE_KEYWORD: |
| case GROUP_USE_CONST_KEYWORD: |
| case GROUP_USE_FUNCTION_KEYWORD: |
| searchPrefix = getPrefix(info, findBaseNamespaceEnd(info, caretOffset), true, PrefixBreaker.WITH_NS_PARTS); |
| break; |
| case EXPRESSION: // no break |
| if (prefix.startsWith("@")) { // NOI18N |
| prefix = prefix.substring(1); |
| } |
| default: |
| searchPrefix = null; |
| break; |
| } |
| request.extraPrefix = searchPrefix; |
| |
| request.anchor = caretOffset |
| // can't just use 'prefix.getLength()' here cos it might have been calculated with |
| // the 'upToOffset' flag set to false |
| - prefix.length(); |
| |
| request.result = result; |
| request.info = info; |
| request.prefix = prefix; |
| request.index = ElementQueryFactory.getIndexQuery(info); |
| request.currentlyEditedFileURL = fileObject.toURL().toString(); |
| |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| CodeStyle codeStyle; |
| switch (context) { |
| case DEFAULT_PARAMETER_VALUE: |
| final CaseInsensitivePrefix nameKindPrefix = NameKind.caseInsensitivePrefix(request.prefix); |
| autoCompleteKeywords(completionResult, request, Arrays.asList("array", "new")); //NOI18N |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request, null, true); |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| final ElementFilter forName = ElementFilter.forName(nameKindPrefix); |
| final Model model = request.result.getModel(); |
| final Set<AliasedName> aliasedNames = ModelUtils.getAliasedNames(model, request.anchor); |
| Set<ConstantElement> constants = request.index.getConstants(nameKindPrefix, aliasedNames, Trait.ALIAS); |
| for (ConstantElement constant : forName.filter(constants)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| completionResult.add(new PHPCompletionItem.ConstantItem(constant, request)); |
| } |
| final EnclosingClass enclosingClass = findEnclosingClass(request.info, lexerToASTOffset(request.result, request.anchor)); |
| if (enclosingClass != null) { |
| String clsName = enclosingClass.getClassName(); |
| for (String classKeyword : PHP_STATIC_CLASS_KEYWORDS) { |
| if (classKeyword.toLowerCase().startsWith(request.prefix)) { //NOI18N |
| completionResult.add(new PHPCompletionItem.ClassScopeKeywordItem(clsName, classKeyword, request)); |
| } |
| } |
| } |
| break; |
| case NAMESPACE_KEYWORD: |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| Set<NamespaceElement> namespaces = request.index.getNamespaces( |
| NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix).toNotFullyQualified())); |
| for (NamespaceElement namespace : namespaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| completionResult.add(new PHPCompletionItem.NamespaceItem(namespace, request, QualifiedNameKind.QUALIFIED)); |
| } |
| break; |
| case GLOBAL: |
| autoCompleteGlobals(completionResult, request); |
| break; |
| case MATCH_EXPRESSION: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteExpression(completionResult, request, PHP_MATCH_EXPRESSION_KEYWORDS); |
| break; |
| case EXPRESSION: |
| autoCompleteExpression(completionResult, request); |
| break; |
| case CLASS_MEMBER_PARAMETER_NAME: |
| autoCompleteExpression(completionResult, request); |
| autoCompleteClassMethodParameterName(completionResult, request, false); |
| break; |
| case STATIC_CLASS_MEMBER_PARAMETER_NAME: |
| autoCompleteExpression(completionResult, request); |
| autoCompleteClassMethodParameterName(completionResult, request, true); |
| break; |
| case FUNCTION_PARAMETER_NAME: |
| autoCompleteExpression(completionResult, request); |
| autoCompleteFunctionParameterName(completionResult, request); |
| break; |
| case GLOBAL_CONST_EXPRESSION: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request, null, true); |
| autoCompleteConstants(completionResult, request); |
| autoCompleteKeywords(completionResult, request, PHP_GLOBAL_CONST_KEYWORDS); |
| break; |
| case CLASS_CONST_EXPRESSION: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request, null, true); |
| autoCompleteConstants(completionResult, request); |
| autoCompleteKeywords(completionResult, request, PHP_CLASS_CONST_KEYWORDS); |
| // NETBEANS-1855 |
| if (!request.prefix.contains("\\")) { // NOI18N |
| // e.g. const CONSTANT = \^Foo\Bar::CONSTANT; |
| autoCompleteClassConstants(completionResult, request); |
| } |
| break; |
| case HTML: |
| case OPEN_TAG: |
| completionResult.add(new PHPCompletionItem.TagItem("<?php", 1, request)); //NOI18N |
| completionResult.add(new PHPCompletionItem.TagItem("<?=", 2, request)); //NOI18N |
| break; |
| case NEW_CLASS: |
| autoCompleteKeywords(completionResult, request, Arrays.asList("class")); //NOI18N |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteNewClass(completionResult, request); |
| break; |
| case CLASS_NAME: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteClassNames(completionResult, request, false); |
| break; |
| case INTERFACE_NAME: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteInterfaceNames(completionResult, request); |
| break; |
| case GROUP_USE_KEYWORD: |
| autoCompleteGroupUse(UseType.TYPE, completionResult, request); |
| List<String> keywords = Arrays.asList("const", "function"); // NOI18N |
| for (String keyword : keywords) { |
| if (startsWith(keyword, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.KeywordItem(keyword, request)); |
| } |
| } |
| break; |
| case GROUP_USE_CONST_KEYWORD: |
| autoCompleteGroupUse(UseType.CONST, completionResult, request); |
| break; |
| case GROUP_USE_FUNCTION_KEYWORD: |
| autoCompleteGroupUse(UseType.FUNCTION, completionResult, request); |
| break; |
| case USE_KEYWORD: |
| codeStyle = CodeStyle.get(request.result.getSnapshot().getSource().getDocument(caseSensitive)); |
| autoCompleteAfterUse( |
| UseType.TYPE, |
| completionResult, |
| request, |
| codeStyle.startUseWithNamespaceSeparator() ? QualifiedNameKind.FULLYQUALIFIED : QualifiedNameKind.QUALIFIED); |
| break; |
| case USE_CONST_KEYWORD: |
| codeStyle = CodeStyle.get(request.result.getSnapshot().getSource().getDocument(caseSensitive)); |
| autoCompleteAfterUse( |
| UseType.CONST, |
| completionResult, |
| request, |
| codeStyle.startUseWithNamespaceSeparator() ? QualifiedNameKind.FULLYQUALIFIED : QualifiedNameKind.QUALIFIED); |
| break; |
| case USE_FUNCTION_KEYWORD: |
| codeStyle = CodeStyle.get(request.result.getSnapshot().getSource().getDocument(caseSensitive)); |
| autoCompleteAfterUse( |
| UseType.FUNCTION, |
| completionResult, |
| request, |
| codeStyle.startUseWithNamespaceSeparator() ? QualifiedNameKind.FULLYQUALIFIED : QualifiedNameKind.QUALIFIED); |
| break; |
| case USE_TRAITS: |
| codeStyle = CodeStyle.get(request.result.getSnapshot().getSource().getDocument(caseSensitive)); |
| autoCompleteAfterUseTrait( |
| completionResult, |
| request, |
| codeStyle.startUseWithNamespaceSeparator() ? QualifiedNameKind.FULLYQUALIFIED : QualifiedNameKind.QUALIFIED); |
| break; |
| case VISIBILITY_MODIFIER_OR_TYPE_NAME: // no break |
| autoCompleteKeywords(completionResult, request, PHP_VISIBILITY_KEYWORDS); |
| autoCompleteKeywords(completionResult, request, Arrays.asList("readonly")); // NOI18N |
| case TYPE_NAME: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request); |
| final ArrayList<String> typesForTypeName = new ArrayList<>(Type.getTypesForEditor()); |
| if (isInType(request)) { |
| // add self and parent |
| typesForTypeName.addAll(Type.getSpecialTypesForType()); |
| } |
| if (isNullableType(info, caretOffset)) { |
| typesForTypeName.remove(Type.FALSE); |
| typesForTypeName.remove(Type.NULL); |
| } |
| if (isUnionType(info, caretOffset)) { |
| typesForTypeName.remove(Type.MIXED); |
| } |
| autoCompleteKeywords(completionResult, request, typesForTypeName); |
| break; |
| case RETURN_UNION_TYPE_NAME: // no break |
| case RETURN_TYPE_NAME: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request); |
| final ArrayList<String> typesForReturnTypeName = new ArrayList<>(Type.getTypesForReturnType()); |
| if (isInType(request)) { |
| // add self and parent |
| typesForReturnTypeName.addAll(Type.getSpecialTypesForType()); |
| typesForReturnTypeName.add(Type.STATIC); |
| } |
| if (isNullableType(info, caretOffset)) { |
| typesForReturnTypeName.remove(Type.FALSE); |
| typesForReturnTypeName.remove(Type.NULL); |
| typesForReturnTypeName.remove(Type.VOID); |
| typesForReturnTypeName.remove(Type.NEVER); |
| } else if (context == CompletionContext.RETURN_UNION_TYPE_NAME) { |
| typesForReturnTypeName.remove(Type.VOID); |
| typesForReturnTypeName.remove(Type.NEVER); |
| typesForReturnTypeName.remove(Type.MIXED); |
| } |
| autoCompleteKeywords(completionResult, request, typesForReturnTypeName); |
| break; |
| case FIELD_TYPE_NAME: |
| autoCompleteFieldType(info, caretOffset, completionResult, request, false); |
| break; |
| case STRING: |
| // LOCAL VARIABLES |
| completionResult.addAll(getVariableProposals(request, null)); |
| // are we in class? |
| if (request.prefix.length() == 0 || startsWith(PHP_CLASS_KEYWORD_THIS, request.prefix)) { |
| final EnclosingClass enclosingCls = findEnclosingClass(info, caretOffset); |
| if (enclosingCls != null) { |
| final String className = enclosingCls.extractClassName(); |
| if (className != null) { |
| completionResult.add(new PHPCompletionItem.ClassScopeKeywordItem(className, PHP_CLASS_KEYWORD_THIS, request)); |
| } |
| } |
| } |
| break; |
| case CLASS_MEMBER: |
| autoCompleteClassMembers(completionResult, request, false); |
| break; |
| case STATIC_CLASS_MEMBER: |
| autoCompleteClassMembers(completionResult, request, true); |
| break; |
| case PHPDOC: |
| PHPDOCCodeCompletion.complete(completionResult, request); |
| if (PHPDOCCodeCompletion.isTypeCtx(request)) { |
| autoCompleteTypeNames(completionResult, request); |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteKeywordsInPHPDoc(completionResult, request); |
| } |
| break; |
| case CLASS_CONTEXT_KEYWORDS: |
| autoCompleteInClassContext(info, caretOffset, completionResult, request); |
| break; |
| case INTERFACE_CONTEXT_KEYWORDS: |
| autoCompleteInInterfaceContext(completionResult, request); |
| break; |
| case METHOD_NAME: |
| autoCompleteMethodName(info, caretOffset, completionResult, request); |
| break; |
| case IMPLEMENTS: |
| autoCompleteKeywords(completionResult, request, Collections.singletonList("implements")); //NOI18N |
| break; |
| case EXTENDS: |
| autoCompleteKeywords(completionResult, request, Collections.singletonList("extends")); //NOI18N |
| break; |
| case INHERITANCE: |
| autoCompleteKeywords(completionResult, request, INHERITANCE_KEYWORDS); |
| break; |
| case THROW_NEW: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteExceptions(completionResult, request, true); |
| break; |
| case THROW: |
| autoCompleteKeywords(completionResult, request, Collections.singletonList("new")); // NOI18N |
| autoCompleteNamespaces(completionResult, request); |
| // XXX allow all class names for static factory methods? e.g. ExceptionFactory::create("Something"); |
| // currently, restrict to classes extending the Exception class |
| autoCompleteExceptions(completionResult, request, false); |
| break; |
| case CATCH: |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteExceptions(completionResult, request, false); |
| break; |
| case CLASS_MEMBER_IN_STRING: |
| autoCompleteClassFields(completionResult, request); |
| break; |
| case SERVER_ENTRY_CONSTANTS: |
| //TODO: probably better PHPCompletionItem instance should be used |
| //autoCompleteMagicItems(proposals, request, PredefinedSymbols.SERVER_ENTRY_CONSTANTS); |
| for (String keyword : PredefinedSymbols.SERVER_ENTRY_CONSTANTS) { |
| if (keyword.startsWith(request.prefix)) { |
| completionResult.add(new PHPCompletionItem.KeywordItem(keyword, request) { |
| @Override |
| public ImageIcon getIcon() { |
| return null; |
| } |
| }); |
| } |
| } |
| |
| break; |
| default: |
| assert false : context; |
| } |
| |
| if (CancelSupport.getDefault().isCancelled()) { |
| return CodeCompletionResult.NONE; |
| } |
| |
| if (LOGGER.isLoggable(Level.FINE)) { |
| long time = System.currentTimeMillis() - startTime; |
| LOGGER.fine(String.format("complete() took %d ms, result contains %d items", time, completionResult.getItems().size())); |
| } |
| |
| return completionResult; |
| } |
| |
| private List<ElementFilter> createTypeFilter(final EnclosingClass enclosingClass) { |
| List<ElementFilter> superTypeIndices = new ArrayList<>(); |
| Expression superClass = enclosingClass.getSuperClass(); |
| if (superClass != null) { |
| String superClsName = enclosingClass.extractUnqualifiedSuperClassName(); |
| superTypeIndices.add(ElementFilter.forSuperClassName(QualifiedName.create(superClsName))); |
| } |
| List<Expression> interfaces = enclosingClass.getInterfaces(); |
| Set<QualifiedName> superIfaceNames = new HashSet<>(); |
| for (Expression identifier : interfaces) { |
| String ifaceName = CodeUtils.extractUnqualifiedName(identifier); |
| if (ifaceName != null) { |
| superIfaceNames.add(QualifiedName.create(ifaceName)); |
| } |
| } |
| if (!superIfaceNames.isEmpty()) { |
| superTypeIndices.add(ElementFilter.forSuperInterfaceNames(superIfaceNames)); |
| } |
| return superTypeIndices; |
| } |
| |
| private void autoCompleteMethodName(ParserResult info, int caretOffset, final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| EnclosingClass enclosingClass = findEnclosingClass(info, lexerToASTOffset(info, caretOffset)); |
| if (enclosingClass != null) { |
| List<ElementFilter> superTypeIndices = createTypeFilter(enclosingClass); |
| String clsName = enclosingClass.getClassName(); |
| NamespaceScope namespaceScope = ModelUtils.getNamespaceScope(request.result.getModel().getFileScope(), request.anchor); |
| String fullyQualifiedClassName = VariousUtils.qualifyTypeNames(clsName, request.anchor, namespaceScope); |
| if (fullyQualifiedClassName != null) { |
| final FileObject fileObject = request.result.getSnapshot().getSource().getFileObject(); |
| final ElementFilter classFilter = ElementFilter.allOf( |
| ElementFilter.forFiles(fileObject), ElementFilter.allOf(superTypeIndices)); |
| Set<ClassElement> classes = classFilter.filter(request.index.getClasses(NameKind.exact(fullyQualifiedClassName))); |
| for (ClassElement classElement : classes) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| ElementFilter methodFilter = ElementFilter.allOf( |
| ElementFilter.forExcludedNames(toNames(request.index.getDeclaredMethods(classElement)), PhpElementKind.METHOD), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix)))); |
| Set<MethodElement> accessibleMethods = methodFilter.filter(request.index.getAccessibleMethods(classElement, classElement)); |
| for (MethodElement method : accessibleMethods) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (!method.isFinal()) { |
| completionResult.add(PHPCompletionItem.MethodDeclarationItem.forMethodName(method, request)); |
| } |
| } |
| Set<MethodElement> magicMethods = methodFilter.filter(request.index.getAccessibleMagicMethods(classElement)); |
| for (MethodElement magicMethod : magicMethods) { |
| if (magicMethod != null) { |
| completionResult.add(PHPCompletionItem.MethodDeclarationItem.forMethodName(magicMethod, request)); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * Finding item after new keyword. |
| * |
| * @param completionResult |
| * @param request |
| */ |
| private void autoCompleteNewClass(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| // At first find all classes that match the prefix |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final QualifiedName prefix = QualifiedName.create(request.prefix).toNotFullyQualified(); |
| final NameKind nameQuery = NameKind.create(request.prefix, |
| isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| Model model = request.result.getModel(); |
| Set<ClassElement> classes = request.index.getClasses(nameQuery, ModelUtils.getAliasedNames(model, request.anchor), Trait.ALIAS); |
| if (!classes.isEmpty()) { |
| completionResult.setFilterable(false); |
| } |
| boolean addedExact = false; |
| final NameKind query; |
| if (classes.size() == 1) { |
| ClassElement clazz = (ClassElement) classes.toArray()[0]; |
| if (!clazz.isAbstract() |
| && !clazz.isAnonymous()) { |
| // if there is only once class find constructors for it |
| query = isCamelCase ? NameKind.create(prefix.toString(), QuerySupport.Kind.CAMEL_CASE) : NameKind.caseInsensitivePrefix(prefix); |
| autoCompleteConstructors(completionResult, request, model, query); |
| } |
| } else { |
| for (ClassElement clazz : classes) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (!clazz.isAbstract() |
| && !clazz.isAnonymous()) { |
| // check whether the prefix is exactly the class |
| NamespaceScope namespaceScope = ModelUtils.getNamespaceScope(request.result.getModel().getFileScope(), request.anchor); |
| String fqPrefixName = VariousUtils.qualifyTypeNames(request.prefix, request.anchor, namespaceScope); |
| if (clazz.getFullyQualifiedName().toString().equals(fqPrefixName)) { |
| // find constructor of the class |
| if (!addedExact) { // add the constructors only once |
| autoCompleteConstructors(completionResult, request, model, NameKind.exact(fqPrefixName)); |
| addedExact = true; |
| } |
| } else { |
| // put to the cc just the class |
| completionResult.add(new PHPCompletionItem.ClassItem(clazz, request, false, null)); |
| } |
| } |
| } |
| } |
| } |
| |
| private void autoCompleteConstructors(final PHPCompletionResult completionResult, final PHPCompletionItem.CompletionRequest request, final Model model, final NameKind query) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| Set<AliasedName> aliasedNames = ModelUtils.getAliasedNames(model, request.anchor); |
| Set<MethodElement> constructors = request.index.getConstructors(query, aliasedNames, Trait.ALIAS); |
| for (MethodElement constructor : constructors) { |
| for (final PHPCompletionItem.NewClassItem newClassItem : PHPCompletionItem.NewClassItem.getNewClassItems(constructor, request)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(newClassItem); |
| } |
| } |
| } |
| |
| private void autoCompleteExceptions(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request, boolean withConstructors) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final NameKind nameQuery = NameKind.create(request.prefix, isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| final Set<ClassElement> classes = request.index.getClasses(nameQuery); |
| final Model model = request.result.getModel(); |
| final Set<QualifiedName> constructorClassNames = new HashSet<>(); |
| for (ClassElement classElement : classes) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (isExceptionClass(classElement)) { |
| completionResult.add(new PHPCompletionItem.ClassItem(classElement, request, false, null)); |
| if (withConstructors) { |
| constructorClassNames.add(classElement.getFullyQualifiedName()); |
| } |
| continue; |
| } |
| if (classElement.getSuperClassName() != null) { |
| Set<ClassElement> inheritedClasses = request.index.getInheritedClasses(classElement); |
| for (ClassElement inheritedClass : inheritedClasses) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (isExceptionClass(inheritedClass)) { |
| completionResult.add(new PHPCompletionItem.ClassItem(classElement, request, false, null)); |
| if (withConstructors) { |
| constructorClassNames.add(classElement.getFullyQualifiedName()); |
| } |
| break; |
| } |
| } |
| } |
| } |
| for (QualifiedName qualifiedName : constructorClassNames) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| autoCompleteConstructors(completionResult, request, model, NameKind.exact(qualifiedName)); |
| } |
| } |
| |
| private boolean isExceptionClass(ClassElement classElement) { |
| return classElement.getFullyQualifiedName().toString().equals(EXCEPTION_CLASS_NAME); |
| } |
| |
| private void autoCompleteClassNames(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, boolean endWithDoubleColon) { |
| autoCompleteClassNames(completionResult, request, endWithDoubleColon, null); |
| } |
| |
| private void autoCompleteClassNames(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, boolean endWithDoubleColon, QualifiedNameKind kind) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final NameKind nameQuery = NameKind.create(request.prefix, |
| isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| Model model = request.result.getModel(); |
| Set<ClassElement> classes = request.index.getClasses(nameQuery, ModelUtils.getAliasedNames(model, request.anchor), Trait.ALIAS); |
| |
| if (!classes.isEmpty()) { |
| completionResult.setFilterable(false); |
| } |
| for (ClassElement clazz : classes) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (!clazz.isAnonymous()) { |
| completionResult.add(new PHPCompletionItem.ClassItem(clazz, request, endWithDoubleColon, kind)); |
| } |
| } |
| } |
| |
| private void autoCompleteInterfaceNames(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| autoCompleteInterfaceNames(completionResult, request, null); |
| } |
| |
| private void autoCompleteInterfaceNames(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request, QualifiedNameKind kind) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final NameKind nameQuery = NameKind.create(request.prefix, |
| isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| |
| Model model = request.result.getModel(); |
| Set<InterfaceElement> interfaces = request.index.getInterfaces(nameQuery, ModelUtils.getAliasedNames(model, request.anchor), Trait.ALIAS); |
| if (!interfaces.isEmpty()) { |
| completionResult.setFilterable(false); |
| } |
| |
| for (InterfaceElement iface : interfaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.InterfaceItem(iface, request, kind, false)); |
| } |
| } |
| |
| private void autoCompleteTypeNames(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| autoCompleteTypeNames(completionResult, request, null, false); |
| } |
| |
| private void autoCompleteAfterUseTrait(final PHPCompletionResult completionResult, final PHPCompletionItem.CompletionRequest request, final QualifiedNameKind kind) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| Set<NamespaceElement> namespaces = request.index.getNamespaces( |
| NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix).toNotFullyQualified())); |
| for (NamespaceElement namespace : namespaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.NamespaceItem(namespace, request, QualifiedNameKind.FULLYQUALIFIED)); |
| } |
| final NameKind nameQuery = NameKind.caseInsensitivePrefix(request.prefix); |
| for (TraitElement trait : request.index.getTraits(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.TraitItem(trait, request)); |
| } |
| } |
| |
| private void autoCompleteGroupUse(UseType useType, PHPCompletionResult completionResult, CompletionRequest request) { |
| assert request.extraPrefix != null; |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| request.insertOnlyMethodsName = true; |
| // we will "complete" FQN so handle search prefix as well |
| if (!request.extraPrefix.startsWith("\\")) { // NOI18N |
| request.extraPrefix = "\\" + request.extraPrefix; // NOI18N |
| } |
| final String prefix = request.extraPrefix + request.prefix; |
| Set<NamespaceElement> namespaces = request.index.getNamespaces( |
| NameKind.caseInsensitivePrefix(QualifiedName.create(prefix).toNotFullyQualified())); |
| for (NamespaceElement namespace : namespaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.NamespaceItem(namespace, request, QualifiedNameKind.FULLYQUALIFIED)); |
| } |
| final NameKind nameQuery = NameKind.caseInsensitivePrefix(prefix); |
| switch (useType) { |
| case TYPE: |
| for (ClassElement clazz : request.index.getClasses(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.ClassItem(clazz, request, false, QualifiedNameKind.FULLYQUALIFIED)); |
| } |
| for (InterfaceElement iface : request.index.getInterfaces(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.InterfaceItem(iface, request, QualifiedNameKind.FULLYQUALIFIED, false)); |
| } |
| // NETBEANS-4650 |
| for (TraitElement trait : request.index.getTraits(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.TraitItem(trait, request)); |
| } |
| break; |
| case CONST: |
| for (ConstantElement constant : request.index.getConstants(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.ConstantItem(constant, request, QualifiedNameKind.FULLYQUALIFIED)); |
| } |
| break; |
| case FUNCTION: |
| for (FunctionElement function : request.index.getFunctions(nameQuery)) { |
| for (PHPCompletionItem.FunctionElementItem item : PHPCompletionItem.FunctionElementItem.getItems(function, request, QualifiedNameKind.FULLYQUALIFIED)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(item); |
| } |
| } |
| break; |
| default: |
| assert false : "Unknown use type: " + useType; |
| } |
| } |
| |
| private void autoCompleteAfterUse( |
| UseType useType, |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, |
| QualifiedNameKind kind) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| Set<NamespaceElement> namespaces = request.index.getNamespaces( |
| NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix).toNotFullyQualified())); |
| for (NamespaceElement namespace : namespaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.NamespaceItem(namespace, request, kind)); |
| } |
| final NameKind nameQuery = NameKind.caseInsensitivePrefix(request.prefix); |
| switch (useType) { |
| case TYPE: |
| for (ClassElement clazz : request.index.getClasses(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.ClassItem(clazz, request, false, kind)); |
| } |
| for (InterfaceElement iface : request.index.getInterfaces(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.InterfaceItem(iface, request, kind, false)); |
| } |
| // NETBEANS-4650 |
| for (TraitElement trait : request.index.getTraits(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.TraitItem(trait, request)); |
| } |
| break; |
| case CONST: |
| for (ConstantElement constant : request.index.getConstants(nameQuery)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.ConstantItem(constant, request)); |
| } |
| break; |
| case FUNCTION: |
| for (FunctionElement function : request.index.getFunctions(nameQuery)) { |
| List<PHPCompletionItem.FunctionElementItem> items = PHPCompletionItem.FunctionElementItem.getItems(function, request); |
| for (PHPCompletionItem.FunctionElementItem item : items) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(item); |
| } |
| } |
| break; |
| default: |
| assert false : "Unknown use type: " + useType; |
| } |
| } |
| |
| private void autoCompleteTypeNames( |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, |
| QualifiedNameKind kind, |
| boolean endWithDoubleColon) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (request.prefix.trim().length() > 0) { |
| autoCompleteClassNames(completionResult, request, endWithDoubleColon, kind); |
| autoCompleteInterfaceNames(completionResult, request, kind); |
| } else { |
| Model model = request.result.getModel(); |
| Set<AliasedName> aliasedNames = ModelUtils.getAliasedNames(model, request.anchor); |
| Collection<PhpElement> allTopLevel = request.index.getTopLevelElements(NameKind.empty(), aliasedNames, Trait.ALIAS); |
| for (PhpElement element : allTopLevel) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (element instanceof ClassElement) { |
| ClassElement classElement = (ClassElement) element; |
| if (!classElement.isAnonymous()) { |
| completionResult.add(new PHPCompletionItem.ClassItem(classElement, request, endWithDoubleColon, kind)); |
| } |
| } else if (element instanceof InterfaceElement) { |
| completionResult.add(new PHPCompletionItem.InterfaceItem((InterfaceElement) element, request, kind, endWithDoubleColon)); |
| } |
| } |
| } |
| } |
| |
| private void autoCompleteKeywords(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, List<String> keywordList) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| for (String keyword : keywordList) { |
| if (keyword.startsWith(request.prefix)) { |
| completionResult.add(new PHPCompletionItem.KeywordItem(keyword, request)); |
| } |
| } |
| |
| } |
| |
| private void autoCompleteKeywordsInPHPDoc(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| BaseDocument doc = (BaseDocument) request.info.getSnapshot().getSource().getDocument(false); |
| if (doc == null) { |
| return; |
| } |
| try { |
| int start = request.anchor - 1; |
| if (start >= 0) { |
| String prefix = doc.getText(start, 1); |
| if (CodeUtils.NULLABLE_TYPE_PREFIX.equals(prefix)) { |
| List<String> keywords = new ArrayList<>(Type.getTypesForEditor()); |
| keywords.remove(Type.FALSE); |
| keywords.remove(Type.NULL); |
| autoCompleteKeywords(completionResult, request, keywords); |
| } else { |
| autoCompleteKeywords(completionResult, request, Type.getTypesForPhpDoc()); |
| } |
| } |
| } catch (BadLocationException ex) { |
| LOGGER.log(Level.WARNING, "Incorrect offset for the nullable type prefix: {0}", ex.offsetRequested()); // NOI18N |
| } |
| } |
| |
| private void autoCompleteNamespaces(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request) { |
| autoCompleteNamespaces(completionResult, request, null); |
| } |
| |
| private void autoCompleteNamespaces(final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, QualifiedNameKind kind) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final QualifiedName prefix = QualifiedName.create(request.prefix).toNotFullyQualified(); |
| Model model = request.result.getModel(); |
| Set<NamespaceElement> namespaces = request.index.getNamespaces(NameKind.caseInsensitivePrefix(prefix), |
| ModelUtils.getAliasedNames(model, request.anchor), Trait.ALIAS); |
| for (NamespaceElement namespace : namespaces) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.NamespaceItem(namespace, request, kind)); |
| } |
| } |
| |
| private void autoCompleteInInterfaceContext(final PHPCompletionResult completionResult, final PHPCompletionItem.CompletionRequest request) { |
| autoCompleteKeywords(completionResult, request, INTERFACE_CONTEXT_KEYWORD_PROPOSAL); |
| } |
| |
| private void autoCompleteInClassContext( |
| ParserResult info, |
| int caretOffset, |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = th.tokenSequence(PHPTokenId.language()); |
| assert tokenSequence != null; |
| |
| autoCompleteKeywords(completionResult, request, CLASS_CONTEXT_KEYWORD_PROPOSAL); |
| if (offerMagicAndInherited(tokenSequence, caretOffset, th)) { |
| EnclosingClass enclosingClass = findEnclosingClass(info, lexerToASTOffset(info, caretOffset)); |
| if (enclosingClass != null) { |
| List<ElementFilter> superTypeIndices = createTypeFilter(enclosingClass); |
| String clsName = enclosingClass.getClassName(); |
| NamespaceScope namespaceScope = ModelUtils.getNamespaceScope(request.result.getModel().getFileScope(), request.anchor); |
| String fullyQualifiedClassName = VariousUtils.qualifyTypeNames(clsName, request.anchor, namespaceScope); |
| if (fullyQualifiedClassName != null) { |
| final FileObject fileObject = request.result.getSnapshot().getSource().getFileObject(); |
| final ElementFilter classFilter = ElementFilter.allOf( |
| ElementFilter.forFiles(fileObject), ElementFilter.allOf(superTypeIndices)); |
| Set<ClassElement> classes = classFilter.filter(request.index.getClasses(NameKind.exact(fullyQualifiedClassName))); |
| for (ClassElement classElement : classes) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| ElementFilter methodFilter = ElementFilter.allOf( |
| ElementFilter.forExcludedNames(toNames(request.index.getDeclaredMethods(classElement)), PhpElementKind.METHOD), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix)))); |
| Set<MethodElement> accessibleMethods = methodFilter.filter(request.index.getAccessibleMethods(classElement, classElement)); |
| for (MethodElement method : accessibleMethods) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (!method.isFinal()) { |
| completionResult.add(PHPCompletionItem.MethodDeclarationItem.getDeclarationItem(method, request)); |
| } |
| } |
| Set<MethodElement> magicMethods = methodFilter.filter(request.index.getAccessibleMagicMethods(classElement)); |
| for (MethodElement magicMethod : magicMethods) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (magicMethod != null) { |
| completionResult.add(PHPCompletionItem.MethodDeclarationItem.getDeclarationItem(magicMethod, request)); |
| } |
| } |
| break; |
| } |
| } |
| } |
| } else if (completeFieldTypes(tokenSequence, caretOffset, th, info.getSnapshot().getSource().getFileObject())){ |
| autoCompleteFieldType(info, caretOffset, completionResult, request, true); |
| } |
| } |
| |
| private void autoCompleteFieldType(ParserResult info, int caretOffset, final PHPCompletionResult completionResult, CompletionRequest request, boolean isInClassContext) { |
| // PHP 7.4 Typed Properties 2.0 |
| // https://wiki.php.net/rfc/typed_properties_v2 |
| autoCompleteNamespaces(completionResult, request); |
| autoCompleteTypeNames(completionResult, request); |
| List<String> keywords = new ArrayList<>(Type.getTypesForFieldType()); |
| boolean isNullableType = isNullableType(info, caretOffset); |
| if (!isInClassContext && !isNullableType) { |
| // e.g. private stat^ |
| TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = th.tokenSequence(PHPTokenId.language()); |
| assert tokenSequence != null; |
| tokenSequence.move(caretOffset); |
| boolean addStaticKeyword = false; |
| boolean addReadonlyKeyword = false; |
| boolean addVisibilityKeyword = false; |
| if (!(!tokenSequence.moveNext() && !tokenSequence.movePrevious())) { |
| Token<PHPTokenId> token = tokenSequence.token(); |
| int tokenIdOffset = tokenSequence.token().offset(th); |
| addStaticKeyword = !CompletionContextFinder.lineContainsAny(token, caretOffset - tokenIdOffset, tokenSequence, Arrays.asList( |
| PHPTokenId.PHP_STATIC, |
| PHPTokenId.PHP_READONLY, |
| PHPTokenId.PHP_OPERATOR // "|" |
| )); |
| addReadonlyKeyword = !CompletionContextFinder.lineContainsAny(token, caretOffset - tokenIdOffset, tokenSequence, Arrays.asList( |
| PHPTokenId.PHP_READONLY, |
| PHPTokenId.PHP_OPERATOR // "|" |
| )); |
| addVisibilityKeyword = !CompletionContextFinder.lineContainsAny(token, caretOffset - tokenIdOffset, tokenSequence, Arrays.asList( |
| PHPTokenId.PHP_PUBLIC, |
| PHPTokenId.PHP_PRIVATE, |
| PHPTokenId.PHP_PROTECTED, |
| PHPTokenId.PHP_OPERATOR // "|" |
| )); |
| } |
| if (addStaticKeyword) { |
| keywords.add("static"); // NOI18N |
| } |
| if (addReadonlyKeyword) { |
| keywords.add("readonly"); // NOI18N |
| } |
| if (addVisibilityKeyword) { |
| keywords.addAll(PHP_VISIBILITY_KEYWORDS); |
| } |
| } |
| if (isNullableType) { |
| keywords.remove(Type.FALSE); |
| keywords.remove(Type.NULL); |
| } |
| if (isUnionType(info, caretOffset)) { |
| keywords.remove(Type.MIXED); |
| } |
| autoCompleteKeywords(completionResult, request, keywords); |
| } |
| |
| private boolean offerMagicAndInherited(TokenSequence<PHPTokenId> tokenSequence, int caretOffset, TokenHierarchy<?> th) { |
| boolean offerMagicAndInherited = true; |
| tokenSequence.move(caretOffset); |
| if (!(!tokenSequence.moveNext() && !tokenSequence.movePrevious())) { |
| Token<PHPTokenId> token = tokenSequence.token(); |
| int tokenIdOffset = tokenSequence.token().offset(th); |
| offerMagicAndInherited = !CompletionContextFinder.lineContainsAny(token, caretOffset - tokenIdOffset, tokenSequence, Arrays.asList( |
| PHPTokenId.PHP_PRIVATE, |
| PHPTokenId.PHP_PUBLIC, |
| PHPTokenId.PHP_PROTECTED, |
| PHPTokenId.PHP_ABSTRACT, |
| PHPTokenId.PHP_VAR, |
| PHPTokenId.PHP_STATIC, |
| PHPTokenId.PHP_CONST, |
| PHPTokenId.PHP_READONLY |
| )); |
| } |
| return offerMagicAndInherited; |
| } |
| |
| private boolean completeFieldTypes(TokenSequence<PHPTokenId> tokenSequence, int caretOffset, TokenHierarchy<?> th, FileObject fileObject) { |
| if (!isPhp74OrNewer(fileObject)) { |
| return false; |
| } |
| // e.g. private static s^tring|int $field; private bool ^$bool; |
| boolean completeTypes = false; |
| tokenSequence.move(caretOffset); |
| if (!(!tokenSequence.moveNext() && !tokenSequence.movePrevious())) { |
| Token<PHPTokenId> token = tokenSequence.token(); |
| int tokenIdOffset = tokenSequence.token().offset(th); |
| completeTypes = !CompletionContextFinder.lineContainsAny(token, caretOffset - tokenIdOffset, tokenSequence, Arrays.asList( |
| PHPTokenId.PHP_TYPE_BOOL, |
| PHPTokenId.PHP_TYPE_INT, |
| PHPTokenId.PHP_TYPE_FLOAT, |
| PHPTokenId.PHP_TYPE_STRING, |
| PHPTokenId.PHP_ARRAY, |
| PHPTokenId.PHP_TYPE_OBJECT, |
| PHPTokenId.PHP_ITERABLE, |
| PHPTokenId.PHP_SELF, |
| PHPTokenId.PHP_PARENT, |
| PHPTokenId.PHP_FALSE, |
| PHPTokenId.PHP_NULL, |
| PHPTokenId.PHP_STRING, |
| PHPTokenId.PHP_CONST |
| )); |
| } |
| return completeTypes; |
| } |
| |
| private static Set<String> toNames(Set<? extends PhpElement> elements) { |
| Set<String> names = new HashSet<>(); |
| for (PhpElement elem : elements) { |
| names.add(elem.getName()); |
| } |
| return names; |
| } |
| |
| private void autoCompleteClassMembers( |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, |
| boolean staticContext) { |
| autoCompleteClassMembers(completionResult, request, staticContext, false); |
| } |
| |
| private void autoCompleteClassMembers( |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, |
| boolean staticContext, |
| boolean completeAccessPrefix) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| // TODO: remove duplicate/redundant code from here |
| |
| TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = LexUtilities.getPHPTokenSequence(th, request.anchor); |
| |
| if (tokenSequence == null) { |
| return; |
| } |
| |
| tokenSequence.move(request.anchor); |
| if (tokenSequence.movePrevious()) { |
| boolean instanceContext = !staticContext; |
| |
| if (tokenSequence.token().id() != PHPTokenId.PHP_PAAMAYIM_NEKUDOTAYIM |
| && tokenSequence.token().id() != PHPTokenId.PHP_OBJECT_OPERATOR |
| && tokenSequence.token().id() != PHPTokenId.PHP_NULLSAFE_OBJECT_OPERATOR) { |
| tokenSequence.movePrevious(); |
| } |
| tokenSequence.movePrevious(); |
| if (tokenSequence.token().id() == PHPTokenId.WHITESPACE) { |
| tokenSequence.movePrevious(); |
| } |
| final CharSequence varName = tokenSequence.token().text(); |
| tokenSequence.moveNext(); |
| |
| List<String> invalidProposalsForClsMembers = INVALID_PROPOSALS_FOR_CLS_MEMBERS; |
| Model model = request.result.getModel(); |
| |
| boolean parentContext = false; |
| boolean selfContext = false; |
| boolean staticLateBindingContext = false; |
| boolean specialVariable = false; |
| if (TokenUtilities.textEquals(varName, "$this")) { // NOI18N |
| specialVariable = true; |
| } else if (TokenUtilities.textEquals(varName, "self")) { // NOI18N |
| staticContext = true; |
| selfContext = true; |
| specialVariable = true; |
| } else if (TokenUtilities.textEquals(varName, "parent")) { // NOI18N |
| invalidProposalsForClsMembers = Collections.emptyList(); |
| staticContext = true; |
| instanceContext = true; |
| specialVariable = true; |
| parentContext = true; |
| } else if (TokenUtilities.textEquals(varName, "static")) { // NOI18N |
| staticContext = true; |
| instanceContext = false; |
| staticLateBindingContext = true; |
| specialVariable = true; |
| } |
| |
| Collection<? extends TypeScope> types = ModelUtils.resolveTypeAfterReferenceToken(model, tokenSequence, request.anchor, specialVariable); |
| if (types != null) { |
| TypeElement enclosingType = getEnclosingType(request, types); |
| if (completeAccessPrefix) { |
| // NETBEANS-1855 |
| types = ModelUtils.resolveType(model, request.anchor); |
| } |
| Set<PhpElement> duplicateElementCheck = new HashSet<>(); |
| for (TypeScope typeScope : types) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final ElementFilter staticFlagFilter = !completeAccessPrefix |
| ? new StaticOrInstanceMembersFilter(staticContext, instanceContext, selfContext, staticLateBindingContext, parentContext) |
| : new ElementFilter() { // NETBEANS-1855 |
| @Override |
| public boolean isAccepted(PhpElement element) { |
| return true; |
| } |
| }; |
| |
| final ElementFilter methodsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.METHOD), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(request.prefix)), |
| staticFlagFilter, |
| ElementFilter.forExcludedNames(invalidProposalsForClsMembers, PhpElementKind.METHOD), |
| ElementFilter.forInstanceOf(MethodElement.class)); |
| final ElementFilter fieldsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.FIELD), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(request.prefix)), |
| staticFlagFilter, |
| ElementFilter.forInstanceOf(FieldElement.class)); |
| final ElementFilter constantsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.TYPE_CONSTANT), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(request.prefix)), |
| ElementFilter.forInstanceOf(TypeConstantElement.class)); |
| HashSet<TypeMemberElement> accessibleTypeMembers = new HashSet<>(); |
| accessibleTypeMembers.addAll(request.index.getAccessibleTypeMembers(typeScope, enclosingType)); |
| // for @mixin tag #241740 |
| if (typeScope instanceof ClassElement) { |
| ClassElement classElement = (ClassElement) typeScope; |
| if (!classElement.getFQMixinClassNames().isEmpty()) { |
| // XXX currently, only when mixins are used directly in the class. should support all cases? |
| accessibleTypeMembers.addAll(request.index.getAccessibleMixinTypeMembers(typeScope, enclosingType)); |
| } |
| } |
| for (final PhpElement phpElement : accessibleTypeMembers) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (duplicateElementCheck.add(phpElement)) { |
| if (methodsFilter.isAccepted(phpElement)) { |
| MethodElement method = (MethodElement) phpElement; |
| List<MethodElementItem> items = PHPCompletionItem.MethodElementItem.getItems(method, request, completeAccessPrefix); |
| for (MethodElementItem methodItem : items) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(methodItem); |
| } |
| } else if (fieldsFilter.isAccepted(phpElement)) { |
| FieldElement field = (FieldElement) phpElement; |
| FieldItem fieldItem = PHPCompletionItem.FieldItem.getItem(field, request, false, completeAccessPrefix); |
| completionResult.add(fieldItem); |
| } else if ((staticContext || completeAccessPrefix) && constantsFilter.isAccepted(phpElement)) { |
| TypeConstantElement constant = (TypeConstantElement) phpElement; |
| TypeConstantItem constantItem = PHPCompletionItem.TypeConstantItem.getItem(constant, request, completeAccessPrefix); |
| completionResult.add(constantItem); |
| } |
| } |
| } |
| if (staticContext) { |
| Set<TypeConstantElement> magicConstants = constantsFilter.filter(request.index.getAccessibleMagicConstants(typeScope)); |
| for (TypeConstantElement magicConstant : magicConstants) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (magicConstant != null) { |
| // NETBEANS-4443 |
| // PHP 8.0 allows ::class on objects (e.g. $instance::class, create()::class) |
| // so don't restrict dynamic access any more |
| // https://wiki.php.net/rfc/class_name_literal_on_object |
| completionResult.add(PHPCompletionItem.TypeConstantItem.getItem(magicConstant, request)); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void autoCompleteClassMethodParameterName( |
| final PHPCompletionResult completionResult, |
| PHPCompletionItem.CompletionRequest request, |
| boolean staticContext |
| ) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = LexUtilities.getPHPTokenSequence(th, request.anchor); |
| if (tokenSequence == null) { |
| return; |
| } |
| Token<? extends PHPTokenId> functionName = CompletionContextFinder.findFunctionInvocationName(tokenSequence, request.anchor); |
| if (functionName != null) { |
| int originalAnchor = request.anchor; |
| try { |
| request.anchor = tokenSequence.offset(); |
| boolean isInstanceContext = !staticContext; |
| boolean isStaticContext = staticContext; |
| |
| if (tokenSequence.token().id() != PHPTokenId.PHP_PAAMAYIM_NEKUDOTAYIM |
| && tokenSequence.token().id() != PHPTokenId.PHP_OBJECT_OPERATOR |
| && tokenSequence.token().id() != PHPTokenId.PHP_NULLSAFE_OBJECT_OPERATOR) { |
| tokenSequence.movePrevious(); |
| } |
| tokenSequence.movePrevious(); |
| if (tokenSequence.token().id() == PHPTokenId.WHITESPACE) { |
| tokenSequence.movePrevious(); |
| } |
| final CharSequence varName = tokenSequence.token().text(); |
| tokenSequence.moveNext(); |
| |
| List<String> invalidProposalsForClsMembers = INVALID_PROPOSALS_FOR_CLS_MEMBERS; |
| Model model = request.result.getModel(); |
| |
| boolean parentContext = false; |
| boolean selfContext = false; |
| boolean staticLateBindingContext = false; |
| boolean specialVariable = false; |
| if (TokenUtilities.textEquals(varName, "$this")) { // NOI18N |
| specialVariable = true; |
| } else if (TokenUtilities.textEquals(varName, "self")) { // NOI18N |
| isStaticContext = true; |
| selfContext = true; |
| specialVariable = true; |
| } else if (TokenUtilities.textEquals(varName, "parent")) { // NOI18N |
| invalidProposalsForClsMembers = Collections.emptyList(); |
| isStaticContext = true; |
| isInstanceContext = true; |
| specialVariable = true; |
| parentContext = true; |
| } else if (TokenUtilities.textEquals(varName, "static")) { // NOI18N |
| isStaticContext = true; |
| isInstanceContext = false; |
| staticLateBindingContext = true; |
| specialVariable = true; |
| } |
| |
| Collection<? extends TypeScope> types = ModelUtils.resolveTypeAfterReferenceToken(model, tokenSequence, request.anchor, specialVariable); |
| TypeElement enclosingType = getEnclosingType(request, types); |
| Set<PhpElement> duplicateElementCheck = new HashSet<>(); |
| for (TypeScope typeScope : types) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final ElementFilter staticFlagFilter = new StaticOrInstanceMembersFilter(isStaticContext, isInstanceContext, selfContext, staticLateBindingContext, true); |
| final ElementFilter methodsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.METHOD), |
| ElementFilter.forName(NameKind.exact(functionName.text().toString())), |
| staticFlagFilter, |
| ElementFilter.forExcludedNames(invalidProposalsForClsMembers, PhpElementKind.METHOD), |
| ElementFilter.forInstanceOf(MethodElement.class)); |
| HashSet<TypeMemberElement> accessibleTypeMembers = new HashSet<>(); |
| accessibleTypeMembers.addAll(request.index.getAccessibleTypeMembers(typeScope, enclosingType)); |
| if (typeScope instanceof ClassElement) { |
| ClassElement classElement = (ClassElement) typeScope; |
| if (!classElement.getFQMixinClassNames().isEmpty()) { |
| accessibleTypeMembers.addAll(request.index.getAccessibleMixinTypeMembers(typeScope, enclosingType)); |
| } |
| } |
| for (final PhpElement phpElement : accessibleTypeMembers) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (duplicateElementCheck.add(phpElement)) { |
| if (methodsFilter.isAccepted(phpElement)) { |
| MethodElement method = (MethodElement) phpElement; |
| for (ParameterElement parameter : method.getParameters()) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| String name = parameter.getName(); |
| if (name != null) { |
| name = name.substring(1); |
| } |
| if (name != null |
| && name.startsWith(request.prefix)) { |
| completionResult.add(new PHPCompletionItem.ParameterNameItem(parameter, request)); |
| } |
| } |
| } |
| } |
| } |
| } |
| } finally { |
| request.anchor = originalAnchor; |
| } |
| } |
| } |
| |
| private void autoCompleteFunctionParameterName(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = LexUtilities.getPHPTokenSequence(th, request.anchor); |
| if (tokenSequence == null) { |
| return; |
| } |
| Token<? extends PHPTokenId> functionName = CompletionContextFinder.findFunctionInvocationName(tokenSequence, request.anchor); |
| if (functionName != null) { |
| Set<PhpElement> elements = request.index.getTopLevelElements(NameKind.exact(functionName.text().toString())); |
| // usually, php doesn't have the same name functions |
| // but just check duplicate name |
| Set<String> duplicateCheck = new HashSet<>(); |
| for (PhpElement element : elements) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (element instanceof FunctionElement) { |
| FunctionElement functionElement = (FunctionElement) element; |
| if (functionElement.isAnonymous()) { |
| continue; |
| } |
| for (ParameterElement parameter : functionElement.getParameters()) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| String name = parameter.getName(); |
| if (name != null) { |
| name = name.substring(1); |
| } |
| if (name != null |
| && name.startsWith(request.prefix) |
| && duplicateCheck.add(name)) { |
| completionResult.add(new PHPCompletionItem.ParameterNameItem(parameter, request)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void autoCompleteClassConstants(final PHPCompletionResult completionResult, final PHPCompletionItem.CompletionRequest request) { |
| // NETBANS-1855 |
| // complete access prefix i.e. add "self::" to the top of constant names |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| final ElementFilter constantsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.TYPE_CONSTANT), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(request.prefix)), |
| ElementFilter.forInstanceOf(TypeConstantElement.class) |
| ); |
| Model model = request.result.getModel(); |
| Collection<? extends TypeScope> types = ModelUtils.resolveType(model, request.anchor); |
| TypeElement enclosingType = getEnclosingType(request, types); |
| for (TypeScope typeScope : types) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| for (final PhpElement phpElement : request.index.getAccessibleTypeMembers(typeScope, enclosingType)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (constantsFilter.isAccepted(phpElement)) { |
| TypeConstantElement constant = (TypeConstantElement) phpElement; |
| TypeConstantItem constantItem = PHPCompletionItem.TypeConstantItem.getItem(constant, request, true); |
| completionResult.add(constantItem); |
| } |
| } |
| } |
| |
| } |
| |
| private void autoCompleteClassFields(final PHPCompletionResult completionResult, final PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| TokenHierarchy<?> th = request.info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = LexUtilities.getPHPTokenSequence(th, request.anchor); |
| Model model = request.result.getModel(); |
| Collection<? extends TypeScope> types = ModelUtils.resolveTypeAfterReferenceToken(model, tokenSequence, request.anchor, false); |
| final ElementFilter fieldsFilter = ElementFilter.allOf( |
| ElementFilter.forKind(PhpElementKind.FIELD), |
| ElementFilter.forName(NameKind.caseInsensitivePrefix(request.prefix)), |
| ElementFilter.forInstanceOf(FieldElement.class)); |
| if (types != null) { |
| TypeElement enclosingType = getEnclosingType(request, types); |
| for (TypeScope typeScope : types) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| for (final PhpElement phpElement : request.index.getAccessibleTypeMembers(typeScope, enclosingType)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (fieldsFilter.isAccepted(phpElement)) { |
| FieldElement field = (FieldElement) phpElement; |
| FieldItem fieldItem = PHPCompletionItem.FieldItem.getItem(field, request); |
| completionResult.add(fieldItem); |
| } |
| } |
| } |
| } |
| } |
| |
| @CheckForNull |
| private TypeElement getEnclosingType(CompletionRequest request, Collection<? extends TypeScope> types) { |
| final EnclosingType enclosingType = findEnclosingType(request.info, lexerToASTOffset(request.result, request.anchor)); |
| final String enclosingTypeName = enclosingType != null ? enclosingType.extractTypeName() : null; |
| NamespaceScope namespaceScope = ModelUtils.getNamespaceScope(request.result.getModel().getFileScope(), request.anchor); |
| final String enclosingFQTypeName = VariousUtils.qualifyTypeNames(enclosingTypeName, request.anchor, namespaceScope); |
| final NameKind enclosingTypeNameKind = (enclosingFQTypeName != null && !enclosingFQTypeName.trim().isEmpty()) ? NameKind.exact(enclosingFQTypeName) : null; |
| Set<FileObject> preferedFileObjects = new HashSet<>(); |
| Set<TypeElement> enclosingTypes = null; |
| FileObject currentFile = request.result.getSnapshot().getSource().getFileObject(); |
| if (currentFile != null) { |
| preferedFileObjects.add(currentFile); |
| } |
| for (TypeScope typeScope : types) { |
| final FileObject fileObject = typeScope.getFileObject(); |
| if (fileObject != null) { |
| preferedFileObjects.add(fileObject); |
| } |
| if (enclosingTypeNameKind != null && enclosingTypes == null) { |
| if (enclosingTypeNameKind.matchesName(typeScope)) { |
| enclosingTypes = Collections.<TypeElement>singleton((TypeElement) typeScope); |
| } |
| } |
| } |
| if (enclosingTypeNameKind != null && enclosingTypes == null) { |
| final ElementFilter forFiles = ElementFilter.forFiles(preferedFileObjects.toArray(new FileObject[preferedFileObjects.size()])); |
| Set<TypeElement> indexTypes = forFiles.prefer(request.index.getTypes(enclosingTypeNameKind)); |
| if (!indexTypes.isEmpty()) { |
| enclosingTypes = new HashSet<>(indexTypes); |
| } |
| } |
| return (enclosingTypes == null || enclosingTypes.isEmpty()) ? null : enclosingTypes.iterator().next(); |
| } |
| |
| private static boolean isNullableType(ParserResult info, int caretOffset) { |
| TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = th.tokenSequence(PHPTokenId.language()); |
| assert tokenSequence != null; |
| tokenSequence.move(caretOffset); |
| if (tokenSequence.movePrevious()) { |
| Token<? extends PHPTokenId> previousToken = LexUtilities.findPrevious(tokenSequence, Arrays.asList(PHPTokenId.WHITESPACE, PHPTokenId.PHP_STRING)); |
| if (previousToken.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(previousToken.text(), "?")) { // NOI18N |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isUnionType(ParserResult info, int caretOffset) { |
| TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = th.tokenSequence(PHPTokenId.language()); |
| assert tokenSequence != null; |
| tokenSequence.move(caretOffset); |
| if (tokenSequence.movePrevious()) { |
| Token<? extends PHPTokenId> previousToken = LexUtilities.findPrevious(tokenSequence, VALID_UNION_TYPE_TOKENS); |
| if (previousToken.id() == PHPTokenId.PHP_OPERATOR && TokenUtilities.textEquals(previousToken.text(), Type.SEPARATOR)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isInType(CompletionRequest request) { |
| return findEnclosingType(request.info, lexerToASTOffset(request.result, request.anchor)) != null; |
| } |
| |
| @CheckForNull |
| private static EnclosingType findEnclosingType(ParserResult info, int offset) { |
| List<ASTNode> nodes = NavUtils.underCaret(info, offset); |
| for (int i = nodes.size() - 1; i >= 0; i--) { |
| ASTNode node = nodes.get(i); |
| if (node instanceof TypeDeclaration |
| && node.getEndOffset() > offset) { |
| return EnclosingType.forTypeDeclaration((TypeDeclaration) node); |
| } |
| if (node instanceof ClassInstanceCreation |
| && node.getEndOffset() > offset) { |
| ClassInstanceCreation classInstanceCreation = (ClassInstanceCreation) node; |
| if (classInstanceCreation.isAnonymous()) { |
| Block body = classInstanceCreation.getBody(); |
| if (body != null |
| && body.getStartOffset() <= offset |
| && body.getEndOffset() >= offset) { |
| return EnclosingType.forClassInstanceCreation(classInstanceCreation); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| @CheckForNull |
| private static EnclosingClass findEnclosingClass(ParserResult info, int offset) { |
| List<ASTNode> nodes = NavUtils.underCaret(info, offset); |
| for (int i = nodes.size() - 1; i >= 0; i--) { |
| ASTNode node = nodes.get(i); |
| if (node instanceof ClassDeclaration |
| && node.getEndOffset() > offset) { |
| return EnclosingClass.forClassDeclaration((ClassDeclaration) node); |
| } |
| if (node instanceof ClassInstanceCreation |
| && node.getEndOffset() > offset) { |
| ClassInstanceCreation classInstanceCreation = (ClassInstanceCreation) node; |
| if (classInstanceCreation.isAnonymous()) { |
| Block body = classInstanceCreation.getBody(); |
| if (body != null |
| && body.getStartOffset() <= offset |
| && body.getEndOffset() >= offset) { |
| return EnclosingClass.forClassInstanceCreation((ClassInstanceCreation) node); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private void autoCompleteExpression(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| autoCompleteNamespaces(completionResult, request); |
| List<String> defaultKeywords = new ArrayList<>(PHP_KEYWORDS.keySet()); |
| defaultKeywords.remove("default =>"); // NOI18N |
| autoCompleteExpression(completionResult, request, defaultKeywords); |
| } |
| |
| private void autoCompleteExpression(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request, List<String> keywords) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| // KEYWORDS |
| for (String keyword : keywords) { |
| if (startsWith(keyword, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.KeywordItem(keyword, request)); |
| } |
| } |
| |
| for (String keyword : PHP_LANGUAGE_CONSTRUCTS_WITH_QUOTES) { |
| if (startsWith(keyword, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.LanguageConstructWithQuotesItem(keyword, request)); |
| } |
| } |
| |
| for (String construct : PHP_LANGUAGE_CONSTRUCTS_WITH_PARENTHESES) { |
| if (startsWith(construct, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.LanguageConstructWithParenthesesItem(construct, request)); |
| } |
| } |
| |
| for (String construct : PHP_LANGUAGE_CONSTRUCTS_WITH_SEMICOLON) { |
| if (startsWith(construct, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.LanguageConstructWithSemicolonItem(construct, request)); |
| } |
| } |
| |
| final boolean offerGlobalVariables = OptionsUtils.codeCompletionVariablesScope().equals(VariablesScope.ALL); |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final NameKind prefix = NameKind.create(request.prefix, |
| isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| |
| final Set<VariableElement> globalVariables = new HashSet<>(); |
| |
| Model model = request.result.getModel(); |
| Set<AliasedName> aliasedNames = ModelUtils.getAliasedNames(model, request.anchor); |
| |
| for (final PhpElement element : request.index.getTopLevelElements(prefix, aliasedNames, Trait.ALIAS)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (element instanceof FunctionElement) { |
| FunctionElement functionElement = (FunctionElement) element; |
| if (!functionElement.isAnonymous()) { |
| for (final PHPCompletionItem.FunctionElementItem functionItem |
| : PHPCompletionItem.FunctionElementItem.getItems(functionElement, request)) { |
| completionResult.add(functionItem); |
| } |
| } |
| } else if (element instanceof ClassElement) { |
| ClassElement classElement = (ClassElement) element; |
| if (!classElement.isAnonymous()) { |
| completionResult.add(new PHPCompletionItem.ClassItem(classElement, request, true, null)); |
| } |
| } else if (element instanceof InterfaceElement) { |
| completionResult.add(new PHPCompletionItem.InterfaceItem((InterfaceElement) element, request, true)); |
| } else if (offerGlobalVariables && element instanceof VariableElement) { |
| globalVariables.add((VariableElement) element); |
| } else if (element instanceof ConstantElement) { |
| completionResult.add(new PHPCompletionItem.ConstantItem((ConstantElement) element, request)); |
| } |
| } |
| FileObject fileObject = request.result.getSnapshot().getSource().getFileObject(); |
| final ElementFilter forCurrentFile = ElementFilter.forFiles(fileObject); |
| completionResult.addAll(getVariableProposals(request, forCurrentFile.reverseFilter(globalVariables))); |
| |
| // Special keywords applicable only inside a class or trait |
| final EnclosingType enclosingType = findEnclosingType(request.info, lexerToASTOffset(request.result, request.anchor)); |
| if (enclosingType != null |
| && (enclosingType.isClassDeclaration() || enclosingType.isTraitDeclaration())) { |
| final String typeName = enclosingType.extractTypeName(); |
| if (typeName != null) { |
| for (final String keyword : PHP_CLASS_KEYWORDS) { |
| if (startsWith(keyword, request.prefix)) { |
| completionResult.add(new PHPCompletionItem.ClassScopeKeywordItem(typeName, keyword, request)); |
| } |
| } |
| // NETBEANS-1855 |
| autoCompleteClassMembers(completionResult, request, false, true); |
| } |
| } |
| } |
| |
| private void autoCompleteGlobals(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| if (OptionsUtils.codeCompletionVariablesScope().equals(VariablesScope.ALL)) { |
| final CaseInsensitivePrefix prefix = NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix)); |
| for (VariableElement variableElement : request.index.getTopLevelVariables(prefix)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.VariableItem(variableElement, request)); |
| } |
| } |
| } |
| |
| private void autoCompleteConstants(final PHPCompletionResult completionResult, PHPCompletionItem.CompletionRequest request) { |
| final boolean isCamelCase = isCamelCaseForTypeNames(request.prefix); |
| final NameKind prefix = NameKind.create(request.prefix, |
| isCamelCase ? Kind.CAMEL_CASE : Kind.CASE_INSENSITIVE_PREFIX); |
| Model model = request.result.getModel(); |
| Set<AliasedName> aliasedNames = ModelUtils.getAliasedNames(model, request.anchor); |
| for (final ConstantElement element : request.index.getConstants(prefix, aliasedNames, Trait.ALIAS)) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return; |
| } |
| completionResult.add(new PHPCompletionItem.ConstantItem((ConstantElement) element, request)); |
| } |
| } |
| |
| /** |
| * @param globalVariables (can be bull) if null then will be looked up in |
| * index |
| */ |
| private Collection<CompletionProposal> getVariableProposals(final CompletionRequest request, Set<VariableElement> globalVariables) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return Collections.emptyList(); |
| } |
| final Map<String, CompletionProposal> proposals = new LinkedHashMap<>(); |
| Model model = request.result.getModel(); |
| VariableScope variableScope = model.getVariableScope(request.anchor); |
| if (variableScope != null) { |
| if (variableScope instanceof NamespaceScope |
| || variableScope instanceof ArrowFunctionScope) { |
| if (globalVariables == null) { |
| FileObject fileObject = request.result.getSnapshot().getSource().getFileObject(); |
| final ElementFilter forCurrentFile = ElementFilter.forFiles(fileObject); |
| globalVariables = forCurrentFile.reverseFilter(request.index.getTopLevelVariables(NameKind.caseInsensitivePrefix(QualifiedName.create(request.prefix)))); |
| } |
| |
| for (final VariableElement globalVariable : globalVariables) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return Collections.emptyList(); |
| } |
| proposals.put(globalVariable.getName(), new PHPCompletionItem.VariableItem(globalVariable, request)); |
| } |
| } |
| |
| List<VariableName> allDeclaredVariables = new ArrayList<>(variableScope.getDeclaredVariables()); |
| // for nested arrow functions |
| if (variableScope instanceof ArrowFunctionScope) { |
| Scope inScope = variableScope.getInScope(); |
| while (inScope instanceof FunctionScope || inScope instanceof NamespaceScope) { |
| allDeclaredVariables.addAll(((VariableScope) inScope).getDeclaredVariables()); |
| if (inScope instanceof FunctionScope |
| && !(inScope instanceof ArrowFunctionScope)) { |
| break; |
| } |
| inScope = inScope.getInScope(); |
| } |
| } |
| |
| Collection<? extends VariableName> declaredVariables = ModelUtils.filter(allDeclaredVariables, nameKind, request.prefix); |
| final int caretOffset = request.anchor + request.prefix.length(); |
| for (VariableName varName : declaredVariables) { |
| if (CancelSupport.getDefault().isCancelled()) { |
| return Collections.emptyList(); |
| } |
| final FileObject realFileObject = varName.getRealFileObject(); |
| if (realFileObject != null || varName.getNameRange().getEnd() < caretOffset) { |
| final String name = varName.getName(); |
| String notDollaredName = name.startsWith("$") ? name.substring(1) : name; |
| if (PredefinedSymbols.SUPERGLOBALS.contains(notDollaredName)) { |
| continue; |
| } |
| if (varName.representsThis()) { |
| continue; |
| } |
| final Collection<? extends String> typeNames = varName.getTypeNames(request.anchor); |
| String typeName = typeNames.size() > 1 ? Type.MIXED : ModelUtils.getFirst(typeNames); |
| final Set<Pair<QualifiedName, Boolean>> qualifiedNames = typeName != null |
| ? Collections.singleton(Pair.of(QualifiedName.create(typeName), false)) |
| : Collections.<Pair<QualifiedName, Boolean>>emptySet(); |
| if (realFileObject != null) { |
| //#183928 - Extend model to allow CTRL + click for 'view/action' variables |
| proposals.put(name, new PHPCompletionItem.VariableItem( |
| VariableElementImpl.create(name, 0, realFileObject, |
| varName.getElementQuery(), TypeResolverImpl.forNames(qualifiedNames), varName.isDeprecated()), request) { |
| @Override |
| public boolean isSmart() { |
| return true; |
| } |
| }); |
| } else { |
| proposals.put(name, new PHPCompletionItem.VariableItem( |
| VariableElementImpl.create(name, 0, request.currentlyEditedFileURL, |
| varName.getElementQuery(), TypeResolverImpl.forNames(qualifiedNames), varName.isDeprecated()), request)); |
| } |
| } |
| } |
| |
| for (final String name : PredefinedSymbols.SUPERGLOBALS) { |
| if (isPrefix("$" + name, request.prefix)) { //NOI18N |
| proposals.put(name, new PHPCompletionItem.SuperGlobalItem(request, name)); |
| } |
| } |
| |
| } |
| return proposals.values(); |
| } |
| |
| private boolean isPrefix(String name, String prefix) { |
| return name != null && (name.startsWith(prefix) |
| || nameKind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX && name.toLowerCase().startsWith(prefix.toLowerCase())); |
| } |
| |
| @Override |
| public Documentation documentElement(ParserResult info, ElementHandle element, Callable<Boolean> cancel) { |
| Documentation result; |
| if (element instanceof ModelElement) { |
| ModelElement mElem = (ModelElement) element; |
| ModelElement parentElem = mElem.getInScope(); |
| FileObject fileObject = mElem.getFileObject(); |
| String fName = fileObject == null ? "?" : fileObject.getNameExt(); //NOI18N |
| String tooltip; |
| if (parentElem instanceof TypeScope) { |
| tooltip = mElem.getPhpElementKind() + ": " + parentElem.getName() + "<b> " + mElem.getName() + " </b>" + "(" + fName + ")"; //NOI18N |
| } else { |
| tooltip = mElem.getPhpElementKind() + ":<b> " + mElem.getName() + " </b>" + "(" + fName + ")"; //NOI18N |
| } |
| result = Documentation.create(String.format("<div align=\"right\"><font size=-1>%s</font></div>", tooltip)); //NOI18N |
| } else { |
| result = ((element instanceof MethodElement) && ((MethodElement) element).isMagic()) ? null : DocRenderer.document(info, element); |
| } |
| return result; |
| } |
| |
| @Override |
| public String document(ParserResult info, ElementHandle element) { |
| return null; |
| } |
| |
| @Override |
| public ElementHandle resolveLink(String link, ElementHandle originalHandle) { |
| return null; |
| } |
| |
| private static boolean isPHPIdentifierPart(char c) { |
| return Character.isJavaIdentifierPart(c) || c == '@'; |
| } |
| |
| @org.netbeans.api.annotations.common.SuppressWarnings(value = "INT_BAD_COMPARISON_WITH_NONNEGATIVE_VALUE", justification = "Not sure about FB analysis correctness") |
| private String getPrefix(ParserResult info, int caretOffset, boolean upToOffset, PrefixBreaker prefixBreaker) { |
| try { |
| BaseDocument doc = (BaseDocument) info.getSnapshot().getSource().getDocument(false); |
| if (doc == null) { |
| return null; |
| } |
| int lineBegin = LineDocumentUtils.getLineStart(doc, caretOffset); |
| if (lineBegin != -1) { |
| int lineEnd = LineDocumentUtils.getLineEnd(doc, caretOffset); |
| String line = doc.getText(lineBegin, lineEnd - lineBegin); |
| int lineOffset = caretOffset - lineBegin; |
| int start = lineOffset; |
| if (lineOffset > 0) { |
| char c = 0; |
| for (int i = lineOffset - 1; i >= 0; i--) { |
| assert i >= 0 && i <= line.length() - 1 : "line:" + line + " | i:" + i + " | line.length():" + line.length() + " | lineBegin:" + lineBegin + " | lineEnd:" + lineEnd + " | caretOffset:" + caretOffset; |
| if (i >= 0 && i <= line.length() - 1) { |
| c = line.charAt(i); |
| if (!isPHPIdentifierPart(c) && c != '\\') { |
| break; |
| } else { |
| start = i; |
| } |
| } |
| } |
| if (start == lineOffset && c == '?' |
| && lineOffset - 2 >= 0 && line.charAt(lineOffset - 2) == '<') { |
| start -= 2; |
| } |
| } |
| |
| // Find identifier end |
| String prefix; |
| if (upToOffset) { |
| prefix = line.substring(start, lineOffset); |
| int lastIndexOfDollar = prefix.lastIndexOf('$'); //NOI18N |
| if (lastIndexOfDollar > 0) { |
| prefix = prefix.substring(lastIndexOfDollar); |
| } |
| } else { |
| if (lineOffset == line.length()) { |
| prefix = line.substring(start); |
| } else { |
| int n = line.length(); |
| int end = lineOffset; |
| for (int j = lineOffset; j < n; j++) { |
| char d = line.charAt(j); |
| // Try to accept Foo::Bar as well |
| if (!isPHPIdentifierPart(d)) { |
| break; |
| } else { |
| end = j + 1; |
| } |
| } |
| prefix = line.substring(start, end); |
| } |
| } |
| |
| if (prefix.length() > 0) { |
| if (prefix.endsWith("::")) { |
| return ""; |
| } |
| |
| if (prefix.endsWith(":") && prefix.length() > 1) { |
| return null; |
| } |
| |
| // Strip out LHS if it's a qualified method, e.g. Benchmark::measure -> measure |
| int q = prefix.lastIndexOf("::"); |
| |
| if (q != -1) { |
| prefix = prefix.substring(q + 2); |
| } |
| |
| // The identifier chars identified by JsLanguage are a bit too permissive; |
| // they include things like "=", "!" and even "&" such that double-clicks will |
| // pick up the whole "token" the user is after. But "=" is only allowed at the |
| // end of identifiers for example. |
| if (prefix.length() == 1) { |
| char c = prefix.charAt(0); |
| if (prefixBreaker.isBreaker(c)) { |
| return null; |
| } |
| } else if (!"<?".equals(prefix)) { //NOI18N |
| for (int i = prefix.length() - 1; i >= 0; i--) { // -2: the last position (-1) can legally be =, ! or ? |
| |
| char c = prefix.charAt(i); |
| if (i == 0 && c == ':') { |
| // : is okay at the begining of prefixes |
| } else if (prefixBreaker.isBreaker(c)) { |
| prefix = prefix.substring(i + 1); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (prefix != null && prefix.startsWith("@")) { //NOI18N |
| final TokenHierarchy<?> tokenHierarchy = info.getSnapshot().getTokenHierarchy(); |
| TokenSequence<PHPTokenId> tokenSequence = tokenHierarchy != null ? LexUtilities.getPHPTokenSequence(tokenHierarchy, caretOffset) : null; |
| if (tokenSequence != null) { |
| tokenSequence.move(caretOffset); |
| if (tokenSequence.moveNext() && tokenSequence.movePrevious()) { |
| Token<PHPTokenId> token = tokenSequence.token(); |
| PHPTokenId id = token.id(); |
| if (id.equals(PHPTokenId.PHP_STRING) || id.equals(PHPTokenId.PHP_TOKEN)) { |
| prefix = prefix.substring(1); |
| } |
| } |
| } |
| } |
| return prefix; |
| } |
| // Else: normal identifier: just return null and let the machinery do the rest |
| } catch (BadLocationException ble) { |
| //Exceptions.printStackTrace(ble); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) { |
| return getPrefix(info, caretOffset, upToOffset, PrefixBreaker.COMMON); |
| } |
| |
| @Override |
| public QueryType getAutoQuery(JTextComponent component, String typedText) { |
| if (typedText.length() == 0) { |
| return QueryType.NONE; |
| } |
| char lastChar = typedText.charAt(typedText.length() - 1); |
| Document document = component.getDocument(); |
| //TokenHierarchy th = TokenHierarchy.get(document); |
| int offset = component.getCaretPosition(); |
| TokenSequence<PHPTokenId> ts = LexUtilities.getPHPTokenSequence(document, offset); |
| if (ts == null) { |
| return QueryType.STOP; |
| } |
| int diff = ts.move(offset); |
| if (diff > 0 && ts.moveNext() || ts.movePrevious()) { |
| Token t = ts.token(); |
| if (t != null) { |
| if (t.id() == PHPTokenId.T_INLINE_HTML) { |
| return QueryType.NONE; |
| } else { |
| if (AUTOPOPUP_STOP_CHARS.contains(Character.valueOf(lastChar))) { |
| return QueryType.STOP; |
| } |
| if (OptionsUtils.autoCompletionTypes()) { |
| if (lastChar == ' ' || lastChar == '\t') { |
| if (ts.movePrevious() |
| && TOKENS_TRIGGERING_AUTOPUP_TYPES_WS.contains(ts.token().id())) { |
| |
| return QueryType.ALL_COMPLETION; |
| } else { |
| return QueryType.STOP; |
| } |
| } |
| |
| if (t.id() == PHPTokenId.PHP_OBJECT_OPERATOR |
| || t.id() == PHPTokenId.PHP_NULLSAFE_OBJECT_OPERATOR |
| || t.id() == PHPTokenId.PHP_PAAMAYIM_NEKUDOTAYIM) { |
| return QueryType.ALL_COMPLETION; |
| } |
| } |
| if (OptionsUtils.autoCompletionVariables()) { |
| if ((t.id() == PHPTokenId.PHP_TOKEN && lastChar == '$') |
| || (t.id() == PHPTokenId.PHP_CONSTANT_ENCAPSED_STRING && lastChar == '$')) { |
| return QueryType.ALL_COMPLETION; |
| } |
| } |
| if (OptionsUtils.autoCompletionNamespaces()) { |
| if (t.id() == PHPTokenId.PHP_NS_SEPARATOR) { |
| return isPhp53OrNewer(document) ? QueryType.ALL_COMPLETION : QueryType.NONE; |
| } |
| } |
| if (t.id() == PHPTokenId.PHPDOC_COMMENT && lastChar == '@') { |
| return QueryType.ALL_COMPLETION; |
| } |
| if (OptionsUtils.autoCompletionFull()) { |
| TokenId id = t.id(); |
| if ((id.equals(PHPTokenId.PHP_STRING) || id.equals(PHPTokenId.PHP_VARIABLE)) && t.length() > 0) { |
| return QueryType.ALL_COMPLETION; |
| } |
| } |
| } |
| } |
| } |
| return QueryType.NONE; |
| } |
| |
| public static boolean isPhp53OrNewer(Document document) { |
| final FileObject fileObject = CodeUtils.getFileObject(document); |
| assert fileObject != null; |
| return CodeUtils.isPhpVersionGreaterThan(fileObject, PhpVersion.PHP_5); |
| } |
| |
| private static boolean isPhp74OrNewer(FileObject fileObject) { |
| if (PHP_VERSION != null) { |
| return PHP_VERSION.compareTo(PhpVersion.PHP_74) >= 0; |
| } |
| assert fileObject != null; |
| return CodeUtils.isPhpVersionGreaterThan(fileObject, PhpVersion.PHP_73); |
| } |
| |
| @Override |
| public String resolveTemplateVariable(String variable, ParserResult info, int caretOffset, String name, Map parameters) { |
| return null; |
| } |
| |
| @Override |
| public Set<String> getApplicableTemplates(Document doc, int selectionBegin, int selectionEnd) { |
| return null; |
| } |
| |
| @Override |
| public ParameterInfo parameters(final ParserResult info, final int caretOffset, CompletionProposal proposal) { |
| final org.netbeans.modules.php.editor.model.Model model = ((PHPParseResult) info).getModel(); |
| ParameterInfoSupport infoSupport = model.getParameterInfoSupport(caretOffset); |
| ParameterInfo parameterInfo = infoSupport.getParameterInfo(); |
| return parameterInfo == null ? ParameterInfo.NONE : parameterInfo; |
| } |
| |
| private boolean startsWith(String theString, String prefix) { |
| if (prefix.length() == 0) { |
| return true; |
| } |
| |
| return caseSensitive ? theString.startsWith(prefix) |
| : theString.toLowerCase().startsWith(prefix.toLowerCase()); |
| } |
| |
| private int findBaseNamespaceEnd(ParserResult info, int caretOffset) { |
| TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy(); |
| assert th != null; |
| TokenSequence<PHPTokenId> tokenSequence = LexUtilities.getPHPTokenSequence(th, caretOffset); |
| assert tokenSequence != null; |
| tokenSequence.move(caretOffset); |
| final boolean moveNextSucces = tokenSequence.moveNext(); |
| if (!moveNextSucces && !tokenSequence.movePrevious()) { |
| assert false; |
| return caretOffset; |
| } |
| boolean hasCurly = false; |
| while (tokenSequence.movePrevious()) { |
| if (!hasCurly) { |
| if (tokenSequence.token().id() == PHPTokenId.PHP_CURLY_OPEN) { |
| hasCurly = true; |
| } |
| } else { |
| // possibly some whitespace before curly open? |
| if (tokenSequence.token().id() != PHPTokenId.WHITESPACE) { |
| tokenSequence.moveNext(); |
| break; |
| } |
| } |
| } |
| if (hasCurly) { |
| return tokenSequence.offset(); |
| } |
| assert false; |
| return caretOffset; |
| } |
| |
| |
| private static class StaticOrInstanceMembersFilter extends ElementFilter { |
| |
| private final boolean forStaticContext; |
| private final boolean forInstanceContext; |
| private final boolean forSelfContext; |
| private final boolean staticAllowed; |
| private final boolean nonstaticAllowed; |
| private final boolean forStaticLateBinding; |
| private final boolean forParentContext; |
| |
| public StaticOrInstanceMembersFilter(final boolean forStaticContext, final boolean forInstanceContext, |
| final boolean forSelfContext, final boolean forStaticLateBinding, final boolean forParentContext) { |
| this.forStaticContext = forStaticContext; |
| this.forInstanceContext = forInstanceContext; |
| this.forSelfContext = forSelfContext; |
| this.forStaticLateBinding = forStaticLateBinding; |
| this.staticAllowed = OptionsUtils.codeCompletionStaticMethods(); |
| this.nonstaticAllowed = OptionsUtils.codeCompletionNonStaticMethods(); |
| this.forParentContext = forParentContext; |
| } |
| |
| @Override |
| public boolean isAccepted(final PhpElement element) { |
| if (forSelfContext && isAcceptedForSelfContext(element)) { |
| return true; |
| } |
| if (forStaticContext && isAcceptedForStaticContext(element)) { |
| return true; |
| } |
| if (forInstanceContext && isAcceptedForNotStaticContext(element)) { |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isAcceptedForNotStaticContext(final PhpElement element) { |
| final boolean isStatic = element.getPhpModifiers().isStatic(); |
| if (forParentContext |
| && !isStatic |
| && element.getPhpElementKind().equals(PhpElementKind.FIELD)) { |
| // parent::fieldName is invalid |
| // this is constant |
| return false; |
| } |
| return !isStatic || (staticAllowed && element.getPhpElementKind().equals(PhpElementKind.METHOD)); |
| } |
| |
| private boolean isAcceptedForStaticContext(final PhpElement element) { |
| final boolean isStatic = element.getPhpModifiers().isStatic(); |
| return isStatic || (nonstaticAllowed && !forStaticLateBinding && element.getPhpElementKind().equals(PhpElementKind.METHOD)); |
| } |
| |
| private boolean isAcceptedForSelfContext(final PhpElement element) { |
| return forSelfContext && nonstaticAllowed && !element.getPhpElementKind().equals(PhpElementKind.FIELD); |
| } |
| } |
| |
| private interface PrefixBreaker { |
| PrefixBreaker COMMON = new PrefixBreaker() { |
| |
| @Override |
| public boolean isBreaker(char c) { |
| return !(isPHPIdentifierPart(c) || c == ':'); |
| } |
| }; |
| |
| PrefixBreaker WITH_NS_PARTS = new PrefixBreaker() { |
| |
| @Override |
| public boolean isBreaker(char c) { |
| return !(isPHPIdentifierPart(c) || c == '\\' || c == ':'); |
| } |
| }; |
| |
| boolean isBreaker(char c); |
| } |
| |
| private static boolean isCamelCaseForTypeNames(final String query) { |
| return false; |
| } |
| |
| private interface EnclosingType { |
| |
| boolean isClassDeclaration(); |
| |
| boolean isTraitDeclaration(); |
| |
| String extractTypeName(); |
| |
| //~ Factories |
| |
| static EnclosingType forTypeDeclaration(final TypeDeclaration typeDeclaration) { |
| return new EnclosingType() { |
| @Override |
| public boolean isClassDeclaration() { |
| return typeDeclaration instanceof ClassDeclaration; |
| } |
| |
| @Override |
| public boolean isTraitDeclaration() { |
| return typeDeclaration instanceof TraitDeclaration; |
| } |
| |
| @Override |
| public String extractTypeName() { |
| return CodeUtils.extractTypeName(typeDeclaration); |
| } |
| }; |
| } |
| |
| static EnclosingType forClassInstanceCreation(final ClassInstanceCreation classInstanceCreation) { |
| assert classInstanceCreation.isAnonymous() : classInstanceCreation; |
| return new EnclosingType() { |
| @Override |
| public boolean isClassDeclaration() { |
| return true; |
| } |
| |
| @Override |
| public boolean isTraitDeclaration() { |
| return false; |
| } |
| |
| @Override |
| public String extractTypeName() { |
| return CodeUtils.extractClassName(classInstanceCreation); |
| } |
| }; |
| } |
| |
| } |
| |
| private interface EnclosingClass { |
| |
| String getClassName(); |
| |
| Expression getSuperClass(); |
| |
| List<Expression> getInterfaces(); |
| |
| String extractClassName(); |
| |
| String extractUnqualifiedSuperClassName(); |
| |
| //~ Factories |
| |
| static EnclosingClass forClassDeclaration(final ClassDeclaration classDeclaration) { |
| return new EnclosingClass() { |
| @Override |
| public String getClassName() { |
| return classDeclaration.getName().getName(); |
| } |
| |
| @Override |
| public Expression getSuperClass() { |
| return classDeclaration.getSuperClass(); |
| } |
| |
| @Override |
| public List<Expression> getInterfaces() { |
| return classDeclaration.getInterfaes(); |
| } |
| |
| @Override |
| public String extractClassName() { |
| return CodeUtils.extractClassName(classDeclaration); |
| } |
| |
| @Override |
| public String extractUnqualifiedSuperClassName() { |
| return CodeUtils.extractUnqualifiedSuperClassName(classDeclaration); |
| } |
| }; |
| } |
| |
| static EnclosingClass forClassInstanceCreation(final ClassInstanceCreation classInstanceCreation) { |
| assert classInstanceCreation.isAnonymous() : classInstanceCreation; |
| return new EnclosingClass() { |
| @Override |
| public String getClassName() { |
| return CodeUtils.extractClassName(classInstanceCreation); |
| } |
| |
| @Override |
| public Expression getSuperClass() { |
| return classInstanceCreation.getSuperClass(); |
| } |
| |
| @Override |
| public List<Expression> getInterfaces() { |
| return classInstanceCreation.getInterfaces(); |
| } |
| |
| @Override |
| public String extractClassName() { |
| return CodeUtils.extractClassName(classInstanceCreation); |
| } |
| |
| @Override |
| public String extractUnqualifiedSuperClassName() { |
| return CodeUtils.extractUnqualifiedSuperClassName(classInstanceCreation); |
| } |
| }; |
| } |
| |
| } |
| |
| } |