package org.codemap.callhierarchy;
import static org.codemap.util.ID.CALL_HIERARCHY_REF;
import java.lang.reflect.Field;
import org.codemap.util.Log;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.internal.corext.callhierarchy.MethodWrapper;
import org.eclipse.jdt.internal.ui.callhierarchy.CallHierarchyViewPart;
import org.eclipse.jdt.internal.ui.callhierarchy.CancelSearchAction;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.ui.IPageListener;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import ch.akuhn.util.List;
public class CallHierarchyTracker {
/**
* Listen to new pages that are opened or closed and triggers adding or
* removal of an IPartListener to them.
*/
private IPageListener pageListener = new IPageListener(){
@Override
public void pageActivated(IWorkbenchPage page) {
addPartListener(page);
}
@Override
public void pageClosed(IWorkbenchPage page) {
removePartListener(page);
}
@Override
public void pageOpened(IWorkbenchPage page) {
addPartListener(page);
}
};
/**
* When new call hierarchy data is available the selection jumps to the root node.
* We use this information to fire an event whenever we think that the call-hierarchy
* displayed has changed.
*
*/
private ISelectionChangedListener changeListener = new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (selection.isEmpty()) return;
if (!(selection instanceof ITreeSelection)) return;
ITreeSelection treeSelection = (ITreeSelection) selection;
if (treeSelection.size() != 1) return;
// might be a new call hierarchy only if exactly one element is selected (the root)
TreePath path = treeSelection.getPaths()[0];
int segmentCount = path.getSegmentCount();
if (segmentCount != 1) return;
// we are sure that root is selected, but it might be an old one
Object methodWrapperObject = treeSelection.toList().get(0);
if (! (methodWrapperObject instanceof MethodWrapper)) return;
MethodWrapper rootMethod = (MethodWrapper) methodWrapperObject;
onTreeRootSelected(rootMethod);
}
};
private ITreeViewerListener treeListener = new ITreeViewerListener() {
@Override
public void treeExpanded(TreeExpansionEvent event) {
waitForResults();
onCallHierarchyExpanded(event);
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
onCallHierarchyCollapsed(event);
}
};
/**
* Reports when a new CallHierarchyViewPart is opened or closed and attaches
* the treeListener and changeListener to the current CallHierarchyViewPart.
*
*/
private IPartListener2 partListener = new IPartListener2() {
@Override
public void partOpened(IWorkbenchPartReference partRef) {
CallHierarchyViewPart callHierarchy = getCallHierarchyPart(partRef);
if (callHierarchy == null) return;
}
@Override
public void partClosed(IWorkbenchPartReference partRef) {
CallHierarchyViewPart callHierarchy = getCallHierarchyPart(partRef);
if (callHierarchy == null) return;
if (callTree != null) {
callTree.removeTreeListener(treeListener);
callTree.removeSelectionChangedListener(changeListener);
}
// there will be a different treeViewer once the CallHierarchy is reopened
callTree = null;
}
@Override
public void partActivated(IWorkbenchPartReference partRef) {
if (isInitialized()) return;
callHierarchyPart = getCallHierarchyPart(partRef);
if (callHierarchyPart == null) return;
Class<? extends CallHierarchyViewPart> c = callHierarchyPart.getClass();
try {
Field field = c.getDeclaredField(CALL_HIERARCHY_VIEWER_ATTRIBUTE);
field.setAccessible(true);
Object treeViewerObject = field.get(callHierarchyPart);
if (!(treeViewerObject instanceof TreeViewer)) return;
callTree = (TreeViewer) treeViewerObject;
callTree.addTreeListener(treeListener);
callTree.addSelectionChangedListener(changeListener);
} catch (Exception e) {
Log.error(e);
}
}
// we don't care about the following events
@Override
public void partInputChanged(IWorkbenchPartReference partRef) {}
@Override
public void partHidden(IWorkbenchPartReference partRef) {}
@Override
public void partDeactivated(IWorkbenchPartReference partRef) {}
@Override
public void partBroughtToTop(IWorkbenchPartReference partRef) {}
@Override
public void partVisible(IWorkbenchPartReference partRef) {}
};
public static final String CALL_HIERARCHY_VIEWER_ATTRIBUTE = "fCallHierarchyViewer";
public static final String CANCEL_SEARCH_ACTION_ATTRIBUTE = "fCancelSearchAction";
private TreeViewer callTree;
private CallModel callModel;
private CallHierarchyViewPart callHierarchyPart;
private MethodWrapper currentRootMethod;
public CallHierarchyTracker() {
callModel = new CallModel();
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
addPageListener(window);
addPartListener(window.getActivePage());
}
protected void removePartListener(IWorkbenchPage page) {
if (page == null) return;
page.removePartListener(partListener);
}
protected void addPartListener(IWorkbenchPage page) {
if (page == null) return;
page.addPartListener(partListener);
}
protected void removePageListener(IWorkbenchWindow window) {
window.removePageListener(pageListener);
}
protected void addPageListener(IWorkbenchWindow window) {
window.addPageListener(pageListener);
}
protected void onTreeRootSelected(MethodWrapper rootMethod) {
// need an identity check here, if the roots are not identical then
// the call hierarchy was reloaded
if (currentRootMethod == rootMethod) return;
currentRootMethod = rootMethod;
callModel.newRoot(currentRootMethod);
waitForResults();
onCallHierarchyResultsLoaded(currentRootMethod);
}
protected void onCallHierarchyCollapsed(TreeExpansionEvent event) {
Object methodWrapperObject = event.getElement();
if (!(methodWrapperObject instanceof MethodWrapper)) return;
MethodWrapper source = (MethodWrapper) methodWrapperObject;
callModel.collapse(source);
}
protected void onCallHierarchyExpanded(TreeExpansionEvent event) {
Object methodWrapperObject = event.getElement();
if (!(methodWrapperObject instanceof MethodWrapper)) return;
MethodWrapper source = (MethodWrapper) methodWrapperObject;
onCallHierarchyResultsLoaded(source);
}
protected void onCallHierarchyResultsLoaded(MethodWrapper source) {
List<MethodWrapper> targets = List.from(source.getCalls(new NullProgressMonitor()));
callModel.expand(source, targets);
}
protected void waitForResults() {
new Impatient().waitFor(callHierarchyPart);
}
private boolean isInitialized() {
return callTree != null;
}
private CallHierarchyViewPart getCallHierarchyPart(IWorkbenchPartReference partRef) {
if (! partRef.getId().equals(CALL_HIERARCHY_REF.id)) return null;
IWorkbenchPart part = partRef.getPart(true);
if (!(part instanceof CallHierarchyViewPart)) return null;
return (CallHierarchyViewPart) part;
}
public void disable() {
callModel.disable();
}
public void enable() {
callModel.enable();
}
/**
* Unregister the active IPartListener and IPageListener. Call on plug-in shutdown to enable
* clean deregistration of all the active listeners.
*/
public void dispose() {
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
removePageListener(window);
removePartListener(window.getActivePage());
}
}
class Impatient {
private static final int STEP = 50;
private static final int MAX_SLEEP_TIME = 200;
private int sleepTime;
public Impatient() {
sleepTime = 50;
}
/**
* wait until an ongoing search for callees has finished and poll for results.
* @param callHierarchyPart the current CallHierarchyViewPart
*/
public void waitFor(CallHierarchyViewPart callHierarchyPart) {
Class<? extends CallHierarchyViewPart> callHiearachyClass = callHierarchyPart.getClass();
try {
Field fCancelSearchActionField = callHiearachyClass.getDeclaredField(CallHierarchyTracker.CANCEL_SEARCH_ACTION_ATTRIBUTE);
fCancelSearchActionField.setAccessible(true);
Object cancelSearchActionObj = fCancelSearchActionField.get(callHierarchyPart);
if (!(cancelSearchActionObj instanceof CancelSearchAction)) return;
CancelSearchAction cancelSearchAction = (CancelSearchAction) cancelSearchActionObj;
// TODO: maybe handle InterruptedExcpetions appropriately
while(cancelSearchAction.isEnabled()) {
Thread.sleep(sleepTime());
}
} catch (Exception e) {
Log.error(e);
}
}
/**
* for a bit less aggressive polling increase the sleep time for every call
* @return the current sleep time
*/
private long sleepTime() {
if (sleepTime < MAX_SLEEP_TIME) {
sleepTime += STEP;
}
return sleepTime;
}
}