/*******************************************************************************
* Copyright (c) 2000, 2010 QNX Software Systems 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:
* QNX Software Systems - Initial API and implementation
* Nokia - Refactored from DebugTextHover to remove CDI dependency
* Wind River Systems - Bug 200418
*******************************************************************************/
package org.eclipse.cdt.debug.ui.editors;
import java.util.EmptyStackException;
import java.util.Stack;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.IASTArraySubscriptExpression;
import org.eclipse.cdt.core.dom.ast.IASTBinaryExpression;
import org.eclipse.cdt.core.dom.ast.IASTComment;
import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFieldReference;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
import org.eclipse.cdt.core.dom.ast.IASTImageLocation;
import org.eclipse.cdt.core.dom.ast.IASTMacroExpansionLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTNodeLocation;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorElifStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorElseStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorEndifStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfdefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfndefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorMacroExpansion;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IASTUnaryExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTDeleteExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTNewExpression;
import org.eclipse.cdt.core.dom.ast.gnu.IGNUASTCompoundStatementExpression;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.debug.internal.ui.CDebugUIMessages;
import org.eclipse.cdt.debug.internal.ui.CDebugUIUtils;
import org.eclipse.cdt.debug.ui.CDebugUIPlugin;
import org.eclipse.cdt.ui.CDTUITools;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.cdt.ui.text.SharedASTJob;
import org.eclipse.cdt.ui.text.c.hover.ICEditorTextHover;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextHoverExtension;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.ui.IEditorPart;
/**
* The text hovering support for C/C++ debugger.
*
* @since 7.0
*/
public abstract class AbstractDebugTextHover implements ICEditorTextHover, ITextHoverExtension {
/**
* ASTVisitor checking for side-effect expressions.
*/
private static class ExpressionChecker extends ASTVisitor {
private boolean fValid;
private ExpressionChecker() {
shouldVisitExpressions = true;
}
private boolean check(IASTNode node) {
fValid = true;
node.accept(this);
return fValid;
}
@Override
public int visit(IASTExpression expression) {
if (expression instanceof IASTFunctionCallExpression) {
fValid = false;
} else if (expression instanceof IASTUnaryExpression) {
IASTUnaryExpression unaryExpression = (IASTUnaryExpression) expression;
switch (unaryExpression.getOperator()) {
case IASTUnaryExpression.op_postFixDecr:
case IASTUnaryExpression.op_postFixIncr:
case IASTUnaryExpression.op_prefixIncr:
case IASTUnaryExpression.op_prefixDecr:
fValid = false;
break;
}
} else if (expression instanceof IASTBinaryExpression) {
IASTBinaryExpression binaryExpression = (IASTBinaryExpression) expression;
switch (binaryExpression.getOperator()) {
case IASTBinaryExpression.op_assign:
case IASTBinaryExpression.op_binaryAndAssign:
case IASTBinaryExpression.op_binaryOrAssign:
case IASTBinaryExpression.op_binaryXorAssign:
case IASTBinaryExpression.op_divideAssign:
case IASTBinaryExpression.op_minusAssign:
case IASTBinaryExpression.op_moduloAssign:
case IASTBinaryExpression.op_multiplyAssign:
case IASTBinaryExpression.op_plusAssign:
case IASTBinaryExpression.op_shiftLeftAssign:
case IASTBinaryExpression.op_shiftRightAssign:
fValid = false;
break;
}
} else if (expression instanceof ICPPASTNewExpression) {
fValid = false;
} else if (expression instanceof ICPPASTDeleteExpression) {
fValid = false;
} else if (expression instanceof IGNUASTCompoundStatementExpression) {
fValid = false;
}
if (!fValid)
return PROCESS_ABORT;
return PROCESS_CONTINUE;
}
}
static final private int MAX_HOVER_INFO_SIZE = 100;
private IEditorPart fEditor;
/**
* @return <code>true</code> if this hover can evaluate an expression
*/
protected abstract boolean canEvaluate();
/**
* Compute a value for given expression.
*
* @param expression
* @return a result string or <code>null</code> if the expression could not be evaluated
*/
protected abstract String evaluateExpression(String expression);
public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
if (canEvaluate()) {
String expression = getExpressionText(textViewer, hoverRegion);
if (expression != null) {
String result = evaluateExpression(expression);
if (result == null)
return null;
try {
StringBuilder buffer = new StringBuilder();
appendVariable(buffer, makeHTMLSafe(expression), makeHTMLSafe(result.trim()));
if (buffer.length() > 0) {
return buffer.toString();
}
} catch (DebugException x) {
CDebugUIPlugin.log(x);
}
}
}
return null;
}
public IRegion getHoverRegion(ITextViewer viewer, int offset) {
if (viewer != null)
return CDebugUIUtils.findWord(viewer.getDocument(), offset);
return null;
}
public final void setEditor(IEditorPart editor) {
if (editor != null) {
fEditor = editor;
}
}
public IInformationControlCreator getHoverControlCreator() {
return null;
}
/**
* Compute the expression text to be evaluated by the debugger.
* <p>
* The default implementation uses an AST to compute a valid, side-effect free
* expression.
* </p>
*
* @param textViewer the underlying text viewer
* @param hoverRegion the hover region as returned by {@link #getHoverRegion(ITextViewer, int)}
* @return an expression string or <code>null</code> if no valid expression could be computed
*/
protected String getExpressionText(ITextViewer textViewer, final IRegion hoverRegion) {
IDocument document = textViewer.getDocument();
if (document == null)
return null;
String expression = getExpressionTextFromAST(document, hoverRegion);
if (expression == null) {
// fallback in case no parsing information is available
try {
return document.get(hoverRegion.getOffset(), hoverRegion.getLength());
} catch (BadLocationException e) {
// ignored
}
} else if (expression.length() == 0) {
// positively invalid expression
return null;
}
return expression;
}
/**
* Compute a valid expression from AST if available.
*
* @param document
* @param hoverRegion
* @return a valid expression string, an empty string to indicate an invalid
* expression or <code>null</code> if the expression could not be
* validated
*/
private String getExpressionTextFromAST(IDocument document, final IRegion hoverRegion) {
ICElement cElement = CDTUITools.getEditorInputCElement(getEditor().getEditorInput());
if (!(cElement instanceof ITranslationUnit)) {
return null;
}
final Position expressionPosition = new Position(0);
SharedASTJob job = new SharedASTJob(CDebugUIMessages.getString("AbstractDebugTextHover.jobName"), (ITranslationUnit) cElement) { //$NON-NLS-1$
@Override
public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) throws CoreException {
if (ast == null) {
return Status.CANCEL_STATUS;
}
int offset = hoverRegion.getOffset();
int length = hoverRegion.getLength();
IASTName name= ast.getNodeSelector(null).findEnclosingName(offset, length);
if (name != null) {
IASTImageLocation imageLoc = name.getImageLocation();
int kind = imageLoc.getLocationKind();
switch (kind) {
case IASTImageLocation.ARGUMENT_TO_MACRO_EXPANSION:
computeMacroArgumentExtent(name, expressionPosition);
break;
default:
if (name.getParent() instanceof IASTPreprocessorMacroExpansion) {
// special case: macro expansion as expression
IASTNode node = ast.getNodeSelector(null).findEnclosingNodeInExpansion(imageLoc.getNodeOffset(), imageLoc.getNodeLength());
if (node instanceof IASTExpression) {
IASTFileLocation exprLoc = node.getFileLocation();
if (exprLoc.getNodeOffset() == imageLoc.getNodeOffset()) {
computeExpressionExtent(node, expressionPosition);
}
}
} else {
computeExpressionExtent(name, expressionPosition);
}
}
} else {
// not a name, but might still be an expression (e.g. this or a selected expression)
IASTNode node = ast.getNodeSelector(null).findEnclosingNode(offset, length);
if (!(node instanceof IASTExpression)) {
node = ast.getNodeSelector(null).findFirstContainedNode(offset, length);
}
if (node instanceof IASTExpression) {
computeExpressionExtent(node, expressionPosition);
} else if (node == null && insideInactiveCode(ast, offset) && !insideComment(ast, offset)) {
return Status.CANCEL_STATUS;
}
}
return Status.OK_STATUS;
}
private void computeMacroArgumentExtent(IASTName name, Position pos) {
IASTImageLocation imageLoc = name.getImageLocation();
int startOffset = imageLoc.getNodeOffset();
int endOffset = startOffset + imageLoc.getNodeLength();
// do some black magic to consider field reference expressions
IASTNode expr = name.getParent();
int macroOffset = name.getFileLocation().getNodeOffset();
if (expr instanceof IASTFieldReference) {
IASTExpression ownerExpr= ((IASTFieldReference) expr).getFieldOwner();
while (ownerExpr instanceof IASTFieldReference || ownerExpr instanceof IASTArraySubscriptExpression) {
if (ownerExpr instanceof IASTArraySubscriptExpression) {
ownerExpr = ((IASTArraySubscriptExpression) ownerExpr).getArrayExpression();
} else {
ownerExpr= ((IASTFieldReference) ownerExpr).getFieldOwner();
}
}
if (ownerExpr instanceof IASTIdExpression) {
IASTName ownerName = ((IASTIdExpression) ownerExpr).getName();
IASTImageLocation ownerImageLoc = ownerName.getImageLocation();
final int nameOffset= ownerImageLoc.getNodeOffset();
// offset should be inside macro expansion
if (nameOffset < startOffset && nameOffset > macroOffset) {
startOffset = nameOffset;
}
}
}
ExpressionChecker checker = new ExpressionChecker();
if (checker.check(expr)) {
pos.offset = startOffset;
pos.length = endOffset - startOffset;
}
}
private void computeExpressionExtent(IASTNode node0, Position pos) {
IASTNode node = node0;
while (node != null && !(node instanceof IASTExpression) && !(node instanceof IASTDeclaration)) {
node = node.getParent();
}
IASTNodeLocation loc = null;
if (node instanceof IASTExpression && !(node instanceof IASTIdExpression)) {
ExpressionChecker checker = new ExpressionChecker();
if (checker.check(node)) {
loc = node.getFileLocation();
}
} else if (node0 instanceof IASTName) {
// fallback: use simple name
loc = ((IASTName) node0).getImageLocation();
if (loc == null) {
IASTNodeLocation[] locations = node0.getNodeLocations();
// avoid macro expansions
if (locations.length == 1 && !(locations[0] instanceof IASTMacroExpansionLocation)) {
loc = locations[0];
}
}
}
if (loc != null) {
pos.offset = loc.getNodeOffset();
pos.length = loc.getNodeLength();
}
}
private boolean insideInactiveCode(IASTTranslationUnit ast, int offset) {
int inactiveCodeStart = -1;
boolean inInactiveCode = false;
Stack<Boolean> inactiveCodeStack = new Stack<Boolean>();
IASTPreprocessorStatement[] preprocStmts = ast.getAllPreprocessorStatements();
for (IASTPreprocessorStatement preprocStmt : preprocStmts) {
IASTPreprocessorStatement statement = preprocStmt;
if (!statement.isPartOfTranslationUnitFile()) {
continue;
}
IASTNodeLocation nodeLocation = statement.getFileLocation();
if (nodeLocation == null) {
continue;
}
int nodeOffset = nodeLocation.getNodeOffset();
int nodeEnd = nodeOffset + nodeLocation.getNodeLength();
if (nodeOffset <= offset && offset < nodeEnd) {
// inside preprocessor directive
return false;
}
if (statement instanceof IASTPreprocessorIfStatement) {
IASTPreprocessorIfStatement ifStmt = (IASTPreprocessorIfStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = nodeEnd;
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorIfdefStatement) {
IASTPreprocessorIfdefStatement ifdefStmt = (IASTPreprocessorIfdefStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifdefStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = nodeEnd;
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorIfndefStatement) {
IASTPreprocessorIfndefStatement ifndefStmt = (IASTPreprocessorIfndefStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifndefStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = nodeEnd;
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorElseStatement) {
IASTPreprocessorElseStatement elseStmt = (IASTPreprocessorElseStatement)statement;
if (!elseStmt.taken() && !inInactiveCode) {
inactiveCodeStart = nodeEnd;
inInactiveCode = true;
} else if (elseStmt.taken() && inInactiveCode) {
int inactiveCodeEnd = nodeOffset;
if (inactiveCodeStart <= offset && offset < inactiveCodeEnd) {
return true;
}
inInactiveCode = false;
}
} else if (statement instanceof IASTPreprocessorElifStatement) {
IASTPreprocessorElifStatement elifStmt = (IASTPreprocessorElifStatement)statement;
if (!elifStmt.taken() && !inInactiveCode) {
inactiveCodeStart = nodeEnd;
inInactiveCode = true;
} else if (elifStmt.taken() && inInactiveCode) {
int inactiveCodeEnd = nodeOffset;
if (inactiveCodeStart <= offset && offset < inactiveCodeEnd) {
return true;
}
inInactiveCode = false;
}
} else if (statement instanceof IASTPreprocessorEndifStatement) {
try {
boolean wasInInactiveCode = inactiveCodeStack.pop().booleanValue();
if (inInactiveCode && !wasInInactiveCode) {
int inactiveCodeEnd = nodeOffset;
if (inactiveCodeStart <= offset && offset < inactiveCodeEnd) {
return true;
}
}
inInactiveCode = wasInInactiveCode;
}
catch (EmptyStackException e) {}
}
}
return false;
}
private boolean insideComment(IASTTranslationUnit ast, int offset) {
IASTComment[] comments = ast.getComments();
for (IASTComment comment : comments) {
if (!comment.isPartOfTranslationUnitFile()) {
continue;
}
IASTNodeLocation location = comment.getFileLocation();
if (location != null) {
if (location.getNodeOffset() <= offset && offset < location.getNodeOffset() + location.getNodeLength()) {
return true;
}
}
}
return false;
}
};
job.setPriority(Job.SHORT);
job.setSystem(true);
job.schedule();
try {
job.join();
} catch (InterruptedException exc) {
job.cancel();
return ""; //$NON-NLS-1$
}
if (!job.getResult().isOK()) {
// indeterminate result
return null;
}
if (expressionPosition.getLength() > 0) {
try {
// Get expression text removing comments, obsolete whitespace, etc.
StringBuilder result = new StringBuilder();
ITypedRegion[] partitions = TextUtilities.computePartitioning(document, ICPartitions.C_PARTITIONING,
expressionPosition.offset, expressionPosition.length, false);
for (ITypedRegion partition : partitions) {
if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())
|| ICPartitions.C_CHARACTER.equals(partition.getType())
|| ICPartitions.C_STRING.equals(partition.getType())) {
result.append(document.get(partition.getOffset(), partition.getLength()));
} else {
result.append(' ');
}
}
String text = result.toString().replaceAll("(\\r\\n|\\n|\t| )+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$
return text;
} catch (BadLocationException exc) {
}
}
// return empty string to indicate invalid expression
return ""; //$NON-NLS-1$
}
/**
* Returns the debug context the expression should be evaluated against.
* The default implementation returns {@link DebugUITools#getDebugContext()}.
*/
protected IAdaptable getSelectionAdaptable() {
return DebugUITools.getDebugContext();
}
/**
* Returns the editor this hover is associated with.
*/
protected final IEditorPart getEditor() {
return fEditor;
}
/**
* Append HTML for the given variable to the given buffer
*/
private static void appendVariable(StringBuilder buffer, String expression, String value) throws DebugException {
if (value.length() > MAX_HOVER_INFO_SIZE)
value = value.substring(0, MAX_HOVER_INFO_SIZE) + " ..."; //$NON-NLS-1$
buffer.append("<p>"); //$NON-NLS-1$
buffer.append("<pre>").append(expression).append("</pre>"); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append(" = "); //$NON-NLS-1$
buffer.append("<b><pre>").append(value).append("</pre></b>"); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append("</p>"); //$NON-NLS-1$
}
/**
* Replace any characters in the given String that would confuse an HTML parser with their escape sequences.
*/
private static String makeHTMLSafe(String string) {
StringBuilder buffer = new StringBuilder(string.length());
for (int i = 0; i != string.length(); i++) {
char ch = string.charAt(i);
switch (ch) {
case '&':
buffer.append("&"); //$NON-NLS-1$
break;
case '<':
buffer.append("<"); //$NON-NLS-1$
break;
case '>':
buffer.append(">"); //$NON-NLS-1$
break;
case '"':
buffer.append("""); //$NON-NLS-1$
break;
default:
buffer.append(ch);
break;
}
}
return buffer.toString();
}
}