/*******************************************************************************
* Copyright (c) 2007 IBM Corporation.
* 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:
* Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation
*******************************************************************************/
package com.redhat.ceylon.eclipse.code.outline;
import static com.redhat.ceylon.eclipse.code.outline.CeylonOutlineNode.DEFAULT_CATEGORY;
import static com.redhat.ceylon.eclipse.code.outline.CeylonOutlineNode.IMPORT_LIST_CATEGORY;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.getPreferences;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.imageRegistry;
import static com.redhat.ceylon.eclipse.ui.CeylonResources.CONFIG_LABELS;
import static com.redhat.ceylon.eclipse.ui.CeylonResources.EXPAND_ALL;
import static com.redhat.ceylon.eclipse.ui.CeylonResources.HIDE_PRIVATE;
import static com.redhat.ceylon.eclipse.ui.CeylonResources.SORT_ALPHA;
import static com.redhat.ceylon.eclipse.util.EditorUtil.getCurrentEditor;
import static org.eclipse.ui.PlatformUI.getWorkbench;
import static org.eclipse.ui.dialogs.PreferencesUtil.createPreferenceDialogOn;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.actions.CollapseAllAction;
import org.eclipse.jdt.internal.ui.packageview.DefaultElementComparer;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.DecoratingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DecorationContext;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import com.redhat.ceylon.compiler.typechecker.context.PhasedUnit;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.SyntheticVariable;
import com.redhat.ceylon.compiler.typechecker.tree.Visitor;
import com.redhat.ceylon.eclipse.code.editor.CeylonSourceViewer;
import com.redhat.ceylon.eclipse.code.parse.CeylonParseController;
import com.redhat.ceylon.eclipse.code.parse.TreeLifecycleListener;
import com.redhat.ceylon.eclipse.code.preferences.CeylonOutlinesPreferencePage;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
import com.redhat.ceylon.ide.common.model.CeylonUnit;
import com.redhat.ceylon.model.typechecker.model.Declaration;
import com.redhat.ceylon.model.typechecker.model.Unit;
public class CeylonOutlinePage extends ContentOutlinePage
implements TreeLifecycleListener, CaretListener {
private static final String OUTLINE_POPUP_MENU_ID =
PLUGIN_ID + ".outline.popupMenu";
private static final ImageDescriptor PUBLIC =
imageRegistry().getDescriptor(HIDE_PRIVATE);
private static final ImageDescriptor ALPHA =
imageRegistry().getDescriptor(SORT_ALPHA);
private final ITreeContentProvider contentProvider =
new OutlineContentProvider();
private StyledCellLabelProvider labelProvider;
private CeylonParseController parseController;
private CeylonSourceViewer sourceViewer;
private volatile boolean suspend = false;
private IPropertyChangeListener propertyChangeListener;
@Override
public void init(IPageSite site) {
super.init(site);
propertyChangeListener =
new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
TreeViewer treeViewer = getTreeViewer();
treeViewer.getTree()
.setFont(CeylonPlugin.getOutlineFont());
treeViewer.refresh();
}
};
getPreferences()
.addPropertyChangeListener(propertyChangeListener);
getWorkbench().getThemeManager()
.addPropertyChangeListener(propertyChangeListener);
}
public CeylonOutlinePage(
CeylonParseController parseController,
CeylonSourceViewer sourceViewer) {
this.parseController = parseController;
this.sourceViewer = sourceViewer;
}
@Override
public Stage getStage() {
return Stage.FOR_OUTLINE;
}
@Override
public void update(
final CeylonParseController controller,
IProgressMonitor monitor) {
if (//editor.isBackgroundParsingPaused() ||
monitor.isCanceled()) {
return;
}
TreeViewer treeViewer = getTreeViewer();
if (treeViewer!=null) {
org.eclipse.swt.widgets.Tree tree =
treeViewer.getTree();
if (!tree.isDisposed()) {
tree.getDisplay()
.syncExec(new Runnable() {
@Override
public void run() {
TreeViewer viewer = getTreeViewer();
if (viewer!=null) {
org.eclipse.swt.widgets.Tree tree =
viewer.getTree();
if (!tree.isDisposed()) {
tree.setRedraw(false);
try {
update(controller);
}
finally {
tree.setRedraw(true);
}
}
}
}
});
}
}
}
private void update(CeylonParseController controller) {
TreeViewer viewer = getTreeViewer();
boolean noInput = viewer.getInput()==null;
Object[] expanded = viewer.getExpandedElements();
if (controller.getStage().ordinal()
>= getStage().ordinal()) {
CeylonOutlineNode rootNode =
new CeylonOutlineBuilder()
.buildTree(controller);
viewer.setInput(rootNode);
if (noInput) {
expand(rootNode);
}
else {
for (Object obj: expanded) {
viewer.expandToLevel(obj, 1);
}
int pos = sourceViewer.getSelectedRange().x;
expandCaretedNode(pos);
}
}
}
@Override
public void createControl(Composite parent) {
super.createControl(parent);
TreeViewer viewer = getTreeViewer();
if (labelProvider!=null) {
labelProvider.dispose();
}
labelProvider = createLabelProvider();
viewer.setLabelProvider(labelProvider);
viewer.setContentProvider(contentProvider);
viewer.setComparer(new DefaultElementComparer());
viewer.getTree()
.setFont(CeylonPlugin.getOutlineFont());
CeylonOutlineNode rootNode =
new CeylonOutlineBuilder()
.buildTree(parseController);
viewer.setInput(rootNode);
expand(rootNode);
sourceViewer.getTextWidget().addCaretListener(this);
}
private DecoratingStyledCellLabelProvider createLabelProvider() {
return new DecoratingStyledCellLabelProvider(
new CeylonOutlineLabelProvider(),
getWorkbench()
.getDecoratorManager()
.getLabelDecorator(),
DecorationContext.DEFAULT_CONTEXT);
}
@Override
public void makeContributions(IMenuManager menuManager,
IToolBarManager toolBarManager,
IStatusLineManager statusLineManager) {
super.makeContributions(menuManager, toolBarManager,
statusLineManager);
MenuManager contextMenu = new MenuManager();
JavaPlugin.createStandardGroups(contextMenu);
TreeViewer treeViewer = getTreeViewer();
getSite()
.registerContextMenu(OUTLINE_POPUP_MENU_ID,
contextMenu, treeViewer);
Control control = treeViewer.getControl();
control.setMenu(contextMenu.createContextMenu(control));
ExpandAllAction expandAction =
new ExpandAllAction();
CollapseAllAction collapseAllAction =
new CollapseAllAction(treeViewer);
LexicalSortingAction sortAction =
new LexicalSortingAction();
HideNonSharedAction hideAction =
new HideNonSharedAction();
toolBarManager.add(expandAction);
toolBarManager.add(collapseAllAction);
toolBarManager.add(sortAction);
toolBarManager.add(hideAction);
menuManager.add(expandAction);
menuManager.add(collapseAllAction);
menuManager.add(new Separator());
menuManager.add(sortAction);
menuManager.add(hideAction);
menuManager.add(new Separator());
ImageDescriptor desc =
CeylonPlugin.imageRegistry()
.getDescriptor(CONFIG_LABELS);
Action configureAction =
new Action("Configure Labels...", desc) {
@Override
public void run() {
createPreferenceDialogOn(
getSite().getShell(),
CeylonOutlinesPreferencePage.ID,
new String[] {
CeylonOutlinesPreferencePage.ID,
CeylonPlugin.COLORS_AND_FONTS_PAGE_ID
},
null).open();
}
};
menuManager.add(configureAction);
}
@Override
public void dispose() {
super.dispose();
if (labelProvider!=null) {
labelProvider.dispose();
labelProvider = null;
}
if (propertyChangeListener!=null) {
getPreferences()
.removePropertyChangeListener(propertyChangeListener);
getWorkbench().getThemeManager()
.removePropertyChangeListener(propertyChangeListener);
propertyChangeListener = null;
}
sourceViewer.getTextWidget().removeCaretListener(this);
sourceViewer = null;
parseController = null;
}
private void expand(CeylonOutlineNode rootNode) {
if (rootNode!=null) {
for (CeylonOutlineNode on: rootNode.getChildren()) {
TreeViewer viewer = getTreeViewer();
if (on.getCategory()==IMPORT_LIST_CATEGORY) {
viewer.collapseToLevel(on,
TreeViewer.ALL_LEVELS);
}
else {
viewer.expandToLevel(on,
TreeViewer.ALL_LEVELS);
}
}
}
}
private static final class OutlineContentProvider
implements ITreeContentProvider {
@Override
public Object[] getChildren(Object element) {
CeylonOutlineNode on =
(CeylonOutlineNode) element;
return on.getChildren().toArray();
}
@Override
public Object getParent(Object element) {
CeylonOutlineNode on =
(CeylonOutlineNode) element;
return on.getParent();
}
@Override
public boolean hasChildren(Object element) {
Object[] children = getChildren(element);
return children!=null && children.length > 0;
}
@Override
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}
@Override
public void dispose() {}
@Override
public void inputChanged(Viewer viewer,
Object oldInput, Object newInput) {}
}
private class ExpandAllAction extends Action {
private ExpandAllAction() {
super("Expand All");
setToolTipText("Expand All");
ImageDescriptor desc =
imageRegistry().getDescriptor(EXPAND_ALL);
setHoverImageDescriptor(desc);
setImageDescriptor(desc);
}
@Override
public void run() {
TreeViewer outlineViewer = getTreeViewer();
if (outlineViewer != null) {
outlineViewer.expandAll();
}
}
}
private static final ViewerComparator fElementComparator =
new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
CeylonOutlineNode t1 = (CeylonOutlineNode) e1;
CeylonOutlineNode t2 = (CeylonOutlineNode) e2;
int cat1 = t1.getCategory();
int cat2 = t2.getCategory();
if (cat1 == cat2) {
String i1 = t1.getIdentifier();
String i2 = t2.getIdentifier();
if (i1==i2) return 0;
if (i1==null) return -1;
if (i2==null) return 1;
return i1.compareTo(i2);
}
return cat1 - cat2;
}
};
private static final ViewerComparator fPositionComparator =
new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
CeylonOutlineNode t1 = (CeylonOutlineNode) e1;
CeylonOutlineNode t2 = (CeylonOutlineNode) e2;
return t1.getStartOffset() - t2.getStartOffset();
}
};
private static final String SORT_BY_NAME =
"sortOutlineViewByName";
private class LexicalSortingAction extends Action {
private LexicalSortingAction() {
setText("Sort");
setToolTipText("Sort by Name");
setDescription("Sort entries lexically by name");
this.setHoverImageDescriptor(ALPHA);
this.setImageDescriptor(ALPHA);
boolean checked =
CeylonPlugin.getPreferences()
.getBoolean(SORT_BY_NAME);
valueChanged(checked, false);
}
@Override
public void run() {
valueChanged(isChecked(), true);
}
private void valueChanged(boolean on, boolean store) {
final TreeViewer outlineViewer = getTreeViewer();
setChecked(on);
Display display =
outlineViewer.getControl()
.getDisplay();
final ViewerComparator comparator;
if (on) {
comparator = fElementComparator;
}
else {
comparator = fPositionComparator;
}
BusyIndicator.showWhile(display,
new Runnable() {
@Override
public void run() {
outlineViewer.setComparator(comparator);
}
});
if (store) {
CeylonPlugin.getPreferences()
.setValue(SORT_BY_NAME, on);
}
}
}
private static final ViewerFilter filter =
new ViewerFilter() {
@Override
public boolean select(Viewer viewer,
Object parentElement, Object element) {
CeylonOutlineNode on =
(CeylonOutlineNode) element;
return on.isShared();
}
};
private static final String HIDE_NON_SHARED =
"hideNonSharedInOutlineView";
private class HideNonSharedAction extends Action {
public HideNonSharedAction() {
setText("Hide Unshared");
setToolTipText("Hide Unshared Declarations");
setDescription("Hide unshared declarations");
setHoverImageDescriptor(PUBLIC);
setImageDescriptor(PUBLIC);
boolean checked =
getPreferences()
.getBoolean(HIDE_NON_SHARED);
valueChanged(checked, false);
}
@Override
public void run() {
valueChanged(isChecked(), true);
}
private void valueChanged(final boolean on, boolean store) {
final TreeViewer outlineViewer = getTreeViewer();
setChecked(on);
Display display =
outlineViewer.getControl()
.getDisplay();
BusyIndicator.showWhile(display,
new Runnable() {
@Override
public void run() {
if (on) {
outlineViewer.addFilter(filter);
}
else {
outlineViewer.removeFilter(filter);
}
}
});
if (store) {
getPreferences()
.setValue(HIDE_NON_SHARED, on);
}
}
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
super.selectionChanged(event);
if (!suspend) {
ITreeSelection selection =
(ITreeSelection)
event.getSelection();
if (!selection.isEmpty()) {
CeylonOutlineNode on =
(CeylonOutlineNode)
selection.getFirstElement();
if (on.getCategory()==DEFAULT_CATEGORY) {
suspend = true;
try {
int startOffset = on.getStartOffset();
int endOffset = on.getEndOffset();
ITextEditor editor =
(ITextEditor)
getCurrentEditor();
editor.selectAndReveal(startOffset,
endOffset-startOffset);
}
finally {
suspend = false;
}
}
}
}
}
@Override
public void caretMoved(CaretEvent event) {
int offset =
sourceViewer.widgetOffset2ModelOffset(
event.caretOffset);
expandCaretedNode(offset);
}
private void expandCaretedNode(int offset) {
if (suspend) return;
if (offset==0) return; //right at the start of file, don't expand the import list
Tree.CompilationUnit rootNode =
parseController.getLastCompilationUnit();
if (rootNode==null) {
return;
}
Unit unit = rootNode.getUnit();
if (unit==null) {
return;
}
if (unit instanceof CeylonUnit) {
CeylonUnit ceylonUnit =
(CeylonUnit) unit;
PhasedUnit phasedUnit =
ceylonUnit.getPhasedUnit();
if (phasedUnit==null ||
! phasedUnit.isFullyTyped()) {
return;
}
}
if (getTreeViewer().getInput()==null) {
return;
}
suspend = true;
try {
OutlineNodeVisitor visitor =
new OutlineNodeVisitor(offset);
rootNode.visit(visitor);
List<CeylonOutlineNode> result = visitor.result;
if (!result.isEmpty()) {
TreePath treePath =
new TreePath(result.toArray());
CeylonOutlineNode last =
result.get(result.size()-1);
if (!last.getChildren().isEmpty()) {
getTreeViewer().expandToLevel(treePath, 1);
}
setSelection(new TreeSelection(treePath));
}
}
finally {
suspend = false;
}
}
private static final class OutlineNodeVisitor extends Visitor {
private final int offset;
private final boolean hideNonshared;
OutlineNodeVisitor(int offset) {
this.offset = offset;
hideNonshared =
getPreferences()
.getBoolean(HIDE_NON_SHARED);
}
List<CeylonOutlineNode> result =
new ArrayList<CeylonOutlineNode>();
private CeylonOutlineNode getParent() {
return result.isEmpty() ? null :
result.get(result.size()-1);
}
@Override
public void visit(Tree.Declaration that) {
if (!(that instanceof Tree.TypeParameterDeclaration) &&
!(that instanceof Tree.TypeConstraint) &&
!(that instanceof Tree.Variable &&
((Tree.Variable) that).getType()
instanceof SyntheticVariable)) {
if (inBounds(that)) {
Declaration dm = that.getDeclarationModel();
if (!hideNonshared ||
dm!=null && dm.isShared()) {
CeylonOutlineNode on =
new CeylonOutlineNode(
that, getParent());
result.add(on);
super.visit(that);
}
}
else {
super.visit(that);
}
}
else {
super.visit(that);
}
}
@Override
public void visit(Tree.SpecifierStatement that) {
Tree.Term bme = that.getBaseMemberExpression();
if (that.getRefinement()) {
if (bme instanceof Tree.BaseMemberExpression ||
bme instanceof Tree.ParameterizedExpression &&
((Tree.ParameterizedExpression) bme).getPrimary()
instanceof Tree.BaseMemberExpression) {
if (inBounds(that)) {
if (!hideNonshared) {
CeylonOutlineNode on =
new CeylonOutlineNode(
that, getParent());
result.add(on);
}
else {
super.visit(that);
}
}
else {
super.visitAny(that);
}
}
}
}
@Override
public void visit(Tree.Parameter that) {}
@Override
public void visit(Tree.CompilationUnit that) {
// result.add(new CeylonOutlineNode(that, ROOT_CATEGORY));
super.visit(that);
}
@Override
public void visit(Tree.ImportList that) {
if (inBounds(that)) {
result.add(new CeylonOutlineNode(that,
IMPORT_LIST_CATEGORY));
}
super.visit(that);
}
@Override
public void visit(Tree.PackageDescriptor that) {
if (inBounds(that)) {
result.add(new CeylonOutlineNode(that));
}
super.visit(that);
}
@Override
public void visit(Tree.ModuleDescriptor that) {
if (inBounds(that)) {
result.add(new CeylonOutlineNode(that));
}
super.visit(that);
}
@Override
public void visit(Tree.Import that) {
if (inBounds(that)) {
result.add(new CeylonOutlineNode(that,
getParent()));
}
super.visit(that);
}
@Override
public void visit(Tree.ImportModule that) {
if (inBounds(that)) {
result.add(new CeylonOutlineNode(that,
getParent()));
}
super.visit(that);
}
private boolean inBounds(Node that) {
Integer tokenStartIndex = that.getStartIndex();
Integer tokenEndIndex = that.getEndIndex();
return tokenStartIndex!=null &&
tokenEndIndex!=null &&
tokenStartIndex<=offset &&
tokenEndIndex>=offset;
}
}
public boolean isDisposed() {
return sourceViewer==null;
}
}