/*******************************************************************************
* Copyright (c) 2009 Manuel Woelker.
* 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:
* Manuel Woelker - initial prototype implementation
*******************************************************************************/
package ccw.editors.outline;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import ccw.CCWPlugin;
import ccw.ClojureCore;
import ccw.editors.clojure.ClojureEditor;
import clojure.lang.IMapEntry;
import clojure.lang.Keyword;
import clojure.lang.LineNumberingPushbackReader;
import clojure.lang.LispReader;
import clojure.lang.LispReader.ReaderException;
import clojure.lang.Obj;
import clojure.lang.RT;
import clojure.lang.Symbol;
public class ClojureOutlinePage extends ContentOutlinePage {
private final class OutlineLabelProvider extends LabelProvider implements
IStyledLabelProvider {
private String getSymbol(List<?> list) {
// TODO: smarter behavior when this is not a symbol
String symbol = NOT_AVAILABLE;
if (list.size() > 1) {
symbol = safeToString(RT.second(list));
}
return symbol;
}
private String getKind(List<?> list) {
// TODO: "smarter" behavior in general
String kind = NOT_AVAILABLE;
if (list.size() > 0) {
kind = safeToString(RT.first(list));
}
return kind;
}
public Image getImage(Object element) {
// TODO: different images for macros, fns, etc.
// this is using results of reading analysis only... :-(
if (isPrivate((List)element)) {
return CCWPlugin.getDefault().getImageRegistry().get(CCWPlugin.PRIVATE_FUNCTION);
} else {
return CCWPlugin.getDefault().getImageRegistry().get(CCWPlugin.PUBLIC_FUNCTION);
}
}
public StyledString getStyledText(Object element) {
StyledString result;
if (element instanceof List<?>) {
List<?> list = (List<?>) element;
result = new StyledString(getSymbol(list));
StyledString kindString = new StyledString(
" : " + getKind(list), //$NON-NLS-1$
StyledString.QUALIFIER_STYLER);
result.append(kindString);
} else {
// TODO: handle non-lists...
result = new StyledString(safeToString(element));
}
return result;
}
}
private final class DocumentChangedListener implements IDocumentListener {
public void documentChanged(DocumentEvent event) {
refreshInput();
}
public void documentAboutToBeChanged(DocumentEvent event) {
}
}
private final class EditorSelectionChangedListener implements
ISelectionChangedListener {
private EditorSelectionChangedListener(TreeViewer viewer) {
}
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
selectInOutline(selection);
}
}
private final class TreeSelectionChangedListener implements
ISelectionChangedListener {
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (isActivePart()) {
// only when this is the active part, i.e. is user initiated
selectInEditor(selection);
}
}
}
private static final String OUTLINE_VIEW_ID = "org.eclipse.ui.views.ContentOutline"; //$NON-NLS-1$
private static final Keyword KEYWORD_LINE = Keyword.intern(null, "line"); //$NON-NLS-1$
private final String NOT_AVAILABLE = "N/A"; //$NON-NLS-1$
private final Object REFRESH_OUTLINE_JOB_FAMILY = new Object();
private final IDocumentProvider documentProvider;
private final ClojureEditor editor;
private List<List> forms = new ArrayList<List>(0);
private boolean sort = CCWPlugin.getDefault().getPreferenceStore().getBoolean("LexicalSortingAction.isChecked");
private IDocument document;
private TreeSelectionChangedListener treeSelectionChangedListener;
private EditorSelectionChangedListener editorSelectionChangedListener;
private DocumentChangedListener documentChangedListener;
private ISelection lastSelection;
private TreeViewer treeViewer;
public ClojureOutlinePage(IDocumentProvider documentProvider,
ClojureEditor editor) {
this.documentProvider = documentProvider;
this.editor = editor;
}
@Override
public void createControl(Composite parent) {
super.createControl(parent);
treeViewer = getTreeViewer();
treeViewer.setContentProvider(new ITreeContentProvider() {
public void inputChanged(Viewer viewer, Object oldInput,
Object newInput) {
}
public void dispose() {
}
public Object[] getElements(Object input) {
return ((List<?>) input).toArray();
}
public boolean hasChildren(Object arg0) {
return false;
}
public Object getParent(Object arg0) {
return null;
}
public Object[] getChildren(Object arg0) {
// TODO: handle children? Granularity, Bindings?
return null;
}
});
treeViewer.setLabelProvider(new DelegatingStyledCellLabelProvider(
new OutlineLabelProvider()));
treeViewer.addSelectionChangedListener(this);
treeViewer.setInput(forms);
treeSelectionChangedListener = new TreeSelectionChangedListener();
treeViewer.addSelectionChangedListener(treeSelectionChangedListener);
IPostSelectionProvider selectionProvider = (IPostSelectionProvider) editor
.getSelectionProvider();
editorSelectionChangedListener = new EditorSelectionChangedListener(
treeViewer);
selectionProvider.addPostSelectionChangedListener(editorSelectionChangedListener);
ISelection selection = selectionProvider.getSelection();
selectInOutline(selection);
registerToolbarActions();
}
private void registerToolbarActions() {
IActionBars actionBars = getSite().getActionBars();
IToolBarManager toolBarManager= actionBars.getToolBarManager();
toolBarManager.add(new LexicalSortingAction());
}
private class LexicalSortingAction extends Action {
public LexicalSortingAction() {
setText("Sort");
setImageDescriptor(CCWPlugin.getDefault().getImageRegistry().getDescriptor(CCWPlugin.SORT));
setToolTipText("Sort");
setDescription("Sort alphabetically");
setChecked(sort);
}
public void run() {
CCWPlugin.getDefault().getPreferenceStore().setValue("LexicalSortingAction.isChecked", sort = isChecked());
setInputInUiThread(forms);
}
}
private static Symbol symbol (Object o) {
return o instanceof Symbol ? (Symbol)o : null;
}
private static boolean isPrivate (List form) {
if (form.size() < 2) return false;
Symbol def = symbol(RT.first(form));
Symbol name = symbol(RT.second(form));
if (def == null || name == null) return false;
return def.getName().matches("(defn-|defvar-)") ||
(name.meta() != null && name.meta().valAt(Keyword.intern("private"), false).equals(Boolean.TRUE));
}
/**
* Find closest matching element to line
*
* @param toFind
* line to find
* @return
*/
protected StructuredSelection findClosest(int toFind) {
Object selected = null;
for (Object o : forms) {
if (o instanceof Obj) {
Obj obj = (Obj) o;
int lineNr = getLineNr(obj);
if (lineNr >= 0 && lineNr <= toFind) {
selected = obj;
}
}
}
if (selected != null) {
return new StructuredSelection(selected);
}
return StructuredSelection.EMPTY;
}
public void setInput(IEditorInput editorInput) {
document = documentProvider.getDocument(editorInput);
documentChangedListener = new DocumentChangedListener();
document.addDocumentListener(documentChangedListener);
refreshInput();
ISelection selection = editor.getSelectionProvider().getSelection();
selectInOutline(selection);
}
private void refreshInput() {
Job job = new Job("Outline browser tree refresh") {
@Override
protected IStatus run(IProgressMonitor monitor) {
String string = document.get();
LineNumberingPushbackReader pushbackReader = new LineNumberingPushbackReader(
new StringReader(string));
Object EOF = new Object();
ArrayList<List> input = new ArrayList<List>();
Object result = null;
while (true) {
try {
result = LispReader.read(pushbackReader, false, EOF, false);
if (result == EOF) break;
if (result instanceof List) input.add((List)result);
} catch (ReaderException e) {
// once a syntax error occurs (often because of a namespaced keyword)
// there's little chance that the rest of the data will be worthwhile...
CCWPlugin.logWarning(String.format("Failed to read file %s (%s)",
editor.getEditorInput().getName(), e.getMessage()));
break;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
ClojureOutlinePage.this.forms = input;
setInputInUiThread(input);
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return REFRESH_OUTLINE_JOB_FAMILY.equals(family);
}
};
job.setSystem(true);
Job.getJobManager().cancel(REFRESH_OUTLINE_JOB_FAMILY);
job.schedule(500);
}
protected void setInputInUiThread (List<List> forms) {
if (sort) {
List<List> sorted = new ArrayList(forms);
Collections.sort(sorted, new Comparator<List> () {
public int compare(List o1, List o2) {
Symbol s1 = symbol(RT.first(o1)),
s2 = symbol(RT.first(o2));
if (s1 == null && s2 == null) return 0; // mandatory for Comparator contract
if (s1 == null) return 1;
if (s2 == null) return -1;
if (s1.getName().equals("ns") && s2.getName().equals("ns")) {
// fall through to order ns decls correctly
} else {
if (s1.getName().equals("ns")) return -1;
if (s2.getName().equals("ns")) return 1;
}
if (isPrivate(o1) != isPrivate(o2)) {
return isPrivate(o1) ? -1 : 1;
}
s1 = symbol(RT.second(o1));
s2 = symbol(RT.second(o2));
if (s1 == null && s2 == null) return 0; // mandatory for Comparator contract
if (s1 == null) return 1;
if (s2 == null) return -1;
return s1.getName().compareToIgnoreCase(s2.getName());
}
});
forms = sorted;
}
final List<List> theForms = forms;
if (getControl() != null) {
getControl().getDisplay().asyncExec(new Runnable() {
public void run() {
if (getControl().isDisposed())
return;
TreeViewer treeViewer = getTreeViewer();
if (treeViewer != null) {
treeViewer.getTree().setRedraw(false);
treeViewer.setInput(theForms);
ISelection treeSelection = treeViewer.getSelection();
if (treeSelection == null || treeSelection.isEmpty()) {
selectInOutline(lastSelection);
}
treeViewer.getTree().setRedraw(true);
}
}
});
}
}
private void selectInEditor(ISelection selection) {
IStructuredSelection sel = (IStructuredSelection) selection;
if (sel.size() == 0)
return;
Obj obj = (Obj) sel.getFirstElement();
int lineNr = getLineNr(obj);
if (lineNr >= 0) {
ClojureCore.gotoEditorLine(editor, lineNr);
}
}
private int getLineNr(Obj obj) {
int lineNr = -1;
if (obj.meta() == null) {
return lineNr;
}
IMapEntry line = obj.meta().entryAt(KEYWORD_LINE);
if (line != null && line.val() instanceof Number) {
lineNr = ((Number) line.val()).intValue();
}
return lineNr;
}
private String safeToString(Object value) {
try {
return value == null ? NOT_AVAILABLE : value.toString();
} catch (Exception e) {
return NOT_AVAILABLE;
}
}
protected boolean isActivePart() {
IWorkbenchPart part = getSite().getPage().getActivePart();
return part != null && OUTLINE_VIEW_ID.equals(part.getSite().getId());
}
@Override
public void dispose() {
try {
if (document != null)
document.removeDocumentListener(documentChangedListener);
} catch (Throwable t) {}
try {
final TreeViewer viewer = getTreeViewer();
if (viewer != null)
viewer.removeSelectionChangedListener(this);
if (viewer != null)
viewer.removeSelectionChangedListener(treeSelectionChangedListener);
} catch (Throwable t) {}
try {
IPostSelectionProvider selectionProvider = (IPostSelectionProvider) editor
.getSelectionProvider();
if (selectionProvider != null)
selectionProvider
.removePostSelectionChangedListener(editorSelectionChangedListener);
} catch (Throwable t) {}
super.dispose();
}
private void selectInOutline(ISelection selection) {
TreeViewer viewer = getTreeViewer();
lastSelection = selection;
if (viewer != null && selection instanceof TextSelection) {
TextSelection textSelection = (TextSelection) selection;
int line = textSelection.getStartLine();
StructuredSelection newSelection = findClosest(line + 1);
ISelection oldSelection = viewer.getSelection();
if (!newSelection.equals(oldSelection)) {
viewer.setSelection(newSelection);
}
}
}
}