View Javadoc

1   /*
2    * $Id: OWLDocumentPartitioner.java,v 1.39 2005/07/11 20:31:09 jlerner Exp $
3    *
4    * Copyright (c) 1999-2004, BBN Technologies, LLC.
5    * All rights reserved.
6    * http://www.daml.org/legal/opensource/bbn_license.html
7    */
8   
9   package com.bbn.swede.editor;
10  
11  import java.util.Collections;
12  import java.util.Iterator;
13  import java.util.LinkedList;
14  
15  import org.eclipse.jface.text.BadLocationException;
16  import org.eclipse.jface.text.DocumentEvent;
17  import org.eclipse.jface.text.IDocument;
18  import org.eclipse.jface.text.IDocumentListener;
19  import org.eclipse.jface.text.IRegion;
20  import org.eclipse.jface.text.ITypedRegion;
21  import org.eclipse.jface.text.rules.FastPartitioner;
22  import org.eclipse.jface.text.source.SourceViewer;
23  
24  import com.bbn.swede.core.OWLCore;
25  import com.bbn.swede.core.dom.AttributeNode;
26  import com.bbn.swede.core.dom.DisplaceableRegion;
27  import com.bbn.swede.core.dom.DocumentRoot;
28  import com.bbn.swede.core.dom.IOASTChangeListener;
29  import com.bbn.swede.core.dom.Literal;
30  import com.bbn.swede.core.dom.OAST;
31  import com.bbn.swede.core.dom.OASTEvent;
32  import com.bbn.swede.core.dom.OASTNode;
33  import com.bbn.swede.core.dom.TagNode;
34  import com.bbn.swede.core.dom.UnparseableNode;
35  import com.bbn.swede.core.dom.OASTEvent.OASTNodeRef;
36  
37  /***
38   * Partitions OWL documents with a lightweight rules-based scanner. Documents
39   * are divided into begin tag, end tag, and "everything else" partitions, with
40   * finer granularity and syntax highlighting controlled by word scanners.
41   * 
42   * @author jlerner
43   * @see com.bbn.swede.editor.rules
44   */
45  public class OWLDocumentPartitioner extends FastPartitioner implements
46     IOASTChangeListener
47  {
48     /***
49      * Updates the document's OAST to reflect changes to the document text.
50      * Text changes are fed to OASTUpdater as events by the 
51      * OWLDelayedSyntaxChecker.
52      * @author jlerner
53      */
54     protected class OASTUpdater implements IDelayedDocumentEventListener, IDocumentListener
55     {
56        private OAST _tree;
57        private OWLDelayedSyntaxChecker _syntaxChecker;
58        private boolean _bCollectEvents = true;
59        private IDocument _document;
60        
61        /***
62         * Attaches the updater to a document.  The updater will register itself
63         * as a document event listener in order to handle delayed OAST updating.
64         * @param document The document
65         */
66        public void connect(IDocument document)
67        {
68           disconnect();
69           _document = document;
70           _document.addDocumentListener(this);
71        }
72        
73        /***
74         * Detaches the updater from its document.  The updater unregisters itself
75         * as a document event listener and ceases to perform delayed OAST 
76         * updates.
77         */
78        public void disconnect()
79        {
80           if (_document != null)
81           {
82              _document.removeDocumentListener(this);
83           }
84        }
85        
86        /***
87         * Creates an updater for an OAST.
88         * @param tree The OAST
89         */
90        public OASTUpdater(OAST tree)
91        {
92           _tree = tree;
93           _syntaxChecker = new OWLDelayedSyntaxChecker();
94           _syntaxChecker.addDelayedDocumentEventListener(this);
95        }
96        
97        /***
98         * Immediately halts delayed event processing.  If delayed event
99         * processing is already stopped, this method has no effect.  Event
100        * processing must be stopped when updating the document's text to 
101        * reflect changes made using the OAST API, or duplicate nodes will
102        * be inserted and the tree will go out of synch with the document.
103        * Event collection must be restarted before formatting of any inserted
104        * text to insure proper updating of offsets and lengths.
105        * @see #startDelayedEventCollection()
106        */
107       public void stopDelayedEventCollection()
108       {
109          _bCollectEvents = false;
110          force();
111       }
112       
113       /***
114        * Immediately resumes delayed event processing.  If delayed event
115        * processing is already active, this method has no effect.
116        * @see #stopDelayedEventCollection()
117        */
118       public void startDelayedEventCollection()
119       {
120          _bCollectEvents = true;
121       }
122       
123 //      public boolean isHandlingEvent()
124 //      {
125 //         return _bHandlingEvent;
126 //      }
127       
128       /***
129        * Determines, based on its type, if an OASTNode must be reparsed in response
130        * to adjacent document changes.
131        * 
132        * @param node The node to check for a reparse requirement
133        * @return <code>true</code> if the node must be reparsed in response to
134        *         adjacent changes, <code>false</code> if not
135        */
136       private boolean mustReparse(OASTNode node)
137       {
138          return (node instanceof Literal || node instanceof UnparseableNode);
139       }
140 
141       /***
142        * Retrieves the beginning of a node's text from the document.
143        * 
144        * @param node Node whose beginning text is desired
145        * @param iCutoff Cutoff offset for returned text. Assumed to be less than or
146        *        equal to <code>node.getLength()</code>.
147        * @return The portion of the node's text before the cutoff offset
148        * @throws BadLocationException if the node start offset or cutoff offset is
149        *         beyond the end of the document.
150        */
151       private String getTextBefore(OASTNode node, int iCutoff)
152          throws BadLocationException
153       {
154          String s = null;
155          synchronized (_tree)
156          {
157             s = fDocument.get(node.getOffset(), iCutoff - node.getOffset());
158             return s;
159          }
160       }
161 
162       /***
163        * Retrieves the end of a node's text from the document.
164        * 
165        * @param node Node whose text is desired
166        * @param iOffsetIgnore Offset at which to start ignoring characters
167        * @param iLengthIgnore Number of characters to ignore, starting at the
168        *        ignore offset
169        * @return The portion of the node's text after the ignore range
170        * @throws BadLocationException if the end of the ignore range or the end
171        *         offset of <code>node</code> is beyond the end of the document.
172        */
173       private String getTextAfter(OASTNode node, int iOffsetIgnore,
174          int iLengthIgnore) throws BadLocationException
175       {
176          String s = null;
177          synchronized (_tree)
178          {
179             s = fDocument.get(iOffsetIgnore + iLengthIgnore, node.getLength()
180                - (iOffsetIgnore - node.getOffset()));
181             return s;
182          }
183       }
184 
185       /***
186        * Assembles the text for a node in the OAST based on the node's starting
187        * offset and a range of text within the node that is to be ignored.
188        * 
189        * @param node Node whose text is desired
190        * @param iOffsetIgnore Offset at which to start ignoring characters
191        * @param iLengthIgnore Number of characters to ignore, starting at the
192        *        ignore offset
193        * @return The node's text
194        * @throws BadLocationException
195        */
196       private String getNodeText(OASTNode node, int iOffsetIgnore,
197          int iLengthIgnore) throws BadLocationException
198       {
199          String s = getTextBefore(node, iOffsetIgnore)
200             + getTextAfter(node, iOffsetIgnore, iLengthIgnore);
201          return s;
202       }
203 
204       /***
205        * Returns the new text for a node, based on the node's stored starting
206        * offset and the length of the text being incorporated into it.
207        * 
208        * @param node The node whose reparse text is desired
209        * @param iLengthNew The length of the new text to be included in the reparse
210        * @return The node's reparse text
211        * @throws BadLocationException
212        */
213       private String getReparseText(OASTNode node, int iLengthNew)
214          throws BadLocationException
215       {
216          String s = null;
217          int temp = 0;
218          synchronized (_tree)
219          {
220             temp = node.getLength() + iLengthNew;
221             temp = node.getOffset();
222             temp = fDocument.getLength();
223 //            OWLCore.trace(
224 //               "OWLDocumentPartitioner.OASTUpdater.getReparseText()", node
225 //                  .getName() + "', "
226 //                  + node.getLength() + "', '"
227 //                  + iLengthNew + "', "
228 //                  + fDocument.getLength() + "', '" 
229 //                  + node.getOffset() + "'",
230 //               false);
231             try
232             {
233                s = fDocument.get(node.getOffset(), node.getLength() + iLengthNew);
234             }
235             catch (BadLocationException ble)
236             {
237                OWLCore.logError(EditorPlugin.getID(), "Bad offset "
238                   + fDocument.getLength() + "", ble);
239             }
240             return s;
241          }
242       }
243       
244       /* (non-Javadoc)
245        * @see com.bbn.swede.editor.IDelayedDocumentEventListener#handleEvent(org.eclipse.jface.text.DocumentEvent)
246        */
247       public void handleEvent(DocumentEvent e)
248       {
249 //         _bHandlingEvent = true;
250          synchronized (_tree)
251          {
252             if(e.getLength() > 0) //delete or replace
253             {
254                OASTNode nodeBegin = _tree.getNode(e.getOffset());
255                OASTNode nodeEnd = _tree.getNode(e.getOffset() + e.getLength() - 1);
256                //TODO: make this work.  It currently breaks everything because
257                //      it's trying to get text out of the document that has
258                //      long since been deleated.
259                //      Maybe the delayed event handler could use a custom
260                //      document event type that stores the removed text?
261 //               try
262 //               {
263 //                  if (fDocument.get(e.getOffset(), e.getLength()).trim().length() == 0
264 //                      && e.getText().trim().length() == 0)
265 //                  {
266 //                     //adjusting whitespace, see if it's safe to do so without
267 //                     //a reparse
268 //                     if (nodeBegin instanceof TagNode
269 //                         && (nodeEnd== nodeBegin || nodeEnd.getParent() == nodeBegin))
270 //                     {
271 //                        OWLCore.trace("handleEvent","zero",false);
272 //                        _tree.replace(e.getOffset(), e.getLength(), e.getText());
273 //                        return;
274 //                     }
275 //                  }
276 //               }
277 //               catch (BadLocationException e2)
278 //               {
279 //                  // TODO Auto-generated catch block
280 //                  e2.printStackTrace();
281 //               }
282                //TODO: try to find a better solution for handling deletes.
283                //      in particular, find a way to detect if it's appropriate to
284                //      do a range-of-nodes replace instead of yanking the common
285                //      ancestor. This may be possible through creative use of the
286                //      nodes' simple partitioning and inspection of children, but
287                //      performance with the current implementation may not be slow
288                //      enough to justify the extra work.
289                OASTNode nodeReplace = nodeBegin.findCommonAncestor(nodeEnd);
290                if(nodeReplace instanceof AttributeNode)
291                {
292                   nodeReplace = nodeReplace.getParent();
293                }
294                if(e.getOffset() == nodeBegin.getOffset())
295                {
296                   nodeBegin = nodeBegin.getParent();
297                }
298                if(e.getOffset() + e.getLength() == nodeEnd.getOffset()
299                   + nodeEnd.getLength())
300                {
301                   nodeEnd = nodeEnd.getParent();
302                }
303                if(nodeBegin == nodeEnd)
304                {
305                   if(nodeBegin instanceof TagNode 
306                      || nodeBegin instanceof DocumentRoot)
307                   {
308                      //deleting a range of nodes between begin and end tags
309                      IRegion[] regions = nodeBegin.simplePartitioning();
310                      if(nodeBegin instanceof DocumentRoot
311                         || (regions.length == 3
312                             && e.getOffset() >= regions[1].getOffset()
313                             && e.getOffset() + e.getLength() <= regions[2].getOffset()))
314                      {
315 //                        OWLCore.trace("handleEvent", "one", false);
316                         _tree.replace(e.getOffset(), e.getLength(), e.getText());
317 //                        _bHandlingEvent = false;
318                         return;
319                      }
320                   }
321                }
322                try
323                {
324                   //all other deletions handled by reparsing the common ancestor
325 //                  OWLCore.trace("handleEvent", "two", false);
326                   int iLength = e.getText().length() - e.getLength();
327                   _tree.replace(nodeReplace, getReparseText(nodeReplace, iLength),
328                      e.getOffset(), iLength);
329                }
330                catch (BadLocationException e1)
331                {
332                   OWLCore.logError(EditorPlugin.getID(),
333                      "Document offset malfunction", e1);
334                }
335             }
336             else //insert only
337             {
338                OASTNode nodeBefore = _tree.getNode(e.getOffset() - 1);
339                OASTNode nodeAfter = _tree.getNode(e.getOffset());
340                try
341                {
342                   if(nodeBefore == nodeAfter)
343                   {
344                      if(nodeAfter instanceof DocumentRoot)
345                      {
346 //                        OWLCore.trace("handleEvent", "three", false);
347                         //HACK: this case doesn't seem like it should be necessary
348                         //      but it fixes the weirdness with inserts at the end of
349                         //      the file
350                         _tree.insert(e.getOffset(), e.getText());
351                      }
352                      else if(nodeAfter instanceof AttributeNode)
353                      {
354                         //reparse attribute node
355 //                        OWLCore.trace("handleEvent", "four", false);
356                         OASTNode nodeParent = nodeAfter.getParent();
357                         _tree.replace(nodeParent, getReparseText(nodeParent, e
358                            .getText().length()), e.getOffset(), e.getText().length());
359                      }
360                      else if(mustReparse(nodeAfter))
361                      {
362                         //reparse literals or unparseables with changes in the middle
363 //                        OWLCore.trace("handleEvent", "five", false);
364                         _tree.replace(nodeAfter, getReparseText(nodeAfter, e
365                            .getText().length()), e.getOffset(), e.getText().length());
366                      }
367                      else
368                      {
369                         OASTNode[] nodes = nodeAfter.getChildren();
370                         int i;
371                         for(i = 0; i < nodes.length; i++)
372                         {
373                            OASTNode n = nodes[i];
374                            if(n.getOffset() > e.getOffset())
375                            {
376                               break;
377                            }
378                         }
379                         OASTNode next = (i < nodes.length ? nodes[i]
380                            : null);
381                         OASTNode prev = (i > 0 ? nodes[i - 1] : null);
382                         if(prev == null || next instanceof AttributeNode)
383                         {
384                            //reparse nodeAfter to incorporate new attribute node
385 //                           OWLCore.trace("handleEvent", "six", false);
386                            _tree.replace(nodeAfter, getReparseText(nodeAfter, e
387                               .getText().length()), e.getOffset(), e.getText().length());
388                         }
389                         else if(next == null)
390                         {
391                            String sNode = getNodeText(nodeAfter, e.getOffset(), e
392                               .getText().length());
393                            int iPos = sNode.lastIndexOf('<');
394                            if(iPos + nodeAfter.getOffset() >= e.getOffset())
395                            {
396                               //insert new text as an ordinary child node.
397                               //insert position is after all existing child nodes.
398 //                              OWLCore.trace("handleEvent", "seven", false);
399                               _tree.insert(e.getOffset(), e.getText());
400                            }
401                            else
402                            {
403                               //reparse because nodeAfter's end tag changed
404 //                              OWLCore.trace("handleEvent", "eight", false);
405                               _tree.replace(nodeAfter, getReparseText(nodeAfter, 
406                                  e.getText().length()), e.getOffset(), 
407                                  e.getText().length());
408                            }
409                         }
410                         else
411                         {
412                            String sNode = getNodeText(nodeAfter, e.getOffset(), e
413                               .getText().length());
414                            OASTNode att = nodeAfter.getLastAttribute();
415                            int iPos = sNode.indexOf('>', (att == null ? 0 : att
416                               .getOffset()
417                               + att.getLength() - nodeAfter.getOffset()));
418                            if(iPos + nodeAfter.getOffset() < e.getOffset())
419                            {
420                               //insert new text as ordinary child node
421 //                              OWLCore.trace("handleEvent", "nine", false);
422                               _tree.insert(e.getOffset(), e.getText());
423                            }
424                            else
425                            {
426                               //reparse nodeAfter to incorporate new text as a
427                               //new attribute node at the end of the begin tag
428 //                              OWLCore.trace("handleEvent", "ten", false);
429                               _tree.replace(nodeAfter, getReparseText(nodeAfter, 
430                                  e.getText().length()), e.getOffset(), 
431                                  e.getText().length());
432                            }
433                         }
434                      }
435                   }
436                   else
437                   {
438                      if(mustReparse(nodeBefore)
439                         || (nodeAfter instanceof AttributeNode && e.getText().trim().length() > 0))
440                      {
441                         //reparse preceding literal or unparsable, or reparse
442                         //parent node to incorporate a changed attribute
443 //                        OWLCore.trace("handleEvent", "eleven", false);
444                         _tree.replace(nodeBefore, getReparseText(nodeBefore, 
445                            e.getText().length()), e.getOffset(), 
446                            e.getText().length());
447                      }
448                      else if(mustReparse(nodeAfter)
449                         || (nodeBefore instanceof AttributeNode && e.getText().trim().length() > 0))
450                      {
451                         //reparse subsuquent literal or unparsable, or reparse
452                         //parent node to incorporate a changed attribute
453 //                        OWLCore.trace("handleEvent", "twelve", false);
454                         _tree.replace(nodeAfter, getReparseText(nodeAfter, 
455                            e.getText().length()), e.getOffset(), 
456                            e.getText().length());
457                      }
458                      else //insert is safe
459                      {
460 //                        OWLCore.trace("handleEvent", "thirteen", false);
461                         _tree.insert(e.getOffset(), e.getText());
462                      }
463                   }
464                }
465                catch (BadLocationException e1)
466                {
467                   OWLCore.logError(EditorPlugin.getID(),
468                      "Document offset malfunction", e1);
469                }
470             }
471          }
472 //         _bHandlingEvent = false;
473       }
474 
475       /* (non-Javadoc)
476        * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
477        */
478       public void documentAboutToBeChanged(DocumentEvent event)
479       {
480          synchronized (_tree)
481          {
482             if (!_bCollectEvents)
483             {
484                OWLCore.trace("OASTUpdater", "event not collected", false);
485                return;
486             }
487             _syntaxChecker.addDocumentEvent(event);
488             _syntaxChecker.updateTimer();
489          }
490       }
491 
492       /* (non-Javadoc)
493        * @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent)
494        */
495       public void documentChanged(DocumentEvent event)
496       {
497          //do nothing
498       }
499 
500       /***
501        * Forces an immediate update of the OAST.
502        */
503       public void force()
504       {
505          _syntaxChecker.forceRun();
506       }
507       
508    }
509 
510    private OAST _tree;
511    private OWLSourceEditor _sourceEditor;
512    private SourceViewer _sourceViewer;
513    private OASTUpdater _updater;
514 
515    /***
516     * Creates a new OWLDocumentPartitioner associated with an OWL Abstract
517     * Syntax Tree.
518     * 
519     * @param tree The OAST to associate with this partitioner
520     */
521    public OWLDocumentPartitioner(OAST tree)
522    {
523       super(new OWLPartitionScanner(), OWLPartitionScanner.AS_CONTENT_TYPES);
524       _tree = tree;
525       _tree.addPrenotifiedOASTChangeListener(this);
526       
527       //The event sent to the prenotified listener doesn't get updated as the
528       //content formatter causes changes to the OAST, so we have to listen
529       //for the normal change event as well in order to select the primary
530       //node.
531       _tree.addOASTChangeListener(new IOASTChangeListener()
532       {
533       
534          public void oastChanged(OASTEvent event)
535          {
536             if (_sourceEditor != null && !event.isTextEvent() && event.getPrimaryNode() != null)
537             {
538                _sourceEditor.selectNode(event.getPrimaryNode());
539             }
540          }
541       
542       });
543       _updater = new OASTUpdater(_tree);
544    }
545 
546    /*
547     *  (non-Javadoc)
548     * @see org.eclipse.jface.text.IDocumentPartitioner#connect(org.eclipse.jface.text.IDocument)
549     */
550    public void connect(IDocument document, boolean delayInitialization)
551    {
552       super.connect(document, delayInitialization);
553       _updater.connect(document);
554    }
555    /***
556     * Finalizes the document partitioner by disconnecting the OASTUpdater from
557     * the document.
558     * @throws Throwable if something goes wrong in the superclass
559     *         <code>finalize()</code> call.
560     */
561    public void finalize() throws Throwable
562    {
563       super.finalize();
564       _updater.disconnect();
565    }
566    /***
567     * Sets a reference to the source editor and source viewer of the 
568     * associated OWL editor.
569     * @param editor The source editor
570     * @param viewer The source viewer
571     */
572    /*package*/ void setEditor(OWLSourceEditor editor, SourceViewer viewer)/package-summary/html">class="comment">package*/ void setEditor(OWLSourceEditor editor, SourceViewer viewer)/package-summary.html">/*package*/ void setEditor(OWLSourceEditor editor, SourceViewer viewer)/package-summary.html">class="comment">package*/ void setEditor(OWLSourceEditor editor, SourceViewer viewer)
573    {
574       _sourceEditor = editor;
575       _sourceViewer = viewer;
576    }
577 
578    /***
579     * Force an OAST update. This ensures that the syntax tree accurately
580     * represents the current contents of the document.
581     */
582    public void updateOAST()
583    {
584       _updater.force();
585    }
586 
587    /***
588     * Retrieves the abstract syntax tree associated with the partitioner.
589     * 
590     * @return The partitioner's OAST
591     */
592    public OAST getOAST()
593    {
594       //Make sure the OAST is up to date before providing a reference to it
595       updateOAST();
596       return _tree;
597    }
598    
599    /***
600     * Displaces the regions in a list to compensate for the addition or
601     * removal of text.
602     * @param llRegions The list of regions
603     * @param offset The offset of the text added or removed
604     * @param length The length of the text.  This may be positive for added
605     *               text or negative for removed text.
606     */
607    private void displaceFormattingRegions(LinkedList llRegions, int offset, int length)
608    {
609       Iterator it = llRegions.iterator();
610       while (it.hasNext())
611       {
612          DisplaceableRegion dr = (DisplaceableRegion)it.next();
613          dr.displace(offset, length);
614       }
615    }
616    /***
617     * Updates a list of text ranges that need to be formatted to reflect the
618     * insertion of additional text.  Regions already in the list will have their
619     * offsets and lengths modified to compensate.  Once this is done, if the
620     * list does not already contain a region that encloses the provided offset
621     * and length, the enclosing partition of the new text will be added to
622     * the list.
623     * @param llRegions The list of regions to be formatted
624     * @param offset The offset of the text added to the document
625     * @param length The length of the text added to the document
626     * @param bExpand <code>true</code> if the offset and length provided need
627     *                to be expanded to encompass their entire enclosing region,
628     *                <code>false</code> if they should be used directly
629     */
630    private void addFormattingRegion(LinkedList llRegions, 
631       int offset, int length, boolean bExpand)
632    {
633       int regionOffset, regionLength;
634       if (bExpand)
635       {
636          ITypedRegion r = this.getPartition(offset);
637          regionOffset = r.getOffset();
638          regionLength = r.getLength();
639       }
640       else
641       {
642          regionOffset = offset;
643          regionLength = length;
644       }
645       if (llRegions.size() == 0)
646       {
647          llRegions.addLast(new DisplaceableRegion(regionOffset, regionLength, false));
648       }
649       else
650       {
651          //Displace all regions in the list based on offset & length.
652          //If one of them overlaps with the new region, it will be automatically
653          //lengthened since all regions are created with bMoveIfEqual set to false.
654          displaceFormattingRegions(llRegions, offset, length);
655          
656          //Now, determine if any region contains the new one specified by
657          //offset & length.  If not, insert the new region at the appropriate
658          //position.
659          int i;
660          for (i = 0; i < llRegions.size(); i++)
661          {
662             DisplaceableRegion dr = (DisplaceableRegion)llRegions.get(i);
663             if (dr.getOffset() > offset)
664             {
665                //The new region is discrete from the existing ones, so add it here
666                break;
667             }
668             else if ((dr.getOffset() + dr.getLength()) >= (offset + length))
669             {
670                //The new region would be contained in this one, do nothing
671                return;
672             }
673          }
674          
675          //i now refers to the first region that belongs after the new one
676          //and is thus the proper insertion position
677          llRegions.add(i, new DisplaceableRegion(regionOffset, regionLength, false));
678       }
679    }
680 
681    /* (non-Javadoc)
682     * @see com.bbn.swede.core.dom.IOASTChangeListener#oastChanged(com.bbn.swede.core.dom.OASTEvent)
683     */
684    public void oastChanged(OASTEvent event)
685    {
686       if (!event.isTextEvent())
687       {
688          OWLCore.trace("OWLDocumentPartitioner", "OAST changed!", false);
689          LinkedList llFormattingRegions = new LinkedList();
690          _updater.stopDelayedEventCollection();
691          try
692          {
693             for (int i = 0; i < event.getCount(); i++)
694             {
695                for (int j = 0; j < event.getRemoved(i).length; j++)
696                {
697                   OASTNodeRef refRemoved = event.getRemoved(i)[j];
698                   fDocument.replace(refRemoved.getOffset(), refRemoved.getLength(), "");
699                   displaceFormattingRegions(llFormattingRegions, refRemoved.getOffset(), refRemoved.getLength());
700                }
701                for (int j = 0; j < event.getInserted(i).length; j++)
702                {
703                   OASTNodeRef refInserted = event.getInserted(i)[j];
704                   String sText = refInserted.getText();
705                   fDocument.replace(refInserted.getOffset(), 0, sText);
706                   if (refInserted.getNode() instanceof AttributeNode)
707                   {
708                      //format the entire containing tag to ensure proper spacing
709                      //of attributes within it.
710                      addFormattingRegion(llFormattingRegions, refInserted.getOffset(), refInserted.getLength(), true);
711                   }
712                   //TODO: format surrounding tags if a literal was inserted?
713                   else
714                   {
715                      addFormattingRegion(llFormattingRegions, refInserted.getOffset(), 
716                                          refInserted.getText().length(), false);
717                   }
718                }
719             }
720             _updater.startDelayedEventCollection();
721             Collections.reverse(llFormattingRegions);
722             Iterator it = llFormattingRegions .iterator();
723             while (it.hasNext())
724             {
725                IRegion region = (IRegion)it.next();
726                if (_sourceEditor != null)
727                {
728                   _sourceEditor.selectAndReveal(region.getOffset(), region.getLength());
729                   _sourceViewer.doOperation(SourceViewer.FORMAT);
730                }
731             }
732             _updater.force();
733          }
734          catch (BadLocationException e)
735          {
736             OWLCore.logError(EditorPlugin.getID(), "Editor failed to process external event", e);
737          }
738 
739          _updater.startDelayedEventCollection();
740       }
741    }
742 }