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