/******************************************************************************* * Copyright (c) 2009 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Zend Technologies *******************************************************************************/ package org.eclipse.php.internal.ui.editor.contentassist; import java.util.*; import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IScopeContext; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.dltk.ast.declarations.ModuleDeclaration; import org.eclipse.dltk.core.*; import org.eclipse.dltk.internal.core.ModelElement; import org.eclipse.dltk.internal.core.SourceType; import org.eclipse.dltk.ui.text.completion.ScriptCompletionProposal; import org.eclipse.jface.text.*; import org.eclipse.jface.text.IRegion; import org.eclipse.php.core.PHPVersion; import org.eclipse.php.core.ast.nodes.*; import org.eclipse.php.core.compiler.PHPFlags; import org.eclipse.php.core.compiler.ast.nodes.NamespaceReference; import org.eclipse.php.core.compiler.ast.nodes.UsePart; import org.eclipse.php.core.project.ProjectOptions; import org.eclipse.php.internal.core.PHPCoreConstants; import org.eclipse.php.internal.core.PHPCorePlugin; import org.eclipse.php.internal.core.codeassist.AliasField; import org.eclipse.php.internal.core.codeassist.AliasMethod; import org.eclipse.php.internal.core.codeassist.AliasType; import org.eclipse.php.internal.core.codeassist.ProposalExtraInfo; import org.eclipse.php.internal.core.compiler.ast.parser.ASTUtils; import org.eclipse.php.internal.core.documentModel.parser.PHPRegionContext; import org.eclipse.php.internal.core.documentModel.parser.regions.IPHPScriptRegion; import org.eclipse.php.internal.core.typeinference.FakeConstructor; import org.eclipse.php.internal.core.typeinference.PHPModelUtils; import org.eclipse.php.internal.core.util.text.PHPTextSequenceUtilities; import org.eclipse.php.internal.core.util.text.TextSequence; import org.eclipse.php.internal.ui.Logger; import org.eclipse.php.internal.ui.PHPUiPlugin; import org.eclipse.php.internal.ui.editor.PHPStructuredEditor; import org.eclipse.php.internal.ui.editor.PHPStructuredTextViewer; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.wst.sse.core.internal.provisional.text.*; /** * This class injects USE statement if needed for the given completion proposal * * @author michael */ public class UseStatementInjector { private ScriptCompletionProposal proposal; public UseStatementInjector(ScriptCompletionProposal proposal) { this.proposal = proposal; } /** * Inserts USE statement into beginning of the document, or after the last * USE statement. * * @param document * @param textViewer * @param offset * @return new offset */ public int inject(IDocument document, ITextViewer textViewer, int offset) { IModelElement modelElement = proposal.getModelElement(); if (modelElement instanceof FakeConstructor) { FakeConstructor fc = (FakeConstructor) modelElement; if (fc.getParent() instanceof AliasType) { return offset; } } else if (modelElement instanceof AliasType || modelElement instanceof AliasMethod || modelElement instanceof AliasField) { return offset; } try { if (modelElement.getElementType() == IModelElement.METHOD && (((IMethod) modelElement).isConstructor())) { modelElement = modelElement.getAncestor(IModelElement.TYPE); } } catch (ModelException e) { Logger.logException(e); } if (modelElement == null) return offset; if (proposal instanceof IPHPCompletionProposalExtension) { IPHPCompletionProposalExtension phpCompletionProposal = (IPHPCompletionProposalExtension) proposal; if (ProposalExtraInfo.isNotInsertUse(phpCompletionProposal.getExtraInfo())) { return offset; } } try { // qualified namespace should return offset directly if (proposal.getReplacementOffset() > 0 && document .getChar(proposal.getReplacementOffset() - 1) == NamespaceReference.NAMESPACE_SEPARATOR) { return offset; } if (modelElement.getElementType() == IModelElement.TYPE && PHPFlags.isNamespace(((IType) modelElement).getFlags())) { if (offset > 0) { String prefix = document.get(proposal.getReplacementOffset(), proposal.getReplacementLength()); String fullName = ((IType) modelElement).getElementName(); if (fullName.startsWith(prefix) && prefix.indexOf(NamespaceReference.NAMESPACE_SEPARATOR) < 0) { // int ITextEditor textEditor = ((PHPStructuredTextViewer) textViewer).getTextEditor(); if (textEditor instanceof PHPStructuredEditor) { IModelElement editorElement = ((PHPStructuredEditor) textEditor).getModelElement(); if (editorElement != null) { ISourceModule sourceModule = ((ModelElement) editorElement).getSourceModule(); String namespaceName = fullName; int nsSeparatorIndex = fullName.indexOf(NamespaceReference.NAMESPACE_SEPARATOR); if (nsSeparatorIndex > 0) { namespaceName = fullName.substring(0, nsSeparatorIndex); } String usePartName = namespaceName; boolean useAlias = !Platform.getPreferencesService().getBoolean(PHPCorePlugin.ID, PHPCoreConstants.CODEASSIST_INSERT_FULL_QUALIFIED_NAME_FOR_NAMESPACE, true, null); ModuleDeclaration moduleDeclaration = SourceParserUtil .getModuleDeclaration(sourceModule); ASTParser parser = ASTParser.newParser(sourceModule); parser.setSource(document.get().toCharArray()); Program program = parser.createAST(null); // Don't insert USE statement for current // namespace. // "program != null" is a workaround for bug // 465687. if (program != null && isSameNamespace(namespaceName, program, sourceModule, offset)) { return offset; } // find existing use statement: UsePart usePart = ASTUtils.findUseStatementByNamespace(moduleDeclaration, usePartName, offset); List<String> importedTypeName = getImportedTypeName(moduleDeclaration, offset); String typeName = namespaceName; // if the class/namespace has not been imported // add use statement if (program != null && !importedTypeName.contains(typeName)) { program.recordModifications(); AST ast = program.getAST(); NamespaceName newNamespaceName = ast .newNamespaceName(createIdentifiers(ast, usePartName), false, false); UseStatementPart newUseStatementPart = ast.newUseStatementPart(newNamespaceName, null); UseStatement newUseStatement = ast.newUseStatement( Arrays.asList(new UseStatementPart[] { newUseStatementPart }), UseStatement.T_NONE); NamespaceDeclaration currentNamespace = getCurrentNamespace(program, sourceModule, offset - 1); List<Statement> statements = program.statements(); if (currentNamespace != null) { // insert in the beginning of the // current namespace: statements = currentNamespace.getBody().statements(); } int index = getLastUsestatementIndex(statements, offset); addUseStatement(index, newUseStatement, statements, document); ast.setInsertUseStatement(true); TextEdit edits = program.rewrite(document, createOptions(modelElement)); // workaround for bug 400976: // when current offset is in a php section // that only contains AstErrors, the use // statements will be added at the beginning // of the previous php section. // If luckily there is a previous php // section in the document, we're good, // otherwise we have to create one now at // the beginning of the document. // Text edits that will be correctly // inserted in an existing php section have // all their offset > 0, so looking for text // edits having all their offset = 0 (and // their length = 0) should be enough to // detect use statements that will be // wrongly inserted outside any existing php // section... if (new Region(0, 0).equals(edits.getRegion())) { String lineDelim = TextUtilities.getDefaultLineDelimiter(document); MultiTextEdit newEdits = new MultiTextEdit(); newEdits.addChild(new InsertEdit(0, "<?php")); newEdits.addChild(new InsertEdit(0, lineDelim)); for (TextEdit edit : edits.getChildren()) { // we have to copy the text edit to // reset its parent newEdits.addChild(edit.copy()); } newEdits.addChild(new InsertEdit(0, "?>")); newEdits.addChild(new InsertEdit(0, lineDelim)); edits = newEdits; } else if (index == 0 && edits.getChildrenSize() == 2) { // workaround for bug 435134 addBlankLineEdit(edits, document); } edits.apply(document); ast.setInsertUseStatement(false); int replacementOffset = proposal.getReplacementOffset() + edits.getLength(); offset += edits.getLength(); proposal.setReplacementOffset(replacementOffset); } else if (!useAlias && (usePart == null || !usePartName.equals(usePart.getNamespace().getFullyQualifiedName()))) { // if the type name already exists, use // fully // qualified name to replace proposal.setReplacementString(NamespaceReference.NAMESPACE_SEPARATOR + fullName); } } } } return offset; } return offset; } else // class members should return offset directly if (modelElement.getElementType() != IModelElement.TYPE && !(modelElement instanceof FakeConstructor)) { IModelElement type = modelElement.getAncestor(IModelElement.TYPE); if (type != null && !PHPFlags.isNamespace(((IType) type).getFlags())) { return offset; } } } catch (Exception e) { PHPUiPlugin.log(e); } return addUseStatement(modelElement, document, textViewer, offset); } private int addUseStatement(IModelElement modelElement, IDocument document, ITextViewer textViewer, int offset) { // add use statement if needed: IType namespace = PHPModelUtils.getCurrentNamespace(modelElement); if (namespace == null) { return offset; } // find source module of the current editor: if (!(textViewer instanceof PHPStructuredTextViewer)) { return offset; } ITextEditor textEditor = ((PHPStructuredTextViewer) textViewer).getTextEditor(); if (!(textEditor instanceof PHPStructuredEditor)) { return offset; } IModelElement editorElement = ((PHPStructuredEditor) textEditor).getModelElement(); if (editorElement == null) { return offset; } ISourceModule sourceModule = ((ModelElement) editorElement).getSourceModule(); try { String namespaceName = namespace.getElementName(); String usePartName = namespaceName; boolean useAlias = !Platform.getPreferencesService().getBoolean(PHPCorePlugin.ID, PHPCoreConstants.CODEASSIST_INSERT_FULL_QUALIFIED_NAME_FOR_NAMESPACE, true, null); if (!useAlias) { usePartName = usePartName + NamespaceReference.NAMESPACE_SEPARATOR + modelElement.getElementName(); } ModuleDeclaration moduleDeclaration = SourceParserUtil.getModuleDeclaration(sourceModule); ASTParser parser = ASTParser.newParser(sourceModule); parser.setSource(document.get().toCharArray()); Program program = parser.createAST(null); // Don't insert USE statement for current namespace. // "program != null" is a workaround for bug 465687. if (program != null && isSameNamespace(namespaceName, program, sourceModule, offset)) { return offset; } // find existing use statement: UsePart usePart = ASTUtils.findUseStatementByNamespace(moduleDeclaration, usePartName, offset); List<String> importedTypeName = getImportedTypeName(moduleDeclaration, offset); String typeName = ""; //$NON-NLS-1$ if (!useAlias) { typeName = modelElement.getElementName().toLowerCase(); } else { if (usePart != null && usePart.getAlias() != null && usePart.getAlias().getName() != null) { typeName = usePart.getAlias().getName(); } else { String elementName = PHPModelUtils.extractElementName(namespaceName); if (elementName != null) { typeName = elementName.toLowerCase(); } } } PHPVersion phpVersion = ProjectOptions.getPHPVersion(modelElement); // if the class/namespace has not been imported // add use statement if (program != null && !importedTypeName.contains(typeName) && canInsertUseStatement(getUseStatementType(modelElement), phpVersion)) { program.recordModifications(); AST ast = program.getAST(); NamespaceName newNamespaceName = ast.newNamespaceName(createIdentifiers(ast, usePartName), false, false); UseStatementPart newUseStatementPart = ast.newUseStatementPart(newNamespaceName, null); int type = getUseStatementType(modelElement); UseStatement newUseStatement = ast .newUseStatement(Arrays.asList(new UseStatementPart[] { newUseStatementPart }), type); NamespaceDeclaration currentNamespace = getCurrentNamespace(program, sourceModule, offset - 1); List<Statement> statements = program.statements(); if (currentNamespace != null) { // insert in the beginning of the current namespace: statements = currentNamespace.getBody().statements(); } int index = getLastUsestatementIndex(statements, offset); addUseStatement(index, newUseStatement, statements, document); ast.setInsertUseStatement(true); TextEdit edits = program.rewrite(document, createOptions(modelElement)); // workaround for bug 400976: // when current offset is in a php section that only contains // AstErrors, the use statements will be added at the beginning // of the previous php section. // If luckily there is a previous php section in the document, // we're good, otherwise we have to create one now at the // beginning of the document. // Text edits that will be correctly inserted in an existing // php section have all their offset > 0, so looking for text // edits having all their offset = 0 (and their length = 0) // should be enough to detect use statements that will be // wrongly inserted outside any existing php section... if (new Region(0, 0).equals(edits.getRegion())) { String lineDelim = TextUtilities.getDefaultLineDelimiter(document); // String lineDelim = // TextUtilities.getDefaultLineDelimiter(document); MultiTextEdit newEdits = new MultiTextEdit(); newEdits.addChild(new InsertEdit(0, "<?php")); newEdits.addChild(new InsertEdit(0, lineDelim)); for (TextEdit edit : edits.getChildren()) { // we have to copy the text edit to reset its parent newEdits.addChild(edit.copy()); } newEdits.addChild(new InsertEdit(0, "?>")); newEdits.addChild(new InsertEdit(0, lineDelim)); edits = newEdits; } else if (index == 0 && edits.getChildrenSize() == 2) { // workaround for bug 435134 addBlankLineEdit(edits, document); } edits.apply(document); ast.setInsertUseStatement(false); if (useAlias && needsAliasPrepend(modelElement)) { // update replacement string: add namespace // alias prefix String namespacePrefix = typeName + NamespaceReference.NAMESPACE_SEPARATOR; String replacementString = proposal.getReplacementString(); String existingNamespacePrefix = readNamespacePrefix(sourceModule, document, offset, phpVersion); // Add alias to the replacement string: if (existingNamespacePrefix == null || !usePartName.toLowerCase().equals(existingNamespacePrefix.toLowerCase())) { replacementString = namespacePrefix + replacementString; } proposal.setReplacementString(replacementString); } int replacementOffset = proposal.getReplacementOffset() + edits.getLength(); offset += edits.getLength(); proposal.setReplacementOffset(replacementOffset); } else if (!useAlias && (usePart == null || !usePartName.equals(usePart.getNamespace().getFullyQualifiedName()))) { String namespacePrefix = NamespaceReference.NAMESPACE_SEPARATOR + namespaceName + NamespaceReference.NAMESPACE_SEPARATOR; String replacementString = proposal.getReplacementString(); String existingNamespacePrefix = readNamespacePrefix(sourceModule, document, offset, phpVersion); // https://bugs.eclipse.org/bugs/show_bug.cgi?id=459306 // if the type name already exists, use fully // qualified name to replace: if (existingNamespacePrefix == null || !namespaceName.toLowerCase().equals(existingNamespacePrefix.toLowerCase())) { replacementString = namespacePrefix + replacementString; } proposal.setReplacementString(replacementString); } } catch (Exception e) { PHPUiPlugin.log(e); } return offset; } private Collection<Identifier> createIdentifiers(AST ast, String namespaceName) { String[] split = namespaceName.split("\\\\"); //$NON-NLS-1$ List<Identifier> identifiers = new ArrayList<>(split.length); for (String s : split) { identifiers.add(ast.newIdentifier(s)); } return identifiers; } private NamespaceDeclaration getCurrentNamespace(Program program, ISourceModule sourceModule, int offset) { SourceType ns = (SourceType) PHPModelUtils.getPossibleCurrentNamespace(sourceModule, offset); if (ns == null) { if (program.statements() != null && !program.statements().isEmpty() && (program.statements().get(0) instanceof NamespaceDeclaration)) { NamespaceDeclaration result = (NamespaceDeclaration) program.statements().get(0); for (Statement statement : program.statements()) { if (statement.getStart() >= offset) { return result; } if (statement instanceof NamespaceDeclaration) { result = (NamespaceDeclaration) statement; } } return result; } else { return null; } } ASTNode node = null; try { node = program.getElementAt(ns.getSourceRange().getOffset()); } catch (ModelException e) { } if (node == null) { return null; } do { switch (node.getType()) { case ASTNode.NAMESPACE: return (NamespaceDeclaration) node; } node = node.getParent(); } while (node != null); return null; } private String getNamespaceName(NamespaceDeclaration namespaceDecl) { StringBuilder nameBuf = new StringBuilder(); NamespaceName name = namespaceDecl.getName(); if (name == null) { return "\\"; //$NON-NLS-1$ } for (Identifier identifier : name.segments()) { if (nameBuf.length() > 0) { nameBuf.append('\\'); } nameBuf.append(identifier.getName()); } return nameBuf.toString(); } private boolean needsAliasPrepend(IModelElement modelElement) throws ModelException { if (modelElement instanceof IMethod) { if (modelElement instanceof FakeConstructor) { return true; } IType declaringType = ((IMethod) modelElement).getDeclaringType(); return declaringType == null || PHPFlags.isNamespace(declaringType.getFlags()); } if (modelElement instanceof IField) { IField field = (IField) modelElement; if (!PHPFlags.isConstant(field.getFlags())) { return false; } IType declaringType = ((IField) modelElement).getDeclaringType(); return declaringType == null || PHPFlags.isNamespace(declaringType.getFlags()); } return true; } private String readNamespacePrefix(ISourceModule sourceModule, IDocument document, int offset, PHPVersion phpVersion) { if (offset > 0) { --offset; } IStructuredDocumentRegion sRegion = ((IStructuredDocument) document).getRegionAtCharacterOffset(offset); if (sRegion != null) { ITextRegion tRegion = sRegion.getRegionAtCharacterOffset(offset); ITextRegionCollection container = sRegion; if (tRegion instanceof ITextRegionContainer) { container = (ITextRegionContainer) tRegion; tRegion = container.getRegionAtCharacterOffset(offset); } if (tRegion != null && tRegion.getType() == PHPRegionContext.PHP_CONTENT) { IPHPScriptRegion phpScriptRegion = (IPHPScriptRegion) tRegion; try { tRegion = phpScriptRegion .getPHPToken(offset - container.getStartOffset() - phpScriptRegion.getStart()); } catch (BadLocationException e) { return null; } // Determine element name: int elementStart = container.getStartOffset() + phpScriptRegion.getStart() + tRegion.getStart(); TextSequence statement = PHPTextSequenceUtilities.getStatement(elementStart + tRegion.getLength(), sRegion, true); if (statement.length() != 0) { int endPosition = PHPTextSequenceUtilities.readBackwardSpaces(statement, statement.length()); int startPosition = PHPTextSequenceUtilities.readIdentifierStartIndex(phpVersion, statement, endPosition, true); String elementName = startPosition < 0 ? "" //$NON-NLS-1$ : statement.subSequence(startPosition, endPosition).toString(); if (elementName.length() > 0) { return PHPModelUtils.extractNamespaceName(elementName, sourceModule, offset); } } } } return null; } private int getLastUsestatementIndex(List<Statement> statements, int offset) { int result = 0; for (int i = 0; i < statements.size(); i++) { Statement statement = statements.get(i); if (statement.getEnd() <= offset && statement instanceof UseStatement) { result = i + 1; } } return result; } /** * Get all the class / namespace names which have been imported by use * statement * * @param moduleDeclaration * @param offset * @return */ private List<String> getImportedTypeName(ModuleDeclaration moduleDeclaration, final int offset) { org.eclipse.php.core.compiler.ast.nodes.UseStatement[] useStatements = ASTUtils .getUseStatements(moduleDeclaration, offset); List<String> importedClass = new ArrayList<>(); for (org.eclipse.php.core.compiler.ast.nodes.UseStatement statement : useStatements) { for (UsePart usePart : statement.getParts()) { String name; if (usePart.getAlias() != null) { name = usePart.getAlias().getName(); } else { // In case there's no alias - the alias is the // last segment of the namespace name: name = usePart.getNamespace().getName(); } importedClass.add(name.toLowerCase()); } } return importedClass; } public int getUseStatementType(IModelElement modelElement) throws ModelException { if (modelElement.getElementType() != IModelElement.TYPE && !(modelElement instanceof FakeConstructor)) { if (modelElement.getElementType() == IModelElement.METHOD) { return UseStatement.T_FUNCTION; } else if (modelElement.getElementType() == IModelElement.FIELD && PHPFlags.isConstant(((IMember) modelElement).getFlags())) { return UseStatement.T_CONST; } } return UseStatement.T_NONE; } private void addUseStatement(int index, UseStatement newUseStatement, List<Statement> statements, IDocument document) { if (index > 0) { // workaround for bug 393253 try { int beginLine = document.getLineOfOffset(statements.get(index - 1).getEnd()) + 1; newUseStatement.setSourceRange(document.getLineOffset(beginLine), 0); } catch (Exception e) { } } statements.add(index, newUseStatement); } private void addBlankLineEdit(TextEdit edits, IDocument document) throws BadLocationException { String lineDelim = TextUtilities.getDefaultLineDelimiter(document); int changeOffset = edits.getChildren()[0].getOffset(); IRegion region = document.getLineInformationOfOffset(changeOffset); String space = document.get(region.getOffset(), changeOffset - region.getOffset()); edits.addChild(new InsertEdit(changeOffset, lineDelim + space)); } private boolean isSameNamespace(String namespaceName, Program program, ISourceModule sourceModule, int offset) { NamespaceDeclaration currentNamespace = getCurrentNamespace(program, sourceModule, offset - 1); if (currentNamespace == null) { return false; } if (namespaceName.equals(getNamespaceName(currentNamespace))) { return true; } return false; } private boolean canInsertUseStatement(int statementType, PHPVersion phpVersion) { return statementType == UseStatement.T_NONE || phpVersion.isGreaterThan(PHPVersion.PHP5_5); } private Map<Object, Object> createOptions(IModelElement modelElement) { Map<Object, Object> options = new HashMap<>(PHPCorePlugin.getOptions()); if (modelElement == null || modelElement.getScriptProject() == null) { return options; } IScopeContext[] contents = new IScopeContext[] { new ProjectScope(modelElement.getScriptProject().getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE }; for (int i = 0; i < contents.length; i++) { IScopeContext scopeContext = contents[i]; IEclipsePreferences node = scopeContext.getNode(PHPCorePlugin.ID); if (node != null) { if (!options.containsKey(PHPCoreConstants.FORMATTER_USE_TABS)) { String useTabs = node.get(PHPCoreConstants.FORMATTER_USE_TABS, null); if (useTabs != null) { options.put(PHPCoreConstants.FORMATTER_USE_TABS, useTabs); } } if (!options.containsKey(PHPCoreConstants.FORMATTER_INDENTATION_SIZE)) { String size = node.get(PHPCoreConstants.FORMATTER_INDENTATION_SIZE, null); if (size != null) { options.put(PHPCoreConstants.FORMATTER_INDENTATION_SIZE, size); } } } } return options; } }