blob: 15e637acf9b0a4fd9e52e99e285b9f5ea9ee8024 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Bug Menot - Bug 445867
*******************************************************************************/
package org.eclipse.jdt.internal.debug.ui;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICodeAssist;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IInitializer;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaStackFrame;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.logicalstructures.JDIPlaceholderVariable;
import org.eclipse.jdt.internal.debug.eval.ast.engine.ASTEvaluationEngine;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover;
import org.eclipse.jface.preference.IPreferenceStore;
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.ITextHoverExtension2;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
public class JavaDebugHover implements IJavaEditorTextHover, ITextHoverExtension, ITextHoverExtension2 {
private IEditorPart fEditor;
/* (non-Javadoc)
* @see org.eclipse.jdt.ui.text.java.hover.IJavaEditorTextHover#setEditor(org.eclipse.ui.IEditorPart)
*/
@Override
public void setEditor(IEditorPart editor) {
fEditor = editor;
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.ITextHover#getHoverRegion(org.eclipse.jface.text.ITextViewer, int)
*/
@Override
public IRegion getHoverRegion(ITextViewer textViewer, int offset) {
return JavaWordFinder.findWord(textViewer.getDocument(), offset);
}
/**
* Returns the stack frame in which to search for variables, or <code>null</code>
* if none.
*
* @return the stack frame in which to search for variables, or <code>null</code>
* if none
*/
protected IJavaStackFrame getFrame() {
IAdaptable adaptable = DebugUITools.getDebugContext();
if (adaptable != null) {
return adaptable.getAdapter(IJavaStackFrame.class);
}
return null;
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.ITextHover#getHoverInfo(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion)
*/
@Override
public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) {
Object object = getHoverInfo2(textViewer, hoverRegion);
if (object instanceof IVariable) {
IVariable var = (IVariable) object;
return getVariableText(var);
}
return null;
}
/**
* Returns a local variable in the given frame based on the hover region
* or <code>null</code> if none.
*
* @return local variable or <code>null</code>
*/
private IVariable resolveLocalVariable(IJavaStackFrame frame, ITextViewer textViewer, IRegion hoverRegion) {
if (frame != null) {
try {
IDocument document= textViewer.getDocument();
if (document != null) {
String variableName= document.get(hoverRegion.getOffset(), hoverRegion.getLength());
return findLocalVariable(frame, variableName);
}
} catch (BadLocationException x) {
}
}
return null;
}
/**
* Returns a local variable in the given frame based on the the given name
* or <code>null</code> if none.
*
* @return local variable or <code>null</code>
*/
public static IVariable findLocalVariable(IJavaStackFrame frame, String variableName) {
if (frame != null) {
try {
return frame.findVariable(variableName);
} catch (DebugException x) {
if (x.getStatus().getCode() != IJavaThread.ERR_THREAD_NOT_SUSPENDED) {
JDIDebugUIPlugin.log(x);
}
}
}
return null;
}
/**
* Returns HTML text for the given variable
*/
private static String getVariableText(IVariable variable) {
StringBuilder buffer= new StringBuilder();
JDIModelPresentation modelPresentation = getModelPresentation();
buffer.append("<p><pre>"); //$NON-NLS-1$
String variableText= modelPresentation.getVariableText((IJavaVariable) variable);
buffer.append(replaceHTMLChars(variableText));
buffer.append("</pre></p>"); //$NON-NLS-1$
modelPresentation.dispose();
if (buffer.length() > 0) {
return buffer.toString();
}
return null;
}
/**
* Replaces reserved HTML characters in the given string with
* their escaped equivalents. This is to ensure that variable
* values containing reserved characters are correctly displayed.
*/
private static String replaceHTMLChars(String variableText) {
StringBuilder buffer= new StringBuilder(variableText.length());
char[] characters = variableText.toCharArray();
for (int i = 0; i < characters.length; i++) {
char character= characters[i];
switch (character) {
case '<':
buffer.append("&lt;"); //$NON-NLS-1$
break;
case '>':
buffer.append("&gt;"); //$NON-NLS-1$
break;
case '&':
buffer.append("&amp;"); //$NON-NLS-1$
break;
case '"':
buffer.append("&quot;"); //$NON-NLS-1$
break;
default:
buffer.append(character);
}
}
return buffer.toString();
}
/**
* Returns a configured model presentation for use displaying variables.
*/
private static JDIModelPresentation getModelPresentation() {
JDIModelPresentation presentation = new JDIModelPresentation();
String[][] booleanPrefs= {
{IJDIPreferencesConstants.PREF_SHOW_QUALIFIED_NAMES, JDIModelPresentation.DISPLAY_QUALIFIED_NAMES}};
String viewId= IDebugUIConstants.ID_VARIABLE_VIEW;
for (int i = 0; i < booleanPrefs.length; i++) {
boolean preferenceValue = getBooleanPreferenceValue(viewId, booleanPrefs[i][0]);
presentation.setAttribute(booleanPrefs[i][1], (preferenceValue ? Boolean.TRUE : Boolean.FALSE));
}
return presentation;
}
/**
* Returns the value of this filters preference (on/off) for the given
* view.
*
* @param part
* @return boolean
*/
public static boolean getBooleanPreferenceValue(String id, String preference) {
String compositeKey = id + "." + preference; //$NON-NLS-1$
IPreferenceStore store = JDIDebugUIPlugin.getDefault().getPreferenceStore();
boolean value = false;
if (store.contains(compositeKey)) {
value = store.getBoolean(compositeKey);
} else {
value = store.getBoolean(preference);
}
return value;
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.ITextHoverExtension#getHoverControlCreator()
*/
@Override
public IInformationControlCreator getHoverControlCreator() {
return new ExpressionInformationControlCreator();
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.ITextHoverExtension2#getHoverInfo2(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion)
*/
@Override
public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) {
IJavaStackFrame frame = getFrame();
if (frame != null) {
// first check for 'this' - code resolve does not resolve java elements for 'this'
IDocument document= textViewer.getDocument();
if (document != null) {
try {
String variableName= document.get(hoverRegion.getOffset(), hoverRegion.getLength());
if (variableName.equals("this")) { //$NON-NLS-1$
try {
IJavaVariable variable = frame.findVariable(variableName);
if (variable != null) {
return variable;
}
} catch (DebugException e) {
return null;
}
}
} catch (BadLocationException e) {
return null;
}
}
ICodeAssist codeAssist = null;
if (fEditor != null) {
IEditorInput input = fEditor.getEditorInput();
Object element = JavaUI.getWorkingCopyManager().getWorkingCopy(input);
if (element == null) {
element = input.getAdapter(IClassFile.class);
}
if (element instanceof ICodeAssist) {
codeAssist = ((ICodeAssist)element);
}
}
if (codeAssist == null) {
return resolveLocalVariable(frame, textViewer, hoverRegion);
}
IJavaElement[] resolve = null;
try {
resolve = codeAssist.codeSelect(hoverRegion.getOffset(), 0);
} catch (JavaModelException e1) {
resolve = new IJavaElement[0];
}
try {
for (int i = 0; i < resolve.length; i++) {
IJavaElement javaElement = resolve[i];
if (javaElement instanceof IField) {
IField field = (IField)javaElement;
IJavaVariable variable = null;
IJavaDebugTarget debugTarget = (IJavaDebugTarget)frame.getDebugTarget();
if (Flags.isStatic(field.getFlags())) {
IJavaType[] javaTypes = debugTarget.getJavaTypes(field.getDeclaringType().getFullyQualifiedName());
if (javaTypes != null) {
for (int j = 0; j < javaTypes.length; j++) {
IJavaType type = javaTypes[j];
if (type instanceof IJavaReferenceType) {
IJavaReferenceType referenceType = (IJavaReferenceType) type;
variable = referenceType.getField(field.getElementName());
}
if (variable != null) {
break;
}
}
}
if (variable == null) {
// the class is not loaded yet, but may be an in-lined primitive constant
Object constant = field.getConstant();
if (constant != null) {
IJavaValue value = null;
if (constant instanceof Integer) {
value = debugTarget.newValue(((Integer)constant).intValue());
} else if (constant instanceof Byte) {
value = debugTarget.newValue(((Byte)constant).byteValue());
} else if (constant instanceof Boolean) {
value = debugTarget.newValue(((Boolean)constant).booleanValue());
} else if (constant instanceof Character) {
value = debugTarget.newValue(((Character)constant).charValue());
} else if (constant instanceof Double) {
value = debugTarget.newValue(((Double)constant).doubleValue());
} else if (constant instanceof Float) {
value = debugTarget.newValue(((Float)constant).floatValue());
} else if (constant instanceof Long) {
value = debugTarget.newValue(((Long)constant).longValue());
} else if (constant instanceof Short) {
value = debugTarget.newValue(((Short)constant).shortValue());
} else if (constant instanceof String) {
value = debugTarget.newValue((String)constant);
}
if (value != null) {
variable = new JDIPlaceholderVariable(field.getElementName(), value);
}
}
if (variable == null) {
return null; // class not loaded yet and not a constant
}
}
} else {
if (!frame.isStatic() && !frame.isNative()) {
// ensure that we only resolve a field access on 'this':
if (!(codeAssist instanceof ITypeRoot)) {
return null;
}
ITypeRoot typeRoot = (ITypeRoot) codeAssist;
ASTNode root = SharedASTProviderCore.getAST(typeRoot, SharedASTProviderCore.WAIT_NO, null);
if (root == null) {
ASTParser parser = ASTParser.newParser(AST.JLS13);
parser.setSource(typeRoot);
parser.setFocalPosition(hoverRegion.getOffset());
root = parser.createAST(null);
}
ASTNode node = NodeFinder.perform(root, hoverRegion.getOffset(), hoverRegion.getLength());
if (node == null) {
return null;
}
StructuralPropertyDescriptor locationInParent = node.getLocationInParent();
if (locationInParent == FieldAccess.NAME_PROPERTY) {
FieldAccess fieldAccess = (FieldAccess) node.getParent();
if (!(fieldAccess.getExpression() instanceof ThisExpression)) {
return null;
}
} else if (locationInParent == QualifiedName.NAME_PROPERTY) {
return null;
}
String typeSignature = Signature.createTypeSignature(field.getDeclaringType().getFullyQualifiedName(), true);
typeSignature = typeSignature.replace('.', '/');
IJavaObject ths = frame.getThis();
if (ths != null) {
variable = ths.getField(field.getElementName(), typeSignature);
}
}
}
if (variable != null) {
return variable;
}
break;
}
if (javaElement instanceof ILocalVariable) {
ILocalVariable var = (ILocalVariable)javaElement;
IJavaElement parent = var.getParent();
while (!(parent instanceof IMethod) && !(parent instanceof IInitializer) && parent != null) {
parent = parent.getParent();
}
if (parent instanceof IInitializer && "()V".equals(frame.getSignature()) //$NON-NLS-1$
&& "<clinit>".equals(frame.getMethodName())) { //$NON-NLS-1$
return findLocalVariable(frame, var.getElementName());
}
if (parent instanceof IMethod) {
IMethod method = (IMethod) parent;
boolean equal = false;
if (method.isBinary()) {
// compare resolved signatures
if (method.getSignature().equals(frame.getSignature()) && method.getElementName().equals(frame.getMethodName())) {
equal = true;
} else {
// Check if there are variables captured by lambda, see bug 516278
if (org.eclipse.jdt.internal.debug.core.model.LambdaUtils.isLambdaFrame(frame)) {
return LambdaUtils.findLocalVariableFromLambdaScope(frame, var);
}
}
} else {
// compare unresolved signatures
// Frames in classes with generics have declaringTypeName like class<V>
// We must get rid of this '<V>' for proper comparison
String frameDeclaringTypeName = frame.getDeclaringTypeName();
int genericPartOffset = frameDeclaringTypeName.indexOf('<');
if (genericPartOffset != -1) {
frameDeclaringTypeName = frameDeclaringTypeName.substring(0, genericPartOffset);
}
if (((frame.isConstructor() && method.isConstructor()) || frame.getMethodName().equals(method.getElementName()))
&& frameDeclaringTypeName.endsWith(method.getDeclaringType().getElementName())
&& frame.getArgumentTypeNames().size() == method.getNumberOfParameters()) {
equal = true;
}
else { // Finding variables in anonymous class
int index = frame.getDeclaringTypeName().indexOf('$');
if (index > 0) {
String name = frame.getDeclaringTypeName().substring(index + 1);
try {
Integer.getInteger(name);
return findLocalVariable(frame, ASTEvaluationEngine.ANONYMOUS_VAR_PREFIX + var.getElementName());
}
catch (NumberFormatException ex) {
}
} else {
// Check if there are variables captured by lambda, see bug 516278
if (org.eclipse.jdt.internal.debug.core.model.LambdaUtils.isLambdaFrame(frame)) {
return LambdaUtils.findLocalVariableFromLambdaScope(frame, var);
}
}
}
}
// find variable if equal or method is a Lambda Method
if (equal || method.isLambdaMethod()) {
return findLocalVariable(frame, var.getElementName());
}
}
break;
}
}
} catch (CoreException e) {
JDIDebugPlugin.log(e);
}
}
return null;
}
public IInformationControlCreator getInformationPresenterControlCreator() {
return new ExpressionInformationControlCreator();
}
}