/** * Aptana Studio * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions). * Please see the license.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.php.internal.contentAssist; import java.io.IOException; import java.io.StringReader; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationExtension; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org2.eclipse.php.core.compiler.PHPFlags; import org2.eclipse.php.internal.core.ast.nodes.ClassDeclaration; import org2.eclipse.php.internal.core.documentModel.parser.regions.PHPRegionTypes; import com.aptana.core.logging.IdeLog; import com.aptana.editor.common.contentassist.ILexemeProvider; import com.aptana.editor.php.PHPEditorPlugin; import com.aptana.editor.php.indexer.EntryUtils; import com.aptana.editor.php.indexer.IElementEntry; import com.aptana.editor.php.indexer.IPHPIndexConstants; import com.aptana.editor.php.internal.indexer.ClassPHPEntryValue; import com.aptana.editor.php.internal.indexer.FunctionPHPEntryValue; import com.aptana.editor.php.internal.indexer.NamespacePHPEntryValue; import com.aptana.editor.php.internal.parser.nodes.IPHPParseNode; import com.aptana.editor.php.internal.parser.nodes.PHPClassParseNode; import com.aptana.editor.php.internal.parser.nodes.PHPFunctionParseNode; import com.aptana.editor.php.internal.parser.nodes.PHPNamespaceNode; import com.aptana.editor.php.internal.parser.nodes.Parameter; import com.aptana.editor.php.internal.text.link.contentassist.LineBreakingReader; import com.aptana.parsing.ast.IParseNode; import com.aptana.parsing.lexer.Lexeme; public class PHPContextCalculator { /** * "Extends" proposal context type. */ protected static final String EXTENDS_PROPOSAL_CONTEXT_TYPE = "EXTENDS_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * "Implements" proposal context type. */ protected static final String IMPLEMENTS_PROPOSAL_CONTEXT_TYPE = "IMPLEMENTS_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * "new" proposal context type. */ protected static final String NEW_PROPOSAL_CONTEXT_TYPE = "NEW_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * Namespace proposal context type. */ protected static final String NAMESPACE_PROPOSAL_CONTEXT_TYPE = "NAMESPACE_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * Trait "use" proposal context type. This context will appear when the content assist is on a trait 'use' call, * _before_ any 'use' block. * * @see #TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE */ protected static final String TRAIT_USE_PROPOSAL_CONTEXT_TYPE = "TRAIT_USE_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * Trait "use" block proposal context type. This context will appear when the content assist is on a trait 'use' * block. * * @see #TRAIT_USE_PROPOSAL_CONTEXT_TYPE */ protected static final String TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE = "TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ /** * Variable-type proposal context type. */ protected static final String VARIABLE_TYPE_PROPOSAL_CONTEXT_TYPE = "VARIABLE_TYPE_PROPOSAL_CONTEXT_TYPE"; //$NON-NLS-1$ private static final String NEW_LINE = System.getProperty("line.separator", "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$ // $codepro.audit.disable platformSpecificLineSeparator /** * DEFAULT_DELIMITER */ public static final String DEFAULT_DELIMITER = NEW_LINE + "\u2022\t"; //$NON-NLS-1$ private static final int[] EMPTY_TYPES = new int[0]; private ProposalContext currentContext; private Character insertedChar; /** * Calculate and return the {@link ProposalContext} for the given offset. * * @param lexemeProvider * @param offset * @param reportedScopeIsUnderClass * Indicate that the reported scope is under a class type. That hint can help the context calculation * when dealing with ambiguous statement that can be resolved to several contexts, such as "use". * @return */ public ProposalContext calculateCompletionContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, boolean reportedScopeIsUnderClass) { internalCalculateContext(lexemeProvider, offset, reportedScopeIsUnderClass); return currentContext; } /** * Calculate and return the {@link ProposalContext} for the given offset.<br> * The calculation here is also accepting a character that is currently being inserted to the document, but not yet * exist in the LexemeProvider. This functionality is useful when computing the * {@link PHPContentAssistProcessor#isValidAutoActivationLocation(char, int, org.eclipse.jface.text.IDocument, int)} * using the context, since at this stage the LexemeProvider does not know about the inserted char. * * @param lexemeProvider * @param offset * @param insertedChar * A char that is currently being inserted into the given offset, but is not yet reflected in the * LexemeProvider. * @param reportedScopeIsUnderClass * Indicate that the reported scope is under a class type. That hint can help the context calculation * when dealing with ambiguous statement that can be resolved to several contexts, such as "use". * @return A Proposal Context. */ public ProposalContext calculateCompletionContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, char insertedChar, boolean reportedScopeIsUnderClass) { // auto-box it this.insertedChar = insertedChar; ProposalContext context = calculateCompletionContext(lexemeProvider, offset, reportedScopeIsUnderClass); // reset the char after the computation this.insertedChar = null; return context; } /* * Do the actual calculation */ private void internalCalculateContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, boolean reportedScopeIsUnderClass) { currentContext = new ProposalContext(new AcceptAllContextFilter(), true, true, null); int lexemePosition = lexemeProvider.getLexemeFloorIndex(offset - 1); // checking line-comment context if (checkLineCommentContext(lexemeProvider, offset, lexemePosition)) { return; } // checking PHPDoc context if (checkPHPDocContext(lexemeProvider, offset, lexemePosition)) { return; } // checking class declaration context if (checkClassDeclarationContext(lexemeProvider, offset, lexemePosition)) { return; } // check implements declaration context if (checkImplementsDeclarationContext(lexemeProvider, offset, lexemePosition)) { return; } // check extends declaration context if (checkExtendsDeclarationContext(lexemeProvider, offset, lexemePosition)) { return; } // checking class "extends" or "implements" declaration context if (checkClassExtendsOrImplementsContext(lexemeProvider, offset, lexemePosition)) { return; } // checking function declaration context if (checkFunctionDeclarationContext(lexemeProvider, offset, lexemePosition)) { return; } // checking new instance context if (checkNewInstanceContext(lexemeProvider, offset, lexemePosition)) { return; } if (reportedScopeIsUnderClass) { // checking for trait 'use' statement context (e.g. use A, B) if (checkTraitUseContext(lexemeProvider, offset, lexemePosition)) { return; } } // checking for trait 'use' block context (e.g. use A, B { B::foo insteadof boo; }) // Order of calls is important here! if (checkTraitUseBlockContext(lexemeProvider, offset, lexemePosition)) { // refine the context filter for an offset that is set right after an 'insteadof' or an 'as' keywords. // in case we are before a keyword context, check if we are in a context that should only offer a // keyword (e.g. 'insteadof' or 'as'). if (checkTraitUseAfterKeywordContext(lexemeProvider, offset, lexemePosition) || checkTraitUseBeforeKeywordContext(lexemeProvider, offset, lexemePosition)) { return; } } // checking for namespace Use statement if (checkNamespaceUseContext(lexemeProvider, offset, lexemePosition)) { return; } } /** * Checks class declaration context and sets it if needed. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position (of offset). * @return true if context is recognized and set, false otherwise */ private boolean checkClassDeclarationContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_CLASS, new String[] { PHPRegionTypes.PHP_NS_SEPARATOR }); if (nearestKeyWord == null) { nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_INTERFACE, new String[] { PHPRegionTypes.PHP_NS_SEPARATOR }); if (nearestKeyWord == null) { return false; } } currentContext = getDenyAllProposalContext(); return true; } /** * Checks class "extends" or "implements" context and sets it if needed. In case this method is returning true, only * "extends" or "implements" keyword are displayed. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position (of offset). * @return true if context is recognized and set, false otherwise */ private boolean checkClassExtendsOrImplementsContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } Lexeme<PHPTokenType> nearestClassKeyWord = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_CLASS, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN, PHPRegionTypes.PHP_EXTENDS }); // WAS SKIPPING: , new int[] {PHPTokenTypes.IDENTIFIER}); if (nearestClassKeyWord == null) { nearestClassKeyWord = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_INTERFACE, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); // WAS SKIPPING: , new int[] {PHPTokenTypes.IDENTIFIER}); if (nearestClassKeyWord == null) { return false; } } // whether class or interface is being declared final boolean declaredClass = PHPRegionTypes.PHP_CLASS.equals(nearestClassKeyWord.getType().getType()); // Check if this declarations already have an "extends" or an "implements" keyword Lexeme<PHPTokenType> extendsToken = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_EXTENDS, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN, PHPRegionTypes.PHP_IMPLEMENTS }); Lexeme<PHPTokenType> implementsToken = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_IMPLEMENTS, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); final boolean alreadyHaveExtends = (extendsToken != null); final boolean alreadyHaveImplements = (implementsToken != null); IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { if (builtinElement instanceof IPHPParseNode) { if (!alreadyHaveExtends && "extends".equals(((IPHPParseNode) builtinElement).getNodeName()) //$NON-NLS-1$ || (!alreadyHaveImplements && declaredClass && "implements".equals(((IPHPParseNode) builtinElement).getNodeName()))) //$NON-NLS-1$ { return true; } } return false; } public boolean acceptElementEntry(IElementEntry element) { return false; } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, false, EMPTY_TYPES); currentContext.setAutoActivateCAAfterApply(true); return true; } /** * Checks implements declaration context and sets it if needed. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position (of offset). * @return true if context is recognized and set, false otherwise */ private boolean checkImplementsDeclarationContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } if (checkImplementsDeclarationContextInternal(lexemeProvider, lexemePosition)) { return true; } else { return checkImplementsDeclarationContextInternal(lexemeProvider, lexemePosition - 1); } } private boolean checkImplementsDeclarationContextInternal(ILexemeProvider<PHPTokenType> lexemeProvider, int lexemePosition) { // searching for "implements" keyword Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_IMPLEMENTS, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); // WAS SKIPPING: , new int[] {PHPTokenTypes.IDENTIFIER, PHPTokenTypes.COMMA }); if (nearestKeyWord == null) { return false; } // the implements keyword found and now we have to check whether a // class is being declared Lexeme<PHPTokenType> classKeyword = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_CLASS, new String[] { PHPRegionTypes.PHP_EXTENDS, PHPRegionTypes.PHP_IMPLEMENTS, PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); // WAS ALSO SKIPPING (in both cases above): PHPTokenTypes.IDENTIFIER, PHPTokenTypes.COMMA // wrong declaration, so no proposals at all if (classKeyword == null) { currentContext = getDenyAllProposalContext(); return true; } // We need to verify that in case we are not right after an 'implement' keyword, we have a comma before we can // allow another interface to be inserted. Lexeme<PHPTokenType> previousNameOrToken = findLexemeBackward(lexemeProvider, lexemePosition, new String[] { PHPRegionTypes.PHP_IMPLEMENTS, PHPRegionTypes.PHP_TOKEN }, new String[] { PHPRegionTypes.WHITESPACE, }); if (previousNameOrToken == null || (previousNameOrToken.getType().getType() == PHPRegionTypes.PHP_TOKEN && !isLexemeText( previousNameOrToken, ","))) //$NON-NLS-1$ { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { if (builtinElement instanceof PHPClassParseNode) { PHPClassParseNode node = (PHPClassParseNode) builtinElement; if (PHPFlags.isInterface(node.getModifiers())) { return true; } } return false; } public boolean acceptElementEntry(IElementEntry element) { if (element.getValue() instanceof ClassPHPEntryValue) { if (PHPFlags.isInterface(((ClassPHPEntryValue) element.getValue()).getModifiers())) { return true; } } return false; } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, EMPTY_TYPES); currentContext.setType(IMPLEMENTS_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks extends declaration context and sets it if needed. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position (of offset). * @return true if context is recognized and set, false otherwise */ private boolean checkExtendsDeclarationContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } if (checkExtendsDeclarationContextInternal(lexemeProvider, lexemePosition)) { return true; } else { return checkExtendsDeclarationContextInternal(lexemeProvider, lexemePosition - 1); } } private boolean checkExtendsDeclarationContextInternal(ILexemeProvider<PHPTokenType> lexemeProvider, int lexemePosition) { Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_EXTENDS, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); // WAS SKIPPING: new int[] {PHPTokenTypes.COMMA, PHPTokenTypes.IDENTIFIER }); if (nearestKeyWord == null) { return false; } // extends keyword found and now we have to check whether interface or // class is being declared. // Also, we need to make sure we do not allow multiple inheritance when a class extension is involved. Lexeme<PHPTokenType> classKeyword = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_CLASS, new String[] { PHPRegionTypes.PHP_EXTENDS, PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); Lexeme<PHPTokenType> interfaceKeyword = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_INTERFACE, new String[] { PHPRegionTypes.PHP_EXTENDS, PHPRegionTypes.PHP_STRING, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_TOKEN }); // WAS ALSO SKIPPING (in both cases): PHPTokenTypes.IDENTIFIER, PHPTokenTypes.COMMA // wrong declaration, so no proposals at all if (classKeyword == null && interfaceKeyword == null) { currentContext = getDenyAllProposalContext(); return true; } final boolean classDeclared = classKeyword != null; // Check that we don't have any strings or commas that indicate a syntax error or a multiple inheritance if (classDeclared) { Lexeme<PHPTokenType> extendsOnly = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_EXTENDS, new String[] { PHPRegionTypes.WHITESPACE }); if (extendsOnly == null) { // We have something else between the completion location and the extends keyword, so we block it. return false; } } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { if (builtinElement instanceof PHPClassParseNode) { PHPClassParseNode node = (PHPClassParseNode) builtinElement; if (ClassDeclaration.MODIFIER_FINAL == node.getModifiers()) { return false; } boolean isInterface = PHPFlags.isInterface(node.getModifiers()); if (classDeclared && !isInterface || !classDeclared && isInterface) { return true; } } return false; } public boolean acceptElementEntry(IElementEntry element) { if (element.getValue() instanceof ClassPHPEntryValue) { ClassPHPEntryValue value = (ClassPHPEntryValue) element.getValue(); if (ClassDeclaration.MODIFIER_FINAL == value.getModifiers()) { return false; } boolean isInterface = PHPFlags.isInterface(value.getModifiers()); if (classDeclared && !isInterface || !classDeclared && isInterface) { return true; } } return false; } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, EMPTY_TYPES); currentContext.setType(EXTENDS_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks function declaration context and sets it if needed. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position (of offset). * @return true if context is recognized and set, false otherwise */ private boolean checkFunctionDeclarationContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } Lexeme<PHPTokenType> functionKeyWord = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_FUNCTION, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.PHP_TOKEN, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_VARIABLE }); if (functionKeyWord == null) { return false; } // Check for any $ sign Lexeme<PHPTokenType> lexeme = lexemeProvider.getLexeme(lexemePosition); if (insertedChar != null && insertedChar.charValue() == '$' || isLexemeText(lexeme, PHPContentAssistProcessor.DOLLAR_SIGN)) { // Deny any proposals currentContext = getDenyAllProposalContext(); return true; } // In any case we are inside the function declaration parentheses, we would like to show code assist for // variable-types. // Before that, we need to verify that the entered char, or the existing one in the offset, is a comma or an // open-parenthesis. boolean contextOK = false; if ((insertedChar != null && (insertedChar.charValue() == ',' || insertedChar.charValue() == '(')) || isLexemeText(lexeme, ",") || isLexemeText(lexeme, "(")) //$NON-NLS-1$ //$NON-NLS-2$ { contextOK = true; } else { // We need to specifically look for a comma or an open parenthesis, skipping only whitespace tokens. Lexeme<PHPTokenType> currentLexeme = null; int positionShift = (insertedChar != null) ? 0 : -1; for (int i = lexemePosition + positionShift; i >= 0; i--) { currentLexeme = lexemeProvider.getLexeme(i); String type = currentLexeme.getType().getType(); if (type.equals(PHPRegionTypes.PHP_TOKEN)) { if (isLexemeText(currentLexeme, ",") || isLexemeText(currentLexeme, "(")) { //$NON-NLS-1$ //$NON-NLS-2$ contextOK = true; } break; } else if (!type.equals(PHPRegionTypes.WHITESPACE)) { break; } } } // We are not in a good offset to offer any assistance if (!contextOK) { // Deny any proposals currentContext = getDenyAllProposalContext(); return true; } // Accept classes and interfaces IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return builtinElement instanceof PHPClassParseNode; } public boolean acceptElementEntry(IElementEntry element) { return element.getValue() instanceof ClassPHPEntryValue; } // deny any external snippets public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, EMPTY_TYPES); currentContext.setType(VARIABLE_TYPE_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks new instance context. * * @param lexemeProvider * - lexeme provider. * @param offset * - offset. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkNewInstanceContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { if (lexemePosition == 0) { return false; } if (checkNewInstanceContextInternal(lexemeProvider, lexemePosition)) { return true; } else { return checkNewInstanceContextInternal(lexemeProvider, lexemePosition - 1); } } /** * Checks new instance context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkNewInstanceContextInternal(ILexemeProvider<PHPTokenType> lexemeProvider, int lexemePosition) { // searching for "new" keyword Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_NEW, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_NS_SEPARATOR, PHPRegionTypes.PHP_STRING }); // WAS SKIPPING: { PHPTokenTypes.IDENTIFIER, PHPTokenTypes.BACKSLASH }); if (nearestKeyWord == null) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { if (builtinElement instanceof PHPClassParseNode) { PHPClassParseNode node = (PHPClassParseNode) builtinElement; if (!PHPFlags.isInterface(node.getModifiers())) { return true; } } return false; } public boolean acceptElementEntry(IElementEntry element) { if (element.getValue() instanceof ClassPHPEntryValue) { if (!PHPFlags.isInterface(((ClassPHPEntryValue) element.getValue()).getModifiers())) { return true; } } return element.getValue() instanceof NamespacePHPEntryValue; } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, EMPTY_TYPES); currentContext.setType(NEW_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks for line-comment context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkLineCommentContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { Lexeme<PHPTokenType> firstLexeme = lexemeProvider.getFirstLexeme(); if (firstLexeme != null) { String lexemeType = firstLexeme.getType().getType(); if (lexemeType.equals(PHPRegionTypes.PHP_COMMENT_START) || lexemeType.equals(PHPRegionTypes.PHP_LINE_COMMENT)) { currentContext = getDenyAllProposalContext(); return true; } } return false; } /** * Checks for PHPDoc context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkPHPDocContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { Lexeme<PHPTokenType> firstLexeme = lexemeProvider.getFirstLexeme(); if (firstLexeme != null && firstLexeme.getType().getType().equals(PHPRegionTypes.PHPDOC_COMMENT_START)) { currentContext = getDenyAllProposalContext(); return true; } return false; } /** * Checks for namespace 'use' context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkNamespaceUseContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { Lexeme<PHPTokenType> nearestUseKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_USE, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_NS_SEPARATOR, PHPRegionTypes.PHP_STRING }); if (nearestUseKeyWord == null) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return builtinElement instanceof PHPNamespaceNode; } public boolean acceptElementEntry(IElementEntry element) { Object value = element.getValue(); return value instanceof NamespacePHPEntryValue || value instanceof ClassPHPEntryValue; } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, new int[] { IPHPIndexConstants.NAMESPACE_CATEGORY }); currentContext.setType(NAMESPACE_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks for trait 'use' context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkTraitUseContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { Lexeme<PHPTokenType> nearestUseKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_USE, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_NS_SEPARATOR, PHPRegionTypes.PHP_STRING, PHPRegionTypes.PHP_TOKEN }); if (nearestUseKeyWord == null) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return builtinElement instanceof PHPNamespaceNode; } public boolean acceptElementEntry(IElementEntry element) { return EntryUtils.isTrait(element); } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, new int[] { IPHPIndexConstants.CLASS_CATEGORY }); currentContext.setType(TRAIT_USE_PROPOSAL_CONTEXT_TYPE); return true; } /** * Checks for trait 'use' block context. * * @param lexemeProvider * - lexeme provider. * @param lexemePosition * - lexeme position. * @return true if context is recognized and set, false otherwise */ private boolean checkTraitUseBlockContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { Lexeme<PHPTokenType> nearestUseKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_USE, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_NS_SEPARATOR, PHPRegionTypes.PHP_STRING, PHPRegionTypes.PHP_PAAMAYIM_NEKUDOTAYIM, PHPRegionTypes.PHP_INSTEADOF, PHPRegionTypes.PHP_AS, PHPRegionTypes.PHP_SEMICOLON, PHPRegionTypes.PHP_CURLY_OPEN, PHPRegionTypes.PHP_TOKEN }); if (nearestUseKeyWord == null) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return false; } public boolean acceptElementEntry(IElementEntry element) { return EntryUtils.isTrait(element) || (EntryUtils.isFunction(element) && ((FunctionPHPEntryValue) element.getValue()) .isTraitMethod()); } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, false, true, new int[] { IPHPIndexConstants.CLASS_CATEGORY }); currentContext.setType(TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE); return true; } private boolean checkTraitUseBeforeKeywordContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { // Since this method is always called after the checkTraitUseBlockContext call, we already know there is a "use" // keyword. Now we just look back till we hit the PAAMAYIM_NEKUDOTAYIM, as we can be inside one of the internal // statement of the use block. Lexeme<PHPTokenType> lexeme = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_STRING, new String[] { PHPRegionTypes.WHITESPACE }); if (lexeme == null) { return false; } lexeme = findLexemeBackward(lexemeProvider, lexemePosition - 1, PHPRegionTypes.PHP_PAAMAYIM_NEKUDOTAYIM, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_STRING }); if (lexeme == null || !lexeme.getType().getType().equals(PHPRegionTypes.PHP_PAAMAYIM_NEKUDOTAYIM)) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return ("insteadof".equals(((IPHPParseNode) builtinElement).getNodeName()) //$NON-NLS-1$ || "as".equals(((IPHPParseNode) builtinElement).getNodeName())); //$NON-NLS-1$ } public boolean acceptElementEntry(IElementEntry element) { // Accept only trait methods when we assist before the "insteadof" or "as". return EntryUtils.isFunction(element) && ((FunctionPHPEntryValue) element.getValue()).isTraitMethod(); } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, null); currentContext.setType(TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE); return true; } private boolean checkTraitUseAfterKeywordContext(ILexemeProvider<PHPTokenType> lexemeProvider, int offset, int lexemePosition) { // Since this method is always called after the checkTraitUseBlockContext call, we already know there is a "use" // keyword. Now we just look back till we hit the "insteadof" or the "as" keywords. Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_INSTEADOF, new String[] { PHPRegionTypes.WHITESPACE }); if (nearestKeyWord == null) { nearestKeyWord = findLexemeBackward(lexemeProvider, lexemePosition, PHPRegionTypes.PHP_AS, new String[] { PHPRegionTypes.WHITESPACE }); } if (nearestKeyWord == null) { return false; } IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return false; } public boolean acceptElementEntry(IElementEntry element) { return EntryUtils.isTrait(element) || (EntryUtils.isFunction(element) && ((FunctionPHPEntryValue) element.getValue()) .isTraitMethod()); } public boolean acceptExternalProposals() { return false; } }; currentContext = new ProposalContext(filter, true, true, new int[] { IPHPIndexConstants.CLASS_CATEGORY }); currentContext.setType(TRAIT_USE_BLOCK_PROPOSAL_CONTEXT_TYPE); return true; } /** * Finds lexeme going backwards. * * @param lexemeProvider * - lexeme provider. * @param startPosition * - start position. * @param typesToFind * - type of lexeme to find. Can be a String or an array of String types. If an array is passes, this * method will stop and return a Lexeme on the first match. * @param allowedTypesToSkip * - types allowed to skip. empty array means no types can be skipped, null means any types can be * skipped. * @return found lexeme or null if not found */ public static Lexeme<PHPTokenType> findLexemeBackward(ILexemeProvider<PHPTokenType> lexemeProvider, int startPosition, Object typesToFind, String[] allowedTypesToSkip) { Set<String> typesSet = new HashSet<String>(); if (typesToFind instanceof String) { typesSet.add(typesToFind.toString()); } else { String[] types = (String[]) typesToFind; for (String type : types) { typesSet.add(type); } } for (int i = startPosition; i >= 0; i--) { Lexeme<PHPTokenType> currentLexeme = lexemeProvider.getLexeme(i); if (typesSet.contains(currentLexeme.getType().getType())) { return currentLexeme; } else { if (allowedTypesToSkip != null) { boolean allowedToSkip = false; for (int j = 0; j < allowedTypesToSkip.length; j++) { if (currentLexeme.getType().getType().equals(allowedTypesToSkip[j])) { allowedToSkip = true; break; } } if (!allowedToSkip) { return null; } } } } return null; } /** * Gets "Deny all" proposal context. * * @return */ private static ProposalContext getDenyAllProposalContext() { IContextFilter filter = new IContextFilter() { public boolean acceptBuiltin(Object builtinElement) { return false; } public boolean acceptElementEntry(IElementEntry element) { return false; } public boolean acceptExternalProposals() { return false; } }; return new ProposalContext(filter, false, false, EMPTY_TYPES); } /** * Returns a CallInfo representing the function name or the class name that comes before the given offset. * * @param lexemeProvider * @param offset * @return A {@link CallInfo}, or null if none is located. */ static CallInfo calculateCallInfo(ILexemeProvider<PHPTokenType> lexemeProvider, int offset) { int startPosition = lexemeProvider.getLexemeFloorIndex(offset - 1); int level = 0; // Check if we are in a function declaration. If we find one, return null to indicate that we should not display // any context information. if (startPosition > 0) { Lexeme<PHPTokenType> nearestKeyWord = findLexemeBackward(lexemeProvider, startPosition - 1, PHPRegionTypes.PHP_FUNCTION, new String[] { PHPRegionTypes.PHP_STRING, PHPRegionTypes.PHP_TOKEN, PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_VARIABLE }); if (nearestKeyWord != null) { return null; } } for (int i = startPosition; i >= 0; i--) { Lexeme<PHPTokenType> currentLexeme = lexemeProvider.getLexeme(i); String type = currentLexeme.getType().getType(); if (PHPRegionTypes.PHP_TOKEN.equals(type)) { if (isLexemeText(currentLexeme, ")")) //$NON-NLS-1$ { level++; } else if (isLexemeText(currentLexeme, "(")) //$NON-NLS-1$ { if (level == 0) { Lexeme<PHPTokenType> function = findLexemeBackward(lexemeProvider, i - 1, PHPRegionTypes.PHP_STRING, new String[] { PHPRegionTypes.WHITESPACE, PHPRegionTypes.PHP_COMMENT, PHPRegionTypes.PHP_COMMENT_END, PHPRegionTypes.PHP_COMMENT_START }); if (function == null) { return null; } return new CallInfo(function.getText(), function.getEndingOffset()); } level--; } } // i--; } return null; } /** * Computes context information about the PHP function parse node. * * @param pn * - parse node. * @param nameEndOffset * @return context info. */ static IContextInformation computeArgContextInformation(PHPFunctionParseNode pn, int nameEndOffset) { StringBuffer bf = new StringBuffer(); Parameter[] parameters = pn.getParameters(); String[] parameterNames = new String[parameters.length]; for (int a = 0; a < parameters.length; a++) { parameterNames[a] = parameters[a].getVariableName(); } for (int a = 0; a < parameters.length; a++) { parameters[a].addLabel(bf); if (a != parameters.length - 1) { bf.append(", "); //$NON-NLS-1$ } } String info = bf.toString(); ContextInformation contextInformation = new ContextInformation(info, info); return new PHPContextInformationWrapper(contextInformation, nameEndOffset); } /** * Computes context information about the PHP function element entry. * * @param entry * - element entry. * @return context info. */ static IContextInformation computeArgContextInformation(IElementEntry entry, int nameEndOffset) { FunctionPHPEntryValue value = (FunctionPHPEntryValue) entry.getValue(); StringBuffer bf = new StringBuffer(); // bf.append(ElementsIndexingUtils.getLastNameInPath(entry.getEntryPath())); Map<String, Set<Object>> parameters = value.getParameters(); int i = 0; for (String parName : parameters.keySet()) { bf.append('$'); bf.append(parName); if (i != parameters.size() - 1) { bf.append(", "); //$NON-NLS-1$ } i++; } String info = bf.toString(); ContextInformation contextInformation = new ContextInformation(info, info); return new PHPContextInformationWrapper(contextInformation, nameEndOffset); } /** * Computes the context information for the built-in PHP class constructors. * * @param cn * @param nameEndOffset * @return Context information. */ static IContextInformation computeConstructorContextInformation(PHPClassParseNode cn, int nameEndOffset) { IParseNode[] children = cn.getChildren(); for (IParseNode child : children) { if (child instanceof PHPFunctionParseNode) { PHPFunctionParseNode func = (PHPFunctionParseNode) child; // Return the first hit for a constructor. // This should be modified once we'll have multiple return // options (like in JDT) if ("__construct".equals(func.getNodeName()) //$NON-NLS-1$ || cn.getNodeName().equals(func.getNodeName())) { Parameter[] parameters = func.getParameters(); if (parameters != null && parameters.length > 0) { return computeArgContextInformation(func, nameEndOffset); } } } } return null; } static String wrapString(String s, int maxWidth, String delimiter) { if (Display.getCurrent() == null) { return s; } StringReader sr = new StringReader(s); GC gc = new GC(Display.getCurrent()); StringBuilder result = new StringBuilder(); LineBreakingReader r = new LineBreakingReader(sr, gc, maxWidth); try { String line = r.readLine(); while (line != null) { result.append(line); line = r.readLine(); if (line != null) { result.append(delimiter); } } } catch (IOException e) { // Will probably never happen since we are reading from a StringReader IdeLog.logWarning(PHPEditorPlugin.getDefault(), "Error reading from a stream", e, null); //$NON-NLS-1$ } finally { sr.close(); } gc.dispose(); return result.toString(); } /** * Returns true if the given lexeme contains the given text. */ static boolean isLexemeText(Lexeme<PHPTokenType> lexeme, String content) { return lexeme != null && content.equals(lexeme.getText()); } /** * A context information that implements {@link IContextInformationExtension} to provide accurate location for the * function name end position. This is needed for an accurate computation of the argument position at the * PHPContextInformationValidator (getCharCount) * * @author Shalom */ static class PHPContextInformationWrapper implements IContextInformation, IContextInformationExtension { private ContextInformation contextInformation; private final int contextPosition; protected PHPContextInformationWrapper(ContextInformation information, int contextPosition) { this.contextInformation = information; this.contextPosition = contextPosition; } public String getContextDisplayString() { return contextInformation.getContextDisplayString(); } public Image getImage() { return contextInformation.getImage(); } public String getInformationDisplayString() { return contextInformation.getContextDisplayString(); } public int getContextInformationPosition() { return contextPosition; } } }