]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/winterwell.markdown/src/winterwell/markdown/editors/MarkdownEditor.java
migrated to svn revision 33108
[simantics/platform.git] / bundles / winterwell.markdown / src / winterwell / markdown / editors / MarkdownEditor.java
1 package winterwell.markdown.editors;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Map.Entry;
9
10 import org.eclipse.core.resources.IFile;
11 import org.eclipse.core.resources.IMarker;
12 import org.eclipse.core.resources.IResource;
13 import org.eclipse.core.resources.IWorkspace;
14 import org.eclipse.core.resources.IWorkspaceRoot;
15 import org.eclipse.core.resources.ResourcesPlugin;
16 import org.eclipse.core.runtime.CoreException;
17 import org.eclipse.core.runtime.IPath;
18 import org.eclipse.jface.preference.IPreferenceStore;
19 import org.eclipse.jface.text.DocumentEvent;
20 import org.eclipse.jface.text.IDocument;
21 import org.eclipse.jface.text.IDocumentListener;
22 import org.eclipse.jface.text.IRegion;
23 import org.eclipse.jface.text.Position;
24 import org.eclipse.jface.text.source.Annotation;
25 import org.eclipse.jface.text.source.ISourceViewer;
26 import org.eclipse.jface.text.source.IVerticalRuler;
27 import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
28 import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
29 import org.eclipse.jface.text.source.projection.ProjectionSupport;
30 import org.eclipse.jface.text.source.projection.ProjectionViewer;
31 import org.eclipse.jface.util.IPropertyChangeListener;
32 import org.eclipse.jface.util.PropertyChangeEvent;
33 import org.eclipse.swt.custom.StyledText;
34 import org.eclipse.swt.widgets.Composite;
35 import org.eclipse.ui.IEditorInput;
36 import org.eclipse.ui.IPathEditorInput;
37 import org.eclipse.ui.editors.text.TextEditor;
38 import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
39 import org.eclipse.ui.texteditor.IDocumentProvider;
40 import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
41 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
42
43 import winterwell.markdown.Activator;
44 import winterwell.markdown.pagemodel.MarkdownPage;
45 import winterwell.markdown.pagemodel.MarkdownPage.Header;
46 import winterwell.markdown.preferences.MarkdownPreferencePage;
47 import winterwell.markdown.views.MarkdownPreview;
48
49
50 /**
51  * Text editor with markdown support.
52  * @author Daniel Winterstein
53  */
54 public class MarkdownEditor extends TextEditor implements IDocumentListener 
55 {
56
57         /**
58          * Maximum length for a task tag message
59          */
60         private static final int MAX_TASK_MSG_LENGTH = 80;
61         private ColorManager colorManager;
62         private MarkdownContentOutlinePage fOutlinePage = null;
63         
64         IDocument oldDoc = null;
65         
66         private MarkdownPage page;
67         
68         
69         private boolean pageDirty = true;
70         
71         private ProjectionSupport projectionSupport;
72         private final IPreferenceStore pStore;
73         private IPropertyChangeListener prefChangeListener;
74         
75
76         public MarkdownEditor() {
77                 super();
78                 pStore = Activator.getDefault().getPreferenceStore();
79                 colorManager = new ColorManager();
80                 setSourceViewerConfiguration(new MDConfiguration(colorManager, getPreferenceStore()));
81         }
82
83         
84         @Override
85         public void createPartControl(Composite parent) {
86                 // Over-ride to add code-folding support 
87                 super.createPartControl(parent);
88                 if (getSourceViewer() instanceof ProjectionViewer) {
89                     ProjectionViewer viewer =(ProjectionViewer)getSourceViewer();
90                     projectionSupport = new ProjectionSupport(viewer,getAnnotationAccess(),getSharedColors());
91                     projectionSupport.install();
92                     //turn projection mode on
93                     viewer.doOperation(ProjectionViewer.TOGGLE);
94                 }
95         }
96         
97         /**
98          *  Returns the editor's source viewer. May return null before the editor's part has been created and after disposal.
99          */
100         public ISourceViewer getViewer() {
101                 return getSourceViewer();
102         }
103         
104         @Override
105         protected ISourceViewer createSourceViewer(Composite parent,
106                         IVerticalRuler ruler, int styles) {
107 //              if (true) return super.createSourceViewer(parent, ruler, styles);
108                 // Create with code-folding
109                 ISourceViewer viewer = new ProjectionViewer(parent, ruler,
110                                 getOverviewRuler(), isOverviewRulerVisible(), styles);
111                 // ensure decoration support has been created and configured.
112                 SourceViewerDecorationSupport decSupport = getSourceViewerDecorationSupport(viewer);
113 //              SourceViewer viewer = (SourceViewer) super.createSourceViewer(parent, ruler, styles);
114                 // Setup word-wrapping          
115                 final StyledText widget = viewer.getTextWidget();
116                 // Listen to pref changes
117                 prefChangeListener = new IPropertyChangeListener() {
118                         public void propertyChange(PropertyChangeEvent event) {
119                                 if (event.getProperty().equals(MarkdownPreferencePage.PREF_WORD_WRAP)) {
120                                         widget.setWordWrap(MarkdownPreferencePage.wordWrap());
121                                 }
122                         }                       
123                 };
124                 pStore.addPropertyChangeListener(prefChangeListener);
125                 // Switch on word-wrapping
126                 if (MarkdownPreferencePage.wordWrap()) {
127                         widget.setWordWrap(true);
128                 }               
129                 return viewer;  
130         }
131         
132         public void dispose() {
133                 if (pStore != null) {
134                         pStore.removePropertyChangeListener(prefChangeListener); 
135                 }
136                 colorManager.dispose();
137                 super.dispose();                
138         }
139         public void documentAboutToBeChanged(DocumentEvent event) {
140         }
141
142         public void documentChanged(DocumentEvent event) {
143                 pageDirty  = true;
144         }
145         
146         @Override
147         protected void doSetInput(IEditorInput input) throws CoreException {
148                 // Detach from old
149                 if (oldDoc!= null) {
150                         oldDoc.removeDocumentListener(this);
151                         if (doc2editor.get(oldDoc) == this) doc2editor.remove(oldDoc);
152                 }
153                 // Set
154                 super.doSetInput(input);                
155                 // Attach as a listener to new doc
156                 IDocument doc = getDocument();
157                 oldDoc = doc;
158                 if (doc==null) return;          
159                 doc.addDocumentListener(this);
160                 doc2editor.put(doc, this);
161                 // Initialise code folding
162                 haveRunFolding = false;
163                 updateSectionFoldingAnnotations(null);
164         }
165
166         @Override
167         protected void editorSaved() {
168                 if (MarkdownPreview.preview != null) {
169                         // Update the preview when the file is saved
170                         MarkdownPreview.preview.update();
171                 }
172         }
173
174         public Object getAdapter(Class required) {
175                 if (IContentOutlinePage.class.equals(required)) {
176                         if (fOutlinePage == null) {
177                                 fOutlinePage= new MarkdownContentOutlinePage(getDocumentProvider(), this);
178                                 if (getEditorInput() != null)
179                                         fOutlinePage.setInput(getEditorInput());
180                         }
181                         return fOutlinePage;
182                 }
183                 return super.getAdapter(required);
184         }
185         public IDocument getDocument() {
186                 IEditorInput input = getEditorInput();
187                 IDocumentProvider docProvider = getDocumentProvider();          
188                 return docProvider==null? null : docProvider.getDocument(input);
189         }
190         /**
191          * 
192          * @return The {@link MarkdownPage} for the document being edited, or null
193          * if unavailable.
194          */
195         public MarkdownPage getMarkdownPage() {
196                 if (pageDirty) updateMarkdownPage();
197                 return page;
198         }
199
200         public int getPrintColumns() {
201                  return getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN);         
202         }
203
204         /**
205          * @return The text of the editor's document, or null if unavailable.
206          */
207         public String getText() {
208                 IDocument doc = getDocument();
209                 return doc==null? null : doc.get();
210         }
211
212         private void updateMarkdownPage() {
213                 String text = getText();
214                 if (text==null) text="";
215                 page = new MarkdownPage(text);
216                 pageDirty = false;
217         }
218
219         void updateTaskTags(IRegion region) {
220         try {   
221                 boolean useTags = pStore.getBoolean(MarkdownPreferencePage.PREF_TASK_TAGS);
222                 if (!useTags) return;           
223                 // Get task tags
224 //              IPreferenceStore peuistore = EditorsUI.getPreferenceStore();
225 ////            IPreferenceStore pStore_jdt = org.eclipse.jdt.core.compiler.getDefault().getPreferenceStore();
226 //              String tagString = peuistore.getString("org.eclipse.jdt.core.compiler.taskTags");
227                 String tagString = pStore.getString(MarkdownPreferencePage.PREF_TASK_TAGS_DEFINED);
228                 List<String> tags = Arrays.asList(tagString.split(","));
229                 // Get resource for editor
230                 IFile docFile = getResource(this);
231                 // Get existing tasks
232                 IMarker[] taskMarkers = docFile.findMarkers(IMarker.TASK, true, IResource.DEPTH_INFINITE);
233                 List<IMarker> markers = new ArrayList<IMarker>(Arrays.asList(taskMarkers));
234 //              Collections.sort(markers, c) sort for efficiency
235                 // Find tags in doc
236                 List<String> text = getMarkdownPage().getText();
237                 for(int i=1; i<=text.size(); i++) {
238                         String line = text.get(i-1); // wierd off-by-one bug
239                         for (String tag : tags) {
240                                 tag = tag.trim();
241                                 int tagIndex = line.indexOf(tag);
242                                 if (tagIndex == -1) continue;
243                                 IMarker exists = updateTaskTags2_checkExisting(i, tagIndex, line, markers);
244                                 if (exists!=null) {
245                                         markers.remove(exists);
246                                         continue;
247                                 }
248                                 IMarker marker = docFile.createMarker(IMarker.TASK);                        
249                                 //Once we have a marker object, we can set its attributes
250                                 marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL);
251                                 String msg = line.substring(line.indexOf(tag), Math.min(tagIndex+MAX_TASK_MSG_LENGTH, line.length()-1));
252                                 marker.setAttribute(IMarker.MESSAGE, msg);
253                                 marker.setAttribute(IMarker.LINE_NUMBER, i);                                                                    
254                         }
255                 }
256                 // Remove old markers
257                 for (IMarker m : markers) {
258                         try {
259                                 m.delete();             
260                         } catch (Exception ex) {
261                         //
262                         }                       
263                 }
264         } catch (Exception ex) {
265                 // 
266         }
267         }
268
269         /**
270          * Find an existing marker, if there is one.
271          * @param i
272          * @param tagIndex
273          * @param line
274          * @param markers
275          * @return 
276          */
277         private IMarker updateTaskTags2_checkExisting(int i, int tagIndex,
278                         String line, List<IMarker> markers) {           
279                 String tagMessage = line.substring(tagIndex).trim();
280                 for (IMarker marker : markers) {
281                         try {
282                             Integer lineNum = (Integer) marker.getAttribute(IMarker.LINE_NUMBER);
283                             if (i != lineNum) continue;
284                             String txt = ((String) marker.getAttribute(IMarker.MESSAGE)).trim();
285                             if (tagMessage.equals(txt)) return marker;
286                         } catch (Exception ex) {
287                                 // Ignore
288                         }
289                 }
290                 return null;
291         }
292
293
294         private IFile getResource(MarkdownEditor markdownEditor) {
295                 IPathEditorInput input = (IPathEditorInput) getEditorInput();
296                 IPath path = input.getPath();           
297                 IWorkspace workspace = ResourcesPlugin.getWorkspace();
298                 IWorkspaceRoot root = workspace.getRoot();
299                 IFile[] files = root.findFilesForLocation(path);
300                 if (files.length != 1) return null;
301                 IFile docFile = files[0];               
302                 return docFile;
303         }
304
305
306         /**
307          * @param doc
308          * @return
309          */
310         public static MarkdownEditor getEditor(IDocument doc) {
311                 return doc2editor.get(doc);
312         }
313
314         private static final Map<IDocument, MarkdownEditor> doc2editor = new HashMap<IDocument, MarkdownEditor>();
315
316
317         /**
318          * @param region 
319          * 
320          */
321         public void updatePage(IRegion region) {
322 //              if (!pageDirty) return;
323                 updateTaskTags(region);
324                 updateSectionFoldingAnnotations(region);
325         }
326         
327         
328         private static final Annotation[] ANNOTATION_ARRAY = new Annotation[0];
329
330         private static final Position[] POSITION_ARRAY = new Position[0];
331         
332         private boolean haveRunFolding = false;
333         private Map<Annotation, Position> oldAnnotations = new HashMap<Annotation, Position>(0);
334
335         /**
336          * @param region can be null
337          */
338         private void updateSectionFoldingAnnotations(IRegion region) {
339                 if (!haveRunFolding) region = null; // Do the whole doc
340                 if ( ! (getSourceViewer() instanceof ProjectionViewer)) return;
341                 ProjectionViewer viewer = ((ProjectionViewer)getSourceViewer());
342                 MarkdownPage mPage = getMarkdownPage();
343                 List<Header> headers = mPage.getHeadings(null);
344                 // this will hold the new annotations along
345                 // with their corresponding positions
346                 Map<Annotation, Position> annotations = new HashMap<Annotation, Position>();
347                 IDocument doc = getDocument();
348                 updateSectionFoldingAnnotations2(doc, headers, annotations, doc.getLength());
349                 // Filter existing ones
350                 Position[] newValues = annotations.values().toArray(POSITION_ARRAY);            
351                 List<Annotation> deletedAnnotations = new ArrayList<Annotation>();
352                 for(Entry<Annotation, Position> ae : oldAnnotations.entrySet()) {
353                         Position oldp = ae.getValue();
354                         boolean stillExists = false;
355                         for (Position newp : newValues) {
356                                 if (oldp.equals(newp)) {
357                                         annotations.remove(newp);
358                                         stillExists = true;
359                                         break;
360                                 }
361                         }
362                         if (!stillExists && intersectsRegion(oldp, region)) {
363                                 deletedAnnotations.add(ae.getKey());
364                         }
365                 }
366                 // Filter out-of-region ones
367                 for(Annotation a : annotations.keySet().toArray(ANNOTATION_ARRAY)) {
368                         Position p = annotations.get(a);
369                         if (!intersectsRegion(p , region)) annotations.remove(a);
370                 }
371                 // Adjust the page
372             ProjectionAnnotationModel annotationModel = viewer.getProjectionAnnotationModel();
373             if (annotationModel==null) return;
374                 annotationModel.modifyAnnotations(deletedAnnotations.toArray(ANNOTATION_ARRAY), annotations, null);
375                 // Remember old values
376                 oldAnnotations.putAll(annotations);
377                 for (Annotation a : deletedAnnotations) {
378                         oldAnnotations.remove(a);       
379                 }               
380                 haveRunFolding = true;
381         }
382
383
384         /**
385          * @param p
386          * @param region
387          * @return true if p overlaps with region, or if region is null
388          */
389         private boolean intersectsRegion(Position p, IRegion region) {
390                 if (region==null) return true;
391                 if (p.offset > region.getOffset()+region.getLength()) return false;
392                 if (p.offset+p.length < region.getOffset()) return false;
393                 return true;
394         }
395
396
397         /**
398          * Calculate where to fold, sticking the info into newAnnotations
399          * @param doc 
400          * @param headers
401          * @param newAnnotations
402          * @param endParent
403          */
404         private void updateSectionFoldingAnnotations2(IDocument doc, List<Header> headers,
405                         Map<Annotation, Position> newAnnotations, int endParent) {
406                 for (int i=0; i<headers.size(); i++) {
407                         Header header = headers.get(i);
408                         ProjectionAnnotation annotation = new ProjectionAnnotation();
409                         try {
410                                 int line = header.getLineNumber();
411                                 int start = doc.getLineOffset(line);
412                                 int end = (i==headers.size()-1)? endParent
413                                                 : doc.getLineOffset(headers.get(i+1).getLineNumber());
414                                 Position position = new Position(start, end-start);
415                                 newAnnotations.put(annotation, position);
416                                 // Recurse
417                                 List<Header> subHeaders = header.getSubHeaders();
418                                 if (subHeaders.size() > 0) {
419                                         updateSectionFoldingAnnotations2(doc, subHeaders, newAnnotations, end);
420                                 }
421                         } catch (Exception ex) {
422                                 System.out.println(ex);
423                         }                       
424                 }               
425         }
426
427
428 }
429
430
431
432 /*
433
434   <?xml version="1.0" encoding="UTF-8" ?> 
435 - <templates>
436   <template name="updateSWT" description="Performs an update to an SWT control in the SWT thread" context="java" enabled="true">${control}.getDisplay().syncExec(new Runnable() { public void run() { ${control}.${cursor} } });</template> 
437   <template name="findView" description="Find a workbench view by ID" context="java" enabled="true" deleted="false">${viewType} ${view} = null; IWorkbenchWindow ${window} = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (${window} != null) { IWorkbenchPage ${activePage} = ${window}.getActivePage(); if (${activePage} != null) ${view} = (${viewType}) ${activePage}.findView("${viewID}"); } if (${view} != null) { ${cursor}//${todo}: Add operations for opened view }</template> 
438   <template name="getActiveEditor" description="Retrieves the currently active editor in the active page" context="java" enabled="true" deleted="false">IEditorPart ${editor} = null; IWorkbenchWindow ${window} = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (${window} != null) { IWorkbenchPage ${activePage} = ${window}.getActivePage(); if (${activePage} != null) ${editor} = ${activePage}.getActiveEditor(); } if (${editor} != null) { ${cursor}//${todo}: Add operations for active editor }</template>
439    IEditorPart editor = null; 
440                 IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); 
441                 if (window != null) { 
442                         IWorkbenchPage activePage = window.getActivePage(); 
443                         if (activePage != null) editor = activePage.getActiveEditor(); 
444                 } 
445                 if (editor != null) { 
446                         // todo: Add operations for active editor                       
447                 }
448                 
449   <template name="openDialog" description="Creates and opens a JFace dialog" context="java" enabled="true" deleted="false">${dialogType} ${dialog} = new ${dialogType}(${cursor}); ${dialog}.create(); //${todo}: Complete dialog creation if (${dialog}.open() == Dialog.OK) { //${todo}: Perform actions on success };</template> 
450   <template name="scanExtensionRegistry" description="Scans the extension registry for extensions of a given extension point" context="java" enabled="true" deleted="false">IExtensionRegistry ${registry} = Platform.getExtensionRegistry(); IExtensionPoint ${point} = ${registry}.getExtensionPoint(${pluginId}, ${expointId}); IExtension[] ${extensions} = ${point}.getExtensions(); for (int ${index} = 0; ${index} < ${extensions}.length; ${index}++) { IConfigurationElement[] ${elements} = ${extensions}[${index}].getConfigurationElements(); for (int ${index2} = 0; ${index2} < ${elements}.length; ${index2}++) { IConfigurationElement ${element} = ${elements}[${index2}]; String ${attValue} = ${element}.getAttribute(${attName}); ${cursor}//${todo}: Implement processing for configuration element } }</template> 
451   <template name="showView" description="finds a workbench view by ID and shows it" context="java" enabled="true" deleted="false">${viewType} ${view} = null; IWorkbenchWindow ${window} = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (${window} != null) { IWorkbenchPage ${activePage} = ${window}.getActivePage(); if (${activePage} != null) try { ${view} = (${viewType}) ${activePage}.showView("${viewID}"); } catch (${Exception} e) { // ${todo}: handle exception } } if (${view} != null) { ${cursor} }</template> 
452   <template name="signalError" description="Shows an error message in the editors status line" context="java" enabled="true" deleted="false">IEditorActionBarContributor ${contributor} = ${editor}.getEditorSite().getActionBarContributor(); if (${contributor} instanceof EditorActionBarContributor) { IActionBars ${actionBars} = ((EditorActionBarContributor) ${contributor}).getActionBars(); if (${actionBars} != null) { IStatusLineManager ${manager} = ${actionBars}.getStatusLineManager(); if (${manager} != null) ${manager}.setErrorMessage(msg); } }</template> 
453   </templates>
454
455 */