/*******************************************************************************
* 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.actions;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.declarations.Argument;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.core.*;
import org.eclipse.dltk.internal.core.AbstractSourceModule;
import org.eclipse.dltk.internal.core.util.MethodOverrideTester;
import org.eclipse.dltk.internal.ui.editor.EditorUtility;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
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.PHPModelUtils;
import org.eclipse.php.internal.ui.Logger;
import org.eclipse.php.internal.ui.PHPUiConstants;
import org.eclipse.php.internal.ui.PHPUiPlugin;
import org.eclipse.php.internal.ui.corext.util.SuperTypeHierarchyCache;
import org.eclipse.php.internal.ui.editor.PHPStructuredEditor;
import org.eclipse.php.ui.CodeGeneration;
import org.eclipse.ui.*;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
public class AddDescriptionAction extends Action implements IObjectActionDelegate {
private static final String PHP_COMMENT_BLOCK_END = " */"; //$NON-NLS-1$
private static final String PHP_COMMENT_BLOCK_MID = " *"; //$NON-NLS-1$
private static final String PHP_COMMENT_BLOCK_START = "/**"; //$NON-NLS-1$
private final String PHP_BLOCK_OPEN_TAG = "<?php"; //$NON-NLS-1$
private final String PHP_BLOCK_CLOSE_TAG = "?>"; //$NON-NLS-1$
private IModelElement[] fModelElement;
private int startPosition = 0;
String docBlock;
String lineDelim;
@Override
public void setActivePart(IAction action, IWorkbenchPart targetPart) {
}
@Override
public void run(IAction action) {
final IModelElement[] modelElement = getModelElement();
if (modelElement == null) {
return;
}
// Sorting the PHP code elements array by "first-line" position.
// this will enable "right" order of iteration
Arrays.sort(modelElement, new modelElementComparatorImplementation());
// iterating the functions that need to add 'PHP Doc' bottoms-up - to
// eliminate mutual interference
for (int i = modelElement.length - 1; i >= 0; i--) {
IModelElement modelElem = modelElement[i];
if (null == modelElem) {
continue; // if we got to null pointer, skipping it
}
IEditorInput input;
try {
input = org.eclipse.php.internal.ui.util.EditorUtility.getEditorInput(modelElem);
} catch (ModelException e) {
Logger.logException(e);
return;
}
IWorkbenchPage page = PHPUiPlugin.getActivePage();
IEditorPart editorPart;
try {
editorPart = IDE.openEditor(page, input, PHPUiConstants.PHP_EDITOR_ID);
} catch (PartInitException e) {
Logger.logException(e);
return;
}
if (editorPart instanceof ITextEditor) {
ITextEditor textEditor = (ITextEditor) editorPart;
IEditorInput editorInput = editorPart.getEditorInput();
IDocument document = textEditor.getDocumentProvider().getDocument(editorInput);
this.lineDelim = TextUtilities.getDefaultLineDelimiter(document);
String docBlockText = handleElement(textEditor, modelElem, document);
if (docBlockText == null) {
return;
}
EditorUtility.revealInEditor(textEditor, startPosition, docBlock.length());
}
}
}
private String handleElement(ITextEditor textEditor, IModelElement modelElem, IDocument document) {
if (textEditor instanceof PHPStructuredEditor) {
PHPStructuredEditor editor = (PHPStructuredEditor) textEditor;
if (editor.getTextViewer() != null && !editor.getTextViewer().isEditable()) {
return null;
}
}
if (modelElem instanceof ISourceModule) {
handleFileDocBlock((ISourceModule) modelElem, (IStructuredDocument) document);
return null;
}
try {
startPosition = getCodeDataOffset(modelElem);
} catch (ModelException e) {
Logger.logException(e);
return null;
}
// Calculating indentation need to be added
String indentString = null;
try {
indentString = getIndentString(document, modelElem);
} catch (BadLocationException e) {
Logger.logException(e);
return null;
}
if (!textEditor.isEditable()) {
return null;
}
int type = modelElem != null ? modelElem.getElementType() : -1;
if (type != IModelElement.METHOD && type != IModelElement.TYPE && type != IModelElement.FIELD) {
assert false;
return null;
}
String comment = null;
try {
switch (type) {
case IModelElement.TYPE:
if (PHPModelUtils.getDocBlock((IType) modelElem) != null) {
return null;
}
comment = createTypeComment((IType) modelElem, lineDelim);
break;
case IModelElement.FIELD:
if (!isParameter((IField) modelElem)) {
if (PHPModelUtils.getDocBlock((IField) modelElem) != null) {
return null;
}
comment = createFieldComment((IField) modelElem, lineDelim);
break;
} else if (modelElem != null) {
// If the element is a parameter in the function
// declaration,
// get the parent element and go to method case.
modelElem = modelElem.getParent();
// reset the position to the beginning of the method
startPosition = getCodeDataOffset(modelElem);
try {
// reset the indent level to the method.
indentString = getIndentString(document, modelElem);
} catch (BadLocationException e) {
Logger.logException(e);
return null;
}
}
case IModelElement.METHOD:
if (PHPModelUtils.getDocBlock((IMethod) modelElem) != null) {
return null;
}
comment = createMethodComment((IMethod) modelElem, lineDelim);
break;
default:
comment = createDefaultComment(lineDelim);
}
} catch (CoreException e) {
Logger.logException(e);
}
if (comment == null) {
comment = createDefaultComment(lineDelim);
}
docBlock = indentPattern(comment, indentString, lineDelim);
String docBlockText = insertDocBlock((IStructuredDocument) document, startPosition, docBlock);
return docBlockText;
}
private boolean isParameter(IField field) {
ISourceModule sourceModule = field.getSourceModule();
ModuleDeclaration moduleDeclaration = SourceParserUtil.getModuleDeclaration(sourceModule);
ASTNode fieldDeclaration = null;
try {
fieldDeclaration = PHPModelUtils.getNodeByField(moduleDeclaration, field);
} catch (ModelException e) {
}
return fieldDeclaration instanceof Argument;
}
private String indentPattern(String originalPattern, String indentation, String lineDelim) {
String delimPlusIndent = lineDelim + indentation;
String indentedPattern = originalPattern.replaceAll(Pattern.quote(lineDelim), delimPlusIndent)
+ delimPlusIndent;
return indentedPattern;
}
private String createDefaultComment(String lineDelimiter) {
return PHP_COMMENT_BLOCK_START + lineDelimiter + PHP_COMMENT_BLOCK_MID + lineDelimiter + PHP_COMMENT_BLOCK_END;
}
private String createTypeComment(IType type, String lineDelimiter) throws CoreException {
return CodeGeneration.getTypeComment(type.getScriptProject(), type.getTypeQualifiedName(),
/* typeParameterNames */null, lineDelimiter);
}
private String createMethodComment(IMethod meth, String lineDelimiter) throws CoreException {
IType declaringType = meth.getDeclaringType();
IMethod overridden = null;
if (!meth.isConstructor() && null != declaringType) {
ITypeHierarchy hierarchy = SuperTypeHierarchyCache.getTypeHierarchy(declaringType);
MethodOverrideTester tester = new MethodOverrideTester(declaringType, hierarchy);
overridden = tester.findOverriddenMethod(meth, true);
}
return CodeGeneration.getMethodComment(meth, overridden, lineDelimiter);
}
private String createFieldComment(IField field, String lineDelimiter) throws ModelException, CoreException {
return CodeGeneration.getFieldComment(field.getScriptProject(), field, lineDelimiter);
}
/**
* Calculates and returns the desired docBlock surrounded by '<?php' and
* '?>' tags (no indentation)
*
* @param document
* - The IStructuredDocument that we are working on
*
* @return String to be used as leading indentation
* @throws CoreException
*/
public String createPHPScopeFileDocBlock(IScriptProject scriptProject) {
String fileComment = null;
try {
fileComment = CodeGeneration.getFileComment(scriptProject, lineDelim);
} catch (CoreException e) {
Logger.logException(e);
}
if (fileComment == null) {
fileComment = createDefaultComment(lineDelim);
}
return PHP_BLOCK_OPEN_TAG + lineDelim + fileComment + PHP_BLOCK_CLOSE_TAG + lineDelim;
}
/**
* Calculates the leading string to be used as indentation prefix
*
* @param document
* The IStructuredDocument that we are working on
* @param modelElem
* A PHPFileData that need to be documented
*
* @return String to be used as leading indentation
*/
private String getIndentString(IDocument document, IModelElement modelElem) throws BadLocationException {
int elementOffset = 0;
String leadingString = null;
try {
elementOffset = getCodeDataOffset(modelElem);
int lineStartOffset = document.getLineInformationOfOffset(elementOffset).getOffset();
leadingString = document.get(lineStartOffset, elementOffset - lineStartOffset);
} catch (ModelException e) {
Logger.logException(e);
return null;
}
// replacing all non-spaces/tabs to single-space, in order to get
// "char-clean" prefix
leadingString = leadingString.replaceAll("[^\\p{javaWhitespace}]", " "); //$NON-NLS-1$ //$NON-NLS-2$
return leadingString;
}
private int getCodeDataOffset(IModelElement modelElem) throws ModelException {
if (modelElem instanceof ISourceModule) {
ISourceReference primaryModelElem = (ISourceReference) (((ISourceModule) modelElem).getPrimaryElement());// .getPHPBlocks();
return primaryModelElem != null
? primaryModelElem.getSourceRange().getOffset() + primaryModelElem.getSourceRange().getLength()
/*
* getPHPStartTag ( ). getEndPosition ()
*/ : -1;
}
if (modelElem instanceof ISourceReference) {
int dataOffset = ((ISourceReference) modelElem).getSourceRange().getOffset();
return dataOffset;
}
assert false;
return -1;
}
@Override
public void selectionChanged(IAction action, ISelection selection) {
if (selection == null || !(selection instanceof IStructuredSelection)) {
return;
}
IStructuredSelection structuredSelection = (IStructuredSelection) selection;
setModelElement(new IModelElement[structuredSelection.size()]);
Iterator<?> i = structuredSelection.iterator();
int idx = 0;
final IModelElement[] modelElement = getModelElement();
while (i.hasNext()) {
modelElement[idx++] = (IModelElement) i.next();
}
}
/**
* Handle a situation where a file DocBlock is requested and there is an
* undocumented class, function or define at the beginning of the document.
* In this case we auto-document the undocumented element to comply the
* DocBlock rules.
*
* @param data
* A PHPFileData that need to be documented
* @param document
* The IStructuredDocument that we are working on
*/
private void handleFileDocBlock(ISourceModule data, IStructuredDocument document) {
// Find the first PHP script region:
IStructuredDocumentRegion sdRegion = document.getFirstStructuredDocumentRegion();
IPHPScriptRegion phpScriptRegion = null;
ITextRegion textRegion = null;
String docBlock = null;
while (sdRegion != null && docBlock == null) {
ITextRegion region = sdRegion.getFirstRegion();
if (region.getType() == PHPRegionContext.PHP_OPEN) {
// File's content starts with '<?PHP' tag
region = sdRegion.getRegionAtCharacterOffset(region.getEnd() + sdRegion.getStartOffset());
if (region != null && region.getType() == PHPRegionContext.PHP_CONTENT) {
phpScriptRegion = (IPHPScriptRegion) region;
try {
docBlock = CodeGeneration.getFileComment(data, null);
} catch (CoreException e) {
Logger.logException("Generating default phpdoc comment", e); //$NON-NLS-1$
}
if (docBlock == null) {
// XXX : should we insert newlines?
docBlock = createDefaultComment("");
}
break;
}
} else if (region.getType() == DOMRegionContext.XML_DECLARATION_OPEN) {
// File's content starts with HTML code
region = sdRegion.getRegionAtCharacterOffset(region.getEnd() + sdRegion.getStartOffset());
if (region != null) {
phpScriptRegion = null;
textRegion = (ITextRegion) region;
docBlock = createPHPScopeFileDocBlock(data.getScriptProject());
break;
}
}
sdRegion = sdRegion.getNext();
}
if (phpScriptRegion != null || textRegion != null) {
try {
int offset = 0;
if (phpScriptRegion == null && textRegion != null) {
// File's content starts with HTML code
offset = 0;
} else if (phpScriptRegion != null && sdRegion != null) {
// File's content starts with '<?php' tag
textRegion = phpScriptRegion.getPHPToken(0);
String lineDelimiter = document.getLineDelimiter(document.getLineOfOffset(textRegion.getStart()));
if (lineDelimiter == null) {
// XXX : should we add a newline before inserting
// docBlock?
lineDelimiter = ""; //$NON-NLS-1$
}
int lineDelimiterLength = lineDelimiter.length();
offset = textRegion.getStart() + sdRegion.getStartOffset() + phpScriptRegion.getStart()
+ lineDelimiterLength;
} else {
assert false;// we shouldn't get here ...
}
if (data instanceof AbstractSourceModule)
insertDocBlock(document, offset, docBlock);
} catch (BadLocationException e) {
}
}
}
private String insertDocBlock(IDocument document, int offset, String docBlock) {
try {
document.replace(offset, 0, docBlock);
} catch (BadLocationException e) {
Logger.logException(e);
docBlock = null;
}
return docBlock;
}
private final class modelElementComparatorImplementation implements Comparator<IModelElement> {
@Override
public int compare(IModelElement object1, IModelElement object2) {
/*
* handling null-pointers on both levels (object=null or
* object1.getUserData()=null) 'null' objects will be considered as
* 'bigger' and will be pushed to the end of the array
*/
if (object1 instanceof ISourceReference && object2 instanceof ISourceReference) {
ISourceReference sourceReference1 = (ISourceReference) object1;
ISourceReference sourceReference2 = (ISourceReference) object2;
try {
if (sourceReference1.getSourceRange() == null) {
if (sourceReference2.getSourceRange() == null) {
return 0; // both null => equal
} else {
return 1; // only object1 is null => object1 is
// bigger
}
}
if (sourceReference2.getSourceRange() == null) {
return -1; // only object2 is null => object2 is bigger
}
return sourceReference1.getSourceRange().getOffset()
- sourceReference2.getSourceRange().getOffset();
} catch (ModelException e) {
Logger.logException(e);
}
}
assert false; // we never supposed to get here
return 0;
}
}
/**
* @param fmodelElement
* the fmodelElement to set
*/
public void setModelElement(IModelElement[] fmodelElement) {
this.fModelElement = fmodelElement;
}
/**
* @return the fmodelElement
*/
public IModelElement[] getModelElement() {
return fModelElement;
}
}