--- /dev/null
+/**\r
+ * Copyright winterwell Mathematics Ltd.\r
+ * @author Daniel Winterstein\r
+ * 11 Jan 2007\r
+ */\r
+package winterwell.markdown.editors;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.HashSet;\r
+import java.util.List;\r
+\r
+import org.eclipse.jface.action.Action;\r
+import org.eclipse.jface.action.IAction;\r
+import org.eclipse.jface.action.IToolBarManager;\r
+import org.eclipse.jface.resource.ImageDescriptor;\r
+import org.eclipse.jface.text.BadLocationException;\r
+import org.eclipse.jface.text.DocumentEvent;\r
+import org.eclipse.jface.text.IDocument;\r
+import org.eclipse.jface.text.IDocumentListener;\r
+import org.eclipse.jface.text.IRegion;\r
+import org.eclipse.jface.text.Region;\r
+import org.eclipse.jface.viewers.ISelection;\r
+import org.eclipse.jface.viewers.IStructuredSelection;\r
+import org.eclipse.jface.viewers.ITreeContentProvider;\r
+import org.eclipse.jface.viewers.LabelProvider;\r
+import org.eclipse.jface.viewers.SelectionChangedEvent;\r
+import org.eclipse.jface.viewers.StructuredSelection;\r
+import org.eclipse.jface.viewers.TreeViewer;\r
+import org.eclipse.jface.viewers.Viewer;\r
+import org.eclipse.swt.SWT;\r
+import org.eclipse.swt.events.KeyEvent;\r
+import org.eclipse.swt.events.KeyListener;\r
+import org.eclipse.swt.widgets.Composite;\r
+import org.eclipse.swt.widgets.Control;\r
+import org.eclipse.ui.IActionBars;\r
+import org.eclipse.ui.part.IPageSite;\r
+import org.eclipse.ui.texteditor.IDocumentProvider;\r
+import org.eclipse.ui.views.contentoutline.ContentOutlinePage;\r
+\r
+import winterwell.markdown.pagemodel.MarkdownPage;\r
+import winterwell.markdown.pagemodel.MarkdownPage.Header;\r
+import winterwell.markdown.pagemodel.MarkdownPage.KLineType;\r
+import winterwell.utils.StrUtils;\r
+import winterwell.utils.Utils;\r
+import winterwell.utils.web.WebUtils;\r
+\r
+/**\r
+ *\r
+ *\r
+ * @author Daniel Winterstein\r
+ */\r
+public final class MarkdownContentOutlinePage extends ContentOutlinePage {\r
+\r
+ /**\r
+ *\r
+ *\r
+ * @author Daniel Winterstein\r
+ */\r
+ public final class ContentProvider implements ITreeContentProvider,\r
+ IDocumentListener {\r
+\r
+ // protected final static String SEGMENTS= "__md_segments";\r
+ // //$NON-NLS-1$\r
+ // protected IPositionUpdater fPositionUpdater= new\r
+ // DefaultPositionUpdater(SEGMENTS);\r
+ private MarkdownPage fContent;\r
+ // protected List fContent= new ArrayList(10);\r
+ private MarkdownEditor fTextEditor;\r
+\r
+ private void parse() {\r
+ fContent = fTextEditor.getMarkdownPage();\r
+ }\r
+\r
+ /*\r
+ * @see IContentProvider#inputChanged(Viewer, Object, Object)\r
+ */\r
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {\r
+ // Detach from old\r
+ if (oldInput != null) {\r
+ IDocument document = fDocumentProvider.getDocument(oldInput);\r
+ if (document != null) {\r
+ document.removeDocumentListener(this);\r
+ }\r
+ }\r
+ fContent = null;\r
+ // Attach to new\r
+ if (newInput == null)\r
+ return;\r
+ IDocument document = fDocumentProvider.getDocument(newInput);\r
+ if (document == null)\r
+ return;\r
+ fTextEditor = MarkdownEditor.getEditor(document);\r
+ document.addDocumentListener(this);\r
+ parse();\r
+ }\r
+\r
+ /*\r
+ * @see IContentProvider#dispose\r
+ */\r
+ public void dispose() {\r
+ fContent = null;\r
+ }\r
+\r
+ /*\r
+ * @see IContentProvider#isDeleted(Object)\r
+ */\r
+ public boolean isDeleted(Object element) {\r
+ return false;\r
+ }\r
+\r
+ /*\r
+ * @see IStructuredContentProvider#getElements(Object)\r
+ */\r
+ public Object[] getElements(Object element) {\r
+ return fContent.getHeadings(null).toArray();\r
+ }\r
+\r
+ /*\r
+ * @see ITreeContentProvider#hasChildren(Object)\r
+ */\r
+ public boolean hasChildren(Object element) {\r
+ if (element == fInput) {\r
+ return true;\r
+ }\r
+ if (element instanceof MarkdownPage.Header) {\r
+ MarkdownPage.Header header = (MarkdownPage.Header) element;\r
+ return header.getSubHeaders().size() > 0;\r
+ }\r
+ ;\r
+ return false;\r
+ }\r
+\r
+ /*\r
+ * @see ITreeContentProvider#getParent(Object)\r
+ */\r
+ public Object getParent(Object element) {\r
+ if (!(element instanceof MarkdownPage.Header))\r
+ return null;\r
+ return ((MarkdownPage.Header) element).getParent();\r
+ }\r
+\r
+ /*\r
+ * @see ITreeContentProvider#getChildren(Object)\r
+ */\r
+ public Object[] getChildren(Object element) {\r
+ if (element == fInput) {\r
+ return fContent.getHeadings(null).toArray();\r
+ }\r
+ if (!(element instanceof MarkdownPage.Header))\r
+ return null;\r
+ return ((MarkdownPage.Header) element).getSubHeaders().toArray();\r
+ }\r
+\r
+ public void documentAboutToBeChanged(DocumentEvent event) {\r
+ // nothing\r
+ }\r
+\r
+ public void documentChanged(DocumentEvent event) {\r
+ parse();\r
+ update();\r
+ }\r
+ }\r
+\r
+ private Object fInput = null;\r
+ private final IDocumentProvider fDocumentProvider;\r
+ private final MarkdownEditor fTextEditor;\r
+ protected boolean showWordCounts;\r
+ private List<Header> selectedHeaders;\r
+\r
+ /**\r
+ * @param documentProvider\r
+ * @param mdEditor\r
+ */\r
+ public MarkdownContentOutlinePage(IDocumentProvider documentProvider,\r
+ MarkdownEditor mdEditor) {\r
+ fDocumentProvider = documentProvider;\r
+ fTextEditor = mdEditor;\r
+ }\r
+\r
+ /*\r
+ * (non-Javadoc) Method declared on ContentOutlinePage\r
+ */\r
+ @Override\r
+ public void createControl(Composite parent) {\r
+ super.createControl(parent);\r
+ TreeViewer viewer = getTreeViewer();\r
+ viewer.setContentProvider(new ContentProvider());\r
+ // Add word count annotations\r
+ viewer.setLabelProvider(new LabelProvider() {\r
+ @Override\r
+ public String getText(Object element) {\r
+ if (!(element instanceof MarkdownPage.Header))\r
+ return super.getText(element);\r
+ Header header = ((MarkdownPage.Header) element);\r
+ String hText = header.toString();\r
+ if (!showWordCounts)\r
+ return hText;\r
+ IRegion region = getRegion(header);\r
+ String text;\r
+ try {\r
+ text = fTextEditor.getDocument().get(region.getOffset(),\r
+ region.getLength());\r
+ text = WebUtils.stripTags(text);\r
+ text = text.replaceAll("#", "").trim();\r
+ assert text.startsWith(hText);\r
+ text = text.substring(hText.length());\r
+ int wc = StrUtils.wordCount(text);\r
+ return hText + " (" + wc + ":" + text.length() + ")";\r
+ } catch (BadLocationException e) {\r
+ return hText;\r
+ }\r
+ }\r
+ });\r
+ viewer.addSelectionChangedListener(this);\r
+\r
+ if (fInput != null)\r
+ viewer.setInput(fInput);\r
+\r
+ // Buttons\r
+ IPageSite site = getSite();\r
+ IActionBars bars = site.getActionBars();\r
+ IToolBarManager toolbar = bars.getToolBarManager();\r
+ // Word count action\r
+ Action action = new Action("123", IAction.AS_CHECK_BOX) {\r
+ @Override\r
+ public void run() {\r
+ showWordCounts = isChecked();\r
+ update();\r
+ }\r
+ };\r
+ action.setToolTipText("Show/hide section word:character counts");\r
+ toolbar.add(action);\r
+ // +/- actions\r
+ action = new Action("<") {\r
+ @Override\r
+ public void run() {\r
+ doPromoteDemote(-1);\r
+ }\r
+ };\r
+ action.setToolTipText("Promote the selected section\n -- move it up a level.");\r
+ toolbar.add(action);\r
+ //\r
+ action = new Action(">") {\r
+ @Override\r
+ public void run() {\r
+ doPromoteDemote(1);\r
+ }\r
+ };\r
+ action.setToolTipText("Demote the selected section\n -- move it down a level.");\r
+ toolbar.add(action);\r
+ // up/down actions\r
+ action = new Action("/\\") {\r
+ @Override\r
+ public void run() {\r
+ try {\r
+ doMove(-1);\r
+ } catch (BadLocationException e) {\r
+ throw Utils.runtime(e);\r
+ }\r
+ }\r
+ };\r
+ action.setToolTipText("Move the selected section earlier");\r
+ toolbar.add(action);\r
+ //\r
+ action = new Action("\\/") {\r
+ @Override\r
+ public void run() {\r
+ try {\r
+ doMove(1);\r
+ } catch (BadLocationException e) {\r
+ throw Utils.runtime(e);\r
+ }\r
+ }\r
+ };\r
+ action.setToolTipText("Move the selected section later");\r
+ toolbar.add(action);\r
+ // Collapse\r
+ ImageDescriptor id = ImageDescriptor.createFromFile(getClass(), "collapseall.gif");\r
+ action = new Action("collapse", id) {\r
+ @Override\r
+ public void run() {\r
+ doCollapseAll();\r
+ }\r
+ };\r
+ action.setImageDescriptor(id);\r
+ action.setToolTipText("Collapse outline tree");\r
+ toolbar.add(action);\r
+ // Sync\r
+ id = ImageDescriptor.createFromFile(getClass(), "synced.gif");\r
+ action = new Action("sync") {\r
+ @Override\r
+ public void run() {\r
+ try {\r
+ doSyncToEditor();\r
+ } catch (BadLocationException e) {\r
+ throw Utils.runtime(e);\r
+ }\r
+ }\r
+ };\r
+ action.setImageDescriptor(id);\r
+ action.setToolTipText("Link with editor");\r
+ toolbar.add(action);\r
+ // Add edit ability\r
+ viewer.getControl().addKeyListener(new KeyListener() {\r
+ public void keyPressed(KeyEvent e) {\r
+ if (e.keyCode==SWT.F2) {\r
+ doEditHeader();\r
+ }\r
+ }\r
+ public void keyReleased(KeyEvent e) {\r
+ //\r
+ }\r
+ });\r
+ }\r
+\r
+ /**\r
+ * @throws BadLocationException\r
+ *\r
+ */\r
+ protected void doSyncToEditor() throws BadLocationException {\r
+ TreeViewer viewer = getTreeViewer();\r
+ if (viewer == null) return;\r
+ // Get header\r
+ MarkdownPage page = fTextEditor.getMarkdownPage();\r
+ int caretOffset = fTextEditor.getViewer().getTextWidget().getCaretOffset();\r
+ IDocument doc = fTextEditor.getDocument();\r
+ int line = doc.getLineOfOffset(caretOffset);\r
+ List<KLineType> lineTypes = page.getLineTypes();\r
+ for(; line>-1; line--) {\r
+ KLineType lt = lineTypes.get(line);\r
+ if (lt.toString().startsWith("H")) break;\r
+ }\r
+ if (line<0) return;\r
+ Header header = (Header) page.getPageObject(line);\r
+ // Set\r
+ IStructuredSelection selection = new StructuredSelection(header);\r
+ viewer.setSelection(selection , true);\r
+ }\r
+\r
+ void doEditHeader() {\r
+ TreeViewer viewer = getTreeViewer();\r
+ viewer.editElement(selectedHeaders.get(0), 0);\r
+ }\r
+\r
+ protected void doCollapseAll() {\r
+ TreeViewer viewer = getTreeViewer();\r
+ if (viewer == null) return;\r
+// Control control = viewer.getControl();\r
+// if (control != null && !control.isDisposed()) {\r
+// control.setRedraw(false);\r
+ viewer.collapseAll();\r
+// control.setRedraw(true);\r
+// }\r
+ }\r
+\r
+ /**\r
+ * Move the selected sections up/down\r
+ * @param i 1 or -1. 1==move later, -1=earlier\r
+ * @throws BadLocationException\r
+ */\r
+ protected void doMove(int i) throws BadLocationException {\r
+ assert i==1 || i==-1;\r
+ if (selectedHeaders == null || selectedHeaders.size() == 0)\r
+ return;\r
+ // Get text region to move\r
+ MarkdownPage.Header first = selectedHeaders.get(0);\r
+ MarkdownPage.Header last = selectedHeaders.get(selectedHeaders.size()-1);\r
+ int start = fTextEditor.getDocument().getLineOffset(\r
+ first.getLineNumber());\r
+ IRegion r = getRegion(last);\r
+ int end = r.getOffset() + r.getLength();\r
+ int length = end - start;\r
+ // Get new insertion point\r
+ int insert;\r
+ if (i==1) {\r
+ Header nextSection = last.getNext();\r
+ if (nextSection==null) return;\r
+ IRegion nr = getRegion(nextSection);\r
+ insert = nr.getOffset()+nr.getLength();\r
+ } else {\r
+ Header prevSection = first.getPrevious();\r
+ if (prevSection==null) return;\r
+ IRegion nr = getRegion(prevSection);\r
+ insert = nr.getOffset();\r
+ }\r
+ // Get text\r
+ String text = fTextEditor.getDocument().get();\r
+ // Move text\r
+ String section = text.substring(start, end);\r
+ String pre, post;\r
+ if (i==1) {\r
+ pre = text.substring(0, start) + text.substring(end, insert);\r
+ post = text.substring(insert);\r
+ } else {\r
+ pre = text.substring(0, insert);\r
+ post = text.substring(insert,start)+text.substring(end);\r
+ }\r
+ text = pre + section + post;\r
+ assert text.length() == fTextEditor.getDocument().get().length() :\r
+ text.length()-fTextEditor.getDocument().get().length()+" chars gained/lost";\r
+ // Update doc\r
+ fTextEditor.getDocument().set(text);\r
+ }\r
+\r
+ /**\r
+ * Does not support -------- / ========= underlining, only # headers\r
+ * @param upDown 1 for demote (e.g. h2 -> h3), -1 for promote (e.g. h2 -> h1)\r
+ */\r
+ protected void doPromoteDemote(int upDown) {\r
+ assert upDown==1 || upDown==-1;\r
+ if (selectedHeaders == null || selectedHeaders.size() == 0)\r
+ return;\r
+ HashSet<Header> toAdjust = new HashSet<Header>(selectedHeaders);\r
+ HashSet<Header> adjusted = new HashSet<Header>();\r
+ // Adjust\r
+ MarkdownPage mdPage = fTextEditor.getMarkdownPage();\r
+ List<String> lines = new ArrayList<String>(mdPage.getText());\r
+ while(toAdjust.size() != 0) {\r
+ Header h = toAdjust.iterator().next();\r
+ toAdjust.remove(h);\r
+ adjusted.add(h);\r
+ String line = lines.get(h.getLineNumber());\r
+ if (upDown==-1) {\r
+ if (h.getLevel() == 1) return; // Level 1; can't promote\r
+ if (line.startsWith("##")) line = line.substring(1);\r
+ else {\r
+ return; // TODO support for ------ / ========\r
+ }\r
+ } else line = "#" + line;\r
+ int ln = h.getLineNumber();\r
+ lines.set(ln, line);\r
+ // kids\r
+ ArrayList<Header> kids = new ArrayList<Header>(h.getSubHeaders());\r
+ for (Header header : kids) {\r
+ if ( ! adjusted.contains(header)) toAdjust.add(header);\r
+ }\r
+ }\r
+ // Set\r
+ StringBuilder sb = new StringBuilder();\r
+ for (String line : lines) {\r
+ sb.append(line);\r
+ }\r
+ fTextEditor.getDocument().set(sb.toString());\r
+ }\r
+\r
+ /**\r
+ * The region of text for this header. This includes the header itself.\r
+ * @param header\r
+ * @return\r
+ * @throws BadLocationException\r
+ */\r
+ protected IRegion getRegion(Header header) {\r
+ try {\r
+ IDocument doc = fTextEditor.getDocument();\r
+ // Line numbers\r
+ int start = header.getLineNumber();\r
+ Header next = header.getNext();\r
+ int end;\r
+ if (next != null) {\r
+ end = next.getLineNumber() - 1;\r
+ } else {\r
+ end = doc.getNumberOfLines() - 1;\r
+ }\r
+ int offset = doc.getLineOffset(start);\r
+ IRegion ei = doc.getLineInformation(end);\r
+ int length = ei.getOffset() + ei.getLength() - offset;\r
+ return new Region(offset, length);\r
+ } catch (BadLocationException ex) {\r
+ throw Utils.runtime(ex);\r
+ }\r
+ }\r
+\r
+ /*\r
+ * (non-Javadoc) Method declared on ContentOutlinePage\r
+ */\r
+ @Override\r
+ public void selectionChanged(SelectionChangedEvent event) {\r
+ super.selectionChanged(event);\r
+ selectedHeaders = null;\r
+ ISelection selection = event.getSelection();\r
+ if (selection.isEmpty())\r
+ return;\r
+ if (!(selection instanceof IStructuredSelection))\r
+ return;\r
+ try {\r
+ IStructuredSelection strucSel = (IStructuredSelection) selection;\r
+ Object[] sections = strucSel.toArray();\r
+ selectedHeaders = (List) Arrays.asList(sections);\r
+ MarkdownPage.Header first = (Header) sections[0];\r
+ MarkdownPage.Header last = (Header) sections[sections.length - 1];\r
+ int start = fTextEditor.getDocument().getLineOffset(\r
+ first.getLineNumber());\r
+ int length;\r
+ if (first == last) {\r
+ length = fTextEditor.getDocument().getLineLength(\r
+ first.getLineNumber());\r
+ } else {\r
+ IRegion r = getRegion(last);\r
+ int end = r.getOffset() + r.getLength();\r
+ length = end - start;\r
+ }\r
+ fTextEditor.setHighlightRange(start, length, true);\r
+ } catch (Exception x) {\r
+ System.out.println(x.getStackTrace());\r
+ fTextEditor.resetHighlightRange();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Sets the input of the outline page\r
+ *\r
+ * @param input\r
+ * the input of this outline page\r
+ */\r
+ public void setInput(Object input) {\r
+ fInput = input;\r
+ update();\r
+ }\r
+\r
+ /**\r
+ * Updates the outline page.\r
+ */\r
+ public void update() {\r
+ TreeViewer viewer = getTreeViewer();\r
+\r
+ if (viewer != null) {\r
+ Control control = viewer.getControl();\r
+ if (control != null && !control.isDisposed()) {\r
+ control.setRedraw(false);\r
+ viewer.setInput(fInput);\r
+ viewer.expandAll();\r
+ control.setRedraw(true);\r
+ }\r
+ }\r
+ }\r
+\r
+}\r