Java tutorial
/******************************************************************************* * 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); } } } }