OSDN Git Service

Imported GNU Classpath 0.20
[pf3gnuchains/gcc-fork.git] / libjava / classpath / javax / swing / text / DefaultStyledDocument.java
index eb56bb0..46b8225 100644 (file)
@@ -42,11 +42,13 @@ import java.awt.Color;
 import java.awt.Font;
 import java.io.Serializable;
 import java.util.Enumeration;
+import java.util.Stack;
 import java.util.Vector;
 
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import javax.swing.event.DocumentEvent;
+import javax.swing.event.UndoableEditEvent;
 import javax.swing.undo.AbstractUndoableEdit;
 import javax.swing.undo.UndoableEdit;
 
@@ -365,7 +367,6 @@ public class DefaultStyledDocument extends AbstractDocument
     public String toString()
     {
       StringBuilder b = new StringBuilder();
-      b.append('<');
       switch (type)
         {
         case StartTagType:
@@ -427,6 +428,29 @@ public class DefaultStyledDocument extends AbstractDocument
 
     /** Holds the length of structural changes. */
     private int length;
+    
+    /** Holds the end offset for structural changes. **/
+    private int endOffset;
+
+    /**
+     * The number of inserted end tags. This is a counter which always gets
+     * incremented when an end tag is inserted. This is evaluated before
+     * content insertion to go up the element stack.
+     */
+    private int numEndTags;
+
+    /**
+     * The number of inserted start tags. This is a counter which always gets
+     * incremented when an end tag is inserted. This is evaluated before
+     * content insertion to go up the element stack.
+     */
+    private int numStartTags;
+
+    /**
+     * The current position in the element tree. This is used for bulk inserts
+     * using ElementSpecs.
+     */
+    private Stack elementStack;
 
     /**
      * Holds fractured elements during insertion of end and start tags.
@@ -450,6 +474,7 @@ public class DefaultStyledDocument extends AbstractDocument
     public ElementBuffer(Element root)
     {
       this.root = root;
+      elementStack = new Stack();
     }
 
     /**
@@ -463,6 +488,85 @@ public class DefaultStyledDocument extends AbstractDocument
     }
 
     /**
+     * Updates the element structure of the document in response to removal of
+     * content. It removes the affected {@link Element}s from the document
+     * structure.
+     *
+     * This method sets some internal parameters and delegates the work
+     * to {@link #removeUpdate}.
+     *
+     * @param offs the offset from which content is remove
+     * @param len the length of the removed content
+     * @param ev the document event that records the changes
+     */
+    public void remove(int offs, int len, DefaultDocumentEvent ev)
+    {
+      offset = offs;
+      length = len;
+      documentEvent = ev;
+      removeUpdate();
+    }
+
+    /**
+     * Updates the element structure of the document in response to removal of
+     * content. It removes the affected {@link Element}s from the document
+     * structure.
+     */
+    protected void removeUpdate()
+    {
+      int startParagraph = root.getElementIndex(offset);
+      int endParagraph = root.getElementIndex(offset + length);
+      Element[] empty = new Element[0];
+      int removeStart = -1;
+      int removeEnd = -1;
+      for (int i = startParagraph;  i < endParagraph; i++)
+        {
+          Element paragraph = root.getElement(i);
+          int contentStart = paragraph.getElementIndex(offset);
+          int contentEnd = paragraph.getElementIndex(offset + length);
+          if (contentStart == paragraph.getStartOffset()
+              && contentEnd == paragraph.getEndOffset())
+            {
+              // In this case we only need to remove the whole paragraph. We
+              // do this in one go after this loop and only record the indices
+              // here.
+              if (removeStart == -1)
+                {
+                  removeStart = i;
+                  removeEnd = i;
+                }
+              else
+                removeEnd = i;
+            }
+          else
+            {
+              // In this case we remove a couple of child elements from this
+              // paragraph.
+              int removeLen = contentEnd - contentStart;
+              Element[] removed = new Element[removeLen];
+              for (int j = contentStart; j < contentEnd; j++)
+                removed[j] = paragraph.getElement(j);
+              ((BranchElement) paragraph).replace(contentStart, removeLen,
+                                                  empty);
+              documentEvent.addEdit(new ElementEdit(paragraph, contentStart,
+                                                    removed, empty));
+            }
+        }
+      // Now we remove paragraphs from the root that have been tagged for
+      // removal.
+      if (removeStart != -1)
+        {
+          int removeLen = removeEnd - removeStart;
+          Element[] removed = new Element[removeLen];
+          for (int i = removeStart; i < removeEnd; i++)
+            removed[i] = root.getElement(i);
+          ((BranchElement) root).replace(removeStart, removeLen, empty);
+          documentEvent.addEdit(new ElementEdit(root, removeStart, removed,
+                                                empty));
+        }
+    }
+
+    /**
      * Modifies the element structure so that the specified interval starts
      * and ends at an element boundary. Content and paragraph elements
      * are split and created as necessary.
@@ -493,11 +597,50 @@ public class DefaultStyledDocument extends AbstractDocument
     {
       // Split up the element at the start offset if necessary.
       Element el = getCharacterElement(offset);
-      split(el, offset);
+      Element[] res = split(el, offset, 0);
+      BranchElement par = (BranchElement) el.getParentElement();
+      if (res[1] != null)
+        {
+          int index = par.getElementIndex(offset);
+          Element[] removed;
+          Element[] added;
+          if (res[0] == null)
+            {
+              removed = new Element[0];
+              added = new Element[]{ res[1] };
+              index++;
+            }
+          else
+            {
+              removed = new Element[]{ el };
+              added = new Element[]{ res[0], res[1] };
+            }
+          par.replace(index, removed.length, added);
+          addEdit(par, index, removed, added);
+        }
 
       int endOffset = offset + length;
       el = getCharacterElement(endOffset);
-      split(el, endOffset);
+      res = split(el, endOffset, 0);
+      par = (BranchElement) el.getParentElement();
+      if (res[1] != null)
+        {
+          int index = par.getElementIndex(offset);
+          Element[] removed;
+          Element[] added;
+          if (res[1] == null)
+            {
+              removed = new Element[0];
+              added = new Element[]{ res[1] };
+            }
+          else
+            {
+              removed = new Element[]{ el };
+              added = new Element[]{ res[0], res[1] };
+            }
+          par.replace(index, removed.length, added);
+          addEdit(par, index, removed, added);
+        }
     }
 
     /**
@@ -505,42 +648,96 @@ public class DefaultStyledDocument extends AbstractDocument
      *
      * @param el the Element to possibly split
      * @param offset the offset at which to possibly split
+     * @param space the amount of space to create between the splitted parts
+     *
+     * @return An array of elements which represent the split result. This
+     *         array has two elements, the two parts of the split. The first
+     *         element might be null, which means that the element which should
+     *         be splitted can remain in place. The second element might also
+     *         be null, which means that the offset is already at an element
+     *         boundary and the element doesn't need to be splitted.
+     *          
      */
-    void split(Element el, int offset)
+    private Element[] split(Element el, int offset, int space)
     {
-      if (el instanceof AbstractElement)
+      // If we are at an element boundary, then return an empty array.
+      if ((offset == el.getStartOffset() || offset == el.getEndOffset())
+          && space == 0 && el.isLeaf())
+        return new Element[2];
+
+      // If the element is an instance of BranchElement, then we recursivly
+      // call this method to perform the split.
+      Element[] res = new Element[2];
+      if (el instanceof BranchElement)
         {
-          AbstractElement ael = (AbstractElement) el;
-          int startOffset = ael.getStartOffset();
-          int endOffset = ael.getEndOffset();
-          int len = endOffset - startOffset;
-          if (startOffset != offset && endOffset != offset)
+          int index = el.getElementIndex(offset);
+          Element child = el.getElement(index);
+          Element[] result = split(child, offset, space);
+          Element[] removed;
+          Element[] added;
+          Element[] newAdded;
+
+          int count = el.getElementCount();
+          if (!(result[1] == null))
             {
-              Element paragraph = ael.getParentElement();
-              if (paragraph instanceof BranchElement)
+              // This is the case when we can keep the first element.
+              if (result[0] == null)
                 {
-                  BranchElement par = (BranchElement) paragraph;
-                  Element child1 = createLeafElement(par, ael, startOffset,
-                                                     offset);
-                  Element child2 = createLeafElement(par, ael, offset,
-                                                     endOffset);
-                  int index = par.getElementIndex(startOffset);
-                  Element[] add = new Element[]{ child1, child2 };
-                  par.replace(index, 1, add);
-                  documentEvent.addEdit(new ElementEdit(par, index,
-                                                        new Element[]{ el },
-                                                        add));
+                  removed = new Element[count - index - 1];
+                  newAdded = new Element[count - index - 1];
+                  added = new Element[]{};
                 }
+              // This is the case when we may not keep the first element.
               else
-                throw new AssertionError("paragraph elements are expected to "
-                                         + "be instances of "
-                         + "javax.swing.text.AbstractDocument.BranchElement");
+                {
+                  removed = new Element[count - index];
+                  newAdded = new Element[count - index];
+                  added = new Element[]{result[0]};
+                }
+              newAdded[0] = result[1];
+              for (int i = index; i < count; i++)
+                {
+                  Element el2 = el.getElement(i);
+                  int ind = i - count + removed.length;
+                  removed[ind] = el2;
+                  if (ind != 0)
+                    newAdded[ind] = el2;
+                }
+
+              ((BranchElement) el).replace(index, removed.length, added);
+              addEdit(el, index, removed, added);
+              BranchElement newPar =
+                (BranchElement) createBranchElement(el.getParentElement(),
+                                                    el.getAttributes());
+              newPar.replace(0, 0, newAdded);
+              res = new Element[]{ null, newPar };
+            }
+          else
+            {
+              removed = new Element[count - index];
+              for (int i = index; i < count; ++i)
+                removed[i - index] = el.getElement(i);
+              added = new Element[0];
+              ((BranchElement) el).replace(index, removed.length,
+                                           added);
+              addEdit(el, index, removed, added);
+              BranchElement newPar =
+                (BranchElement) createBranchElement(el.getParentElement(),
+                                                    el.getAttributes());
+              newPar.replace(0, 0, removed);
+              res = new Element[]{ null, newPar };
             }
         }
-      else
-        throw new AssertionError("content elements are expected to be "
-                                 + "instances of "
-                       + "javax.swing.text.AbstractDocument.AbstractElement");
+      else if (el instanceof LeafElement)
+        {
+          BranchElement par = (BranchElement) el.getParentElement();
+          Element el1 = createLeafElement(par, el.getAttributes(),
+                                          el.getStartOffset(), offset);
+          Element el2 = createLeafElement(par, el.getAttributes(),
+                                          offset + space, el.getEndOffset());
+          res = new Element[]{ el1, el2 };
+        }
+      return res;
     }
 
     /**
@@ -560,9 +757,18 @@ public class DefaultStyledDocument extends AbstractDocument
     public void insert(int offset, int length, ElementSpec[] data,
                        DefaultDocumentEvent ev)
     {
+      if (length == 0)
+        return;
       this.offset = offset;
       this.length = length;
+      this.endOffset = offset + length;
       documentEvent = ev;
+      // Push the root and the paragraph at offset onto the element stack.
+      elementStack.clear();
+      elementStack.push(root);
+      elementStack.push(root.getElement(root.getElementIndex(offset)));
+      numEndTags = 0;
+      numStartTags = 0;
       insertUpdate(data);
     }
 
@@ -573,202 +779,348 @@ public class DefaultStyledDocument extends AbstractDocument
      * {@link #insert}.
      *
      * @param data the element specifications for the elements to be inserte
-     */
+     */ 
     protected void insertUpdate(ElementSpec[] data)
     {
+      if (data[0].getType() == ElementSpec.EndTagType)
+        {
+          // fracture deepest child here
+          BranchElement paragraph = (BranchElement) elementStack.peek();
+          Element curr = paragraph.getParentElement();
+          int index = curr.getElementIndex(offset);
+          while (!curr.isLeaf())
+            {
+              index = curr.getElementIndex(offset);
+              curr = curr.getElement(index);
+            }
+          Element parent = curr.getParentElement();
+          Element newEl1 = createLeafElement(parent,
+                                             curr.getAttributes(),
+                                             curr.getStartOffset(), offset);
+          Element grandParent = parent.getParentElement();
+          BranchElement nextBranch = 
+            (BranchElement) grandParent.getElement
+              (grandParent.getElementIndex(parent.getEndOffset()));
+          Element firstLeaf = nextBranch.getElement(0);
+          while (!firstLeaf.isLeaf())
+            {
+              firstLeaf = firstLeaf.getElement(0);
+            }
+          BranchElement parent2 = (BranchElement) firstLeaf.getParentElement();
+          Element newEl2 = 
+            createLeafElement(parent2, 
+                              firstLeaf.getAttributes(), 
+                              offset, firstLeaf.getEndOffset());
+          parent2.replace(0, 1, new Element[] { newEl2 });
+          
+          
+          ((BranchElement) parent).
+              replace(index, 1, new Element[] { newEl1 });
+        }
+      
       for (int i = 0; i < data.length; i++)
         {
+          BranchElement paragraph = (BranchElement) elementStack.peek();
           switch (data[i].getType())
             {
             case ElementSpec.StartTagType:
-              insertStartTag(data[i]);
+              switch (data[i].getDirection())
+                {
+                case ElementSpec.JoinFractureDirection:
+                  insertFracture(data[i]);
+                  break;
+                case ElementSpec.JoinNextDirection:
+                  int index = paragraph.getElementIndex(offset);
+                  elementStack.push(paragraph.getElement(index));
+                  break;
+                case ElementSpec.OriginateDirection:
+                  Element current = (Element) elementStack.peek();
+                  Element newParagraph =
+                    insertParagraph((BranchElement) current, offset);
+                  elementStack.push(newParagraph);
+                  break;
+                default:
+                  break;
+                }
               break;
             case ElementSpec.EndTagType:
-              insertEndTag(data[i]);
+              elementStack.pop();
               break;
-            default:
+            case ElementSpec.ContentType:
               insertContentTag(data[i]);
               break;
             }
         }
+      endEdit();
     }
 
     /**
-     * Insert a new paragraph after the paragraph at the current position.
-     *
-     * @param tag the element spec that describes the element to be inserted
+     * Finishes an insertion by possibly evaluating the outstanding start and
+     * end tags. However, this is only performed if the event has received any
+     * modifications.
+     */
+    private void endEdit()
+    {
+      if (documentEvent.modified)
+        prepareContentInsertion();
+    }
+
+    /**
+     * Evaluates the number of inserted end tags and performs the corresponding
+     * structural changes.
      */
-    void insertStartTag(ElementSpec tag)
+    private void prepareContentInsertion()
     {
-      BranchElement root = (BranchElement) getDefaultRootElement();
-      int index = root.getElementIndex(offset);
-      if (index == -1)
-        index = 0;
-
-      BranchElement newParagraph =
-        (BranchElement) createBranchElement(root, tag.getAttributes());
-      newParagraph.setResolveParent(getStyle(StyleContext.DEFAULT_STYLE));
-
-      // Add new paragraph into document structure.
-      Element[] added = new Element[]{newParagraph};
-      root.replace(index + 1, 0, added);
-      ElementEdit edit = new ElementEdit(root, index + 1, new Element[0],
-                                         added);
-      documentEvent.addEdit(edit);
-
-      // Maybe add fractured elements.
-      if (tag.getDirection() == ElementSpec.JoinFractureDirection)
+      while (numEndTags > 0)
+        {
+          elementStack.pop();
+          numEndTags--;
+        }
+
+      while (numStartTags > 0)
         {
-          Element[] newFracture = new Element[fracture.length];
-          for (int i = 0; i < fracture.length; i++)
+          Element current = (Element) elementStack.peek();
+          Element newParagraph =
+            insertParagraph((BranchElement) current, offset);
+          elementStack.push(newParagraph);
+          numStartTags--;
+        }
+    }
+
+    private Element insertParagraph(BranchElement par, int offset)
+    {
+      Element current = par.getElement(par.getElementIndex(offset));
+      Element[] res = split(current, offset, 0);
+      int index = par.getElementIndex(offset);
+      Element ret;
+      if (res[1] != null)
+        {
+          Element[] removed;
+          Element[] added;
+          if (res[0] == null)
             {
-              Element oldLeaf = fracture[i];
-              Element newLeaf = createLeafElement(newParagraph,
-                                                  oldLeaf.getAttributes(),
-                                                  oldLeaf.getStartOffset(),
-                                                  oldLeaf.getEndOffset());
-              newFracture[i] = newLeaf;
+              removed = new Element[0];
+              if (res[1] instanceof BranchElement)
+                {
+                  added = new Element[]{ res[1] };
+                  ret = res[1];
+                }
+              else
+                {
+                  ret = createBranchElement(par, null);
+                  added = new Element[]{ ret, res[1] };
+                }
+              index++;
+            }
+          else
+            {
+              removed = new Element[]{ current };
+              if (res[1] instanceof BranchElement)
+                {
+                  ret = res[1];
+                  added = new Element[]{ res[0], res[1] };
+                }
+              else
+                {
+                  ret = createBranchElement(par, null);
+                  added = new Element[]{ res[0], ret, res[1] };
+                }
             }
-          newParagraph.replace(0, 0, newFracture);
-          edit = new ElementEdit(newParagraph, 0, new Element[0],
-                                 fracture);
-          documentEvent.addEdit(edit);
-          fracture = new Element[0];
+          par.replace(index, removed.length, added);
+          addEdit(par, index, removed, added);
         }
+      else
+        {
+          ret = createBranchElement(par, null);
+          Element[] added = new Element[]{ ret };
+          par.replace(index, 0, added);
+          addEdit(par, index, new Element[0], added);
+        }
+      return ret;
     }
-
+    
     /**
-     * Inserts an end tag into the document structure. This cuts of the
-     * current paragraph element, possibly fracturing it's child elements.
-     * The fractured elements are saved so that they can be joined later
-     * with a new paragraph element.
+     * Inserts a fracture into the document structure.
+     * 
+     * @param tag - the element spec.
      */
-    void insertEndTag(ElementSpec tag)
+    private void insertFracture(ElementSpec tag)
     {
-      BranchElement root = (BranchElement) getDefaultRootElement();
-      int parIndex = root.getElementIndex(offset);
-      BranchElement paragraph = (BranchElement) root.getElement(parIndex);
-
-      int index = paragraph.getElementIndex(offset);
-      LeafElement content = (LeafElement) paragraph.getElement(index);
-      // We might have to split the element at offset.
-      split(content, offset);
-      index = paragraph.getElementIndex(offset);
-
-      int count = paragraph.getElementCount();
-      // Store fractured elements.
-      fracture = new Element[count - index];
-      for (int i = index; i < count; ++i)
-        fracture[i - index] = paragraph.getElement(i);
-
-      // Delete fractured elements.
-      paragraph.replace(index, count - index, new Element[0]);
-
-      // Add this action to the document event.
-      ElementEdit edit = new ElementEdit(paragraph, index, fracture,
-                                         new Element[0]);
-      documentEvent.addEdit(edit);
+      // This is the parent of the paragraph about to be fractured.  We will
+      // create a new child of this parent.
+      BranchElement parent = (BranchElement) elementStack.peek();
+      int parentIndex = parent.getElementIndex(offset);
+      
+      // This is the old paragraph.  We must remove all its children that 
+      // occur after offset and move them to a new paragraph.  We must
+      // also recreate its child that occurs at offset to have the proper
+      // end offset.  The remainder of this child will also go in the new
+      // paragraph.
+      BranchElement previous = (BranchElement) parent.getElement(parentIndex);
+      
+      // This is the new paragraph.
+      BranchElement newBranch = 
+        (BranchElement) createBranchElement(parent, previous.getAttributes());
+      
+      
+      // The steps we must take to properly fracture are:
+      // 1. Recreate the LeafElement at offset to have the correct end offset.
+      // 2. Create a new LeafElement with the remainder of the LeafElement in 
+      //    #1 ==> this is whatever was in that LeafElement to the right of the
+      //    inserted newline.
+      // 3. Find the paragraph at offset and remove all its children that 
+      //    occur _after_ offset.  These will be moved to the newly created
+      //    paragraph.
+      // 4. Move the LeafElement created in #2 and all the LeafElements removed
+      //    in #3 to the newly created paragraph.
+      // 5. Add the new paragraph to the parent.
+      int previousIndex = previous.getElementIndex(offset);
+      int numReplaced = previous.getElementCount() - previousIndex;
+      Element previousLeaf = previous.getElement(previousIndex);
+      AttributeSet prevLeafAtts = previous.getAttributes();
+      
+      // This recreates the child at offset to have the proper end offset.  
+      // (Step 1).
+      Element newPreviousLeaf = 
+        createLeafElement(previous, 
+                          prevLeafAtts, previousLeaf.getStartOffset(), 
+                          offset);
+      // This creates the new child, which is the remainder of the old child.  
+      // (Step 2).
+      
+      Element firstLeafInNewBranch = 
+        createLeafElement(newBranch, prevLeafAtts, 
+                          offset, previousLeaf.getEndOffset());
+      
+      // Now we move the new LeafElement and all the old children that occurred
+      // after the offset to the new paragraph.  (Step 4).
+      Element[] newLeaves = new Element[numReplaced];
+      newLeaves[0] = firstLeafInNewBranch;
+      for (int i = 1; i < numReplaced; i++)
+        newLeaves[i] = previous.getElement(previousIndex + i);
+      newBranch.replace(0, 0, newLeaves);
+      addEdit(newBranch, 0, null, newLeaves);
+            
+      // Now we remove the children after the offset from the previous 
+      // paragraph. (Step 3).
+      int removeSize = previous.getElementCount() - previousIndex;
+      Element[] add = new Element[] { newPreviousLeaf };
+      Element[] remove = new Element[removeSize];
+      for (int j = 0; j < removeSize; j++)
+        remove[j] = previous.getElement(previousIndex + j);
+      previous.replace(previousIndex, removeSize, add);
+      addEdit(previous, previousIndex, remove, add);
+      
+      // Finally we add the new paragraph to the parent. (Step 5).
+      Element[] nb = new Element[] { newBranch };
+      int index = parentIndex + 1;
+      parent.replace(index, 0, nb);
+      addEdit(parent, index, null, nb);
     }
-
+    
     /**
      * Inserts a content element into the document structure.
-     *
+     * 
      * @param tag the element spec
      */
-    void insertContentTag(ElementSpec tag)
+    private void insertContentTag(ElementSpec tag)
     {
+      prepareContentInsertion();
       int len = tag.getLength();
       int dir = tag.getDirection();
+      AttributeSet tagAtts = tag.getAttributes();
       if (dir == ElementSpec.JoinPreviousDirection)
         {
-          Element prev = getCharacterElement(offset);
-          BranchElement prevParent = (BranchElement) prev.getParentElement();
-          Element join = createLeafElement(prevParent, tag.getAttributes(),
-                                           prev.getStartOffset(),
-                                           Math.max(prev.getEndOffset(),
-                                                    offset + len));
-          int ind = prevParent.getElementIndex(offset);
-          if (ind == -1)
-            ind = 0;
-          Element[] add = new Element[]{join};
-          prevParent.replace(ind, 1, add);
-
-          // Add this action to the document event.
-          ElementEdit edit = new ElementEdit(prevParent, ind,
-                                             new Element[]{prev}, add);
-          documentEvent.addEdit(edit);
+          // The mauve tests to this class show that a JoinPrevious insertion
+          // does not add any edits to the document event. To me this means
+          // that nothing is done here. The previous element naturally should
+          // expand so that it covers the new characters.
         }
       else if (dir == ElementSpec.JoinNextDirection)
         {
-          Element next = getCharacterElement(offset + len);
-          BranchElement nextParent = (BranchElement) next.getParentElement();
-          Element join = createLeafElement(nextParent, tag.getAttributes(),
-                                           offset,
-                                           next.getEndOffset());
-          int ind = nextParent.getElementIndex(offset + len);
-          if (ind == -1)
-            ind = 0;
-          Element[] add = new Element[]{join};
-          nextParent.replace(ind, 1, add);
-
-          // Add this action to the document event.
-          ElementEdit edit = new ElementEdit(nextParent, ind,
-                                             new Element[]{next}, add);
-          documentEvent.addEdit(edit);
+          // FIXME:
+          // Have to handle JoinNext differently depending on whether
+          // or not it comes after a fracture.  If comes after a fracture, 
+          // the insertFracture method takes care of everything and nothing
+          // needs to be done here.  Otherwise, we need to adjust the
+          // Element structure.  For now, I check if the elementStack's 
+          // top Element is the immediate parent of the LeafElement at
+          // offset - if so, we did not come immediately after a 
+          // fracture.  This seems awkward and should probably be improved.
+          // We may be doing too much in insertFracture because we are 
+          // adjusting the offsets, the correct thing to do may be to 
+          // create a new branch element and push it on to element stack
+          // and then this method here can be more general.
+          
+          BranchElement paragraph = (BranchElement) elementStack.peek();
+          int index = paragraph.getElementIndex(offset);
+          Element target = paragraph.getElement(index);
+          if (target.isLeaf() && paragraph.getElementCount() > (index + 1))
+            {
+              Element next = paragraph.getElement(index + 1);
+              Element newEl1 = createLeafElement(paragraph,
+                                                 target.getAttributes(),
+                                                 target.getStartOffset(),
+                                                 offset);
+              Element newEl2 = createLeafElement(paragraph,
+                                                 next.getAttributes(), offset,
+                                                 next.getEndOffset());
+              Element[] add = new Element[] { newEl1, newEl2 };
+              paragraph.replace (index, 2, add);
+              addEdit(paragraph, index, new Element[] { target, next }, add);
+            }
         }
-      else
+      else if (dir == ElementSpec.OriginateDirection)
         {
-          BranchElement par = (BranchElement) getParagraphElement(offset);
-
-          int ind = par.getElementIndex(offset);
-
-          // Make room for the element.
-          // Cut previous element.
-          Element prev = par.getElement(ind);
-          if (prev != null && prev.getStartOffset() < offset)
+          BranchElement paragraph = (BranchElement) elementStack.peek();
+          int index = paragraph.getElementIndex(offset);
+          Element current = paragraph.getElement(index);
+          
+          Element[] added;
+          Element[] removed = new Element[] {current};
+          Element[] splitRes = split(current, offset, length);
+          if (splitRes[0] == null)
             {
-              Element cutPrev = createLeafElement(par, prev.getAttributes(),
-                                                  prev.getStartOffset(),
-                                                  offset);
-              Element[] remove = new Element[]{prev};
-              Element[] add = new Element[]{cutPrev};
-              if (prev.getEndOffset() > offset + len)
-                {
-                  Element rem = createLeafElement(par, prev.getAttributes(),
-                                                  offset + len,
-                                                  prev.getEndOffset());
-                  add = new Element[]{cutPrev, rem};
-                }
-
-              par.replace(ind, 1, add);
-              documentEvent.addEdit(new ElementEdit(par, ind, remove, add));
-              ind++;
+              added = new Element[2];
+              added[0] = createLeafElement(paragraph, tagAtts,
+                                           offset, endOffset);
+              added[1] = splitRes[1];
+              removed = new Element[0];
+              index++;
             }
-          // ind now points to the next element.
-
-          // Cut next element if necessary.
-          Element next = par.getElement(ind);
-          if (next != null && next.getStartOffset() < offset + len)
+          else if (current.getStartOffset() == offset)
+            { 
+              // This is if the new insertion happens immediately before 
+              // the <code>current</code> Element.  In this case there are 2 
+              // resulting Elements.              
+              added = new Element[2];
+              added[0] = createLeafElement(paragraph, tagAtts, offset,
+                                           endOffset);
+              added[1] = splitRes[1];
+            }
+          else if (current.getEndOffset() == endOffset)
             {
-              Element cutNext = createLeafElement(par, next.getAttributes(),
-                                                  offset + len,
-                                                  next.getEndOffset());
-              Element[] remove = new Element[]{next};
-              Element[] add = new Element[]{cutNext};
-              par.replace(ind, 1, add);
-              documentEvent.addEdit(new ElementEdit(par, ind, remove,
-                                                    add));
+              // This is if the new insertion happens right at the end of 
+              // the <code>current</code> Element.  In this case there are 
+              // 2 resulting Elements.
+              added = new Element[2];
+              added[0] = splitRes[0];
+              added[1] = createLeafElement(paragraph, tagAtts, offset,
+                                           endOffset);
             }
-
-          // Insert new element.
-          Element newEl = createLeafElement(par, tag.getAttributes(),
-                                            offset, offset + len);
-          Element[] added = new Element[]{newEl};
-          par.replace(ind, 0, added);
-          // Add this action to the document event.
-          ElementEdit edit = new ElementEdit(par, ind, new Element[0],
-                                             added);
-          documentEvent.addEdit(edit);
+          else
+            {
+              // This is if the new insertion is in the middle of the 
+              // <code>current</code> Element.  In this case 
+              // there will be 3 resulting Elements.
+              added = new Element[3];
+              added[0] = splitRes[0];
+              added[1] = createLeafElement(paragraph, tagAtts, offset,
+                                           endOffset);
+              added[2] = splitRes[1];
+            }          
+          paragraph.replace(index, removed.length, added);
+          addEdit(paragraph, index, removed, added);
         }
       offset += len;
     }
@@ -800,6 +1152,79 @@ public class DefaultStyledDocument extends AbstractDocument
       result.replace(0, 0, children);
       return result;
     }
+
+    /**
+     * Adds an ElementChange for a given element modification to the document
+     * event. If there already is an ElementChange registered for this element,
+     * this method tries to merge the ElementChanges together. However, this
+     * is only possible if the indices of the new and old ElementChange are
+     * equal.
+     *
+     * @param e the element
+     * @param i the index of the change
+     * @param removed the removed elements, or <code>null</code>
+     * @param added the added elements, or <code>null</code>
+     */
+    private void addEdit(Element e, int i, Element[] removed, Element[] added)
+    {
+      // Perform sanity check first.
+      DocumentEvent.ElementChange ec = documentEvent.getChange(e);
+
+      // Merge the existing stuff with the new stuff.
+      Element[] oldAdded = ec == null ? null: ec.getChildrenAdded();
+      Element[] newAdded;
+      if (oldAdded != null && added != null)
+        {
+          if (ec.getIndex() <= i)
+            {
+              int index = i - ec.getIndex();
+              // Merge adds together.
+              newAdded = new Element[oldAdded.length + added.length];
+              System.arraycopy(oldAdded, 0, newAdded, 0, index);
+              System.arraycopy(added, 0, newAdded, index, added.length);
+              System.arraycopy(oldAdded, index, newAdded, index + added.length,
+                               oldAdded.length - index);
+              i = ec.getIndex();
+            }
+          else
+            throw new AssertionError("Not yet implemented case.");
+        }
+      else if (added != null)
+        newAdded = added;
+      else if (oldAdded != null)
+        newAdded = oldAdded;
+      else
+        newAdded = new Element[0];
+
+      Element[] oldRemoved = ec == null ? null: ec.getChildrenRemoved();
+      Element[] newRemoved;
+      if (oldRemoved != null && removed != null)
+        {
+          if (ec.getIndex() <= i)
+            {
+              int index = i - ec.getIndex();
+              // Merge removes together.
+              newRemoved = new Element[oldRemoved.length + removed.length];
+              System.arraycopy(oldAdded, 0, newRemoved, 0, index);
+              System.arraycopy(removed, 0, newRemoved, index, removed.length);
+              System.arraycopy(oldRemoved, index, newRemoved,
+                               index + removed.length,
+                               oldRemoved.length - index);
+              i = ec.getIndex();
+            }
+          else
+            throw new AssertionError("Not yet implemented case.");
+        }
+      else if (removed != null)
+        newRemoved = removed;
+      else if (oldRemoved != null)
+        newRemoved = oldRemoved;
+      else
+        newRemoved = new Element[0];
+
+      // Replace the existing edit for the element with the merged.
+      documentEvent.addEdit(new ElementEdit(e, i, newRemoved, newAdded));
+    }
   }
 
   /**
@@ -824,7 +1249,7 @@ public class DefaultStyledDocument extends AbstractDocument
      */
     public String getName()
     {
-      return "section";
+      return SectionElementName;
     }
   }
 
@@ -945,9 +1370,7 @@ public class DefaultStyledDocument extends AbstractDocument
     // Use createBranchElement() and createLeafElement instead.
     SectionElement section = new SectionElement();
 
-    BranchElement paragraph =
-      (BranchElement) createBranchElement(section, null);
-    paragraph.setResolveParent(getStyle(StyleContext.DEFAULT_STYLE));
+    BranchElement paragraph = new BranchElement(section, null);
     tmp = new Element[1];
     tmp[0] = paragraph;
     section.replace(0, 0, tmp);
@@ -1043,7 +1466,11 @@ public class DefaultStyledDocument extends AbstractDocument
   {
     Element paragraph = getParagraphElement(position);
     AttributeSet attributes = paragraph.getAttributes();
-    return (Style) attributes.getResolveParent();
+    AttributeSet a = attributes.getResolveParent();
+    // If the resolve parent is not of type Style, we return null.
+    if (a instanceof Style)
+      return (Style) a;
+    return null;
   }
 
   /**
@@ -1112,50 +1539,54 @@ public class DefaultStyledDocument extends AbstractDocument
                                     AttributeSet attributes,
                                     boolean replace)
   {
-    DefaultDocumentEvent ev =
-      new DefaultDocumentEvent(offset, length,
-                              DocumentEvent.EventType.CHANGE);
-
-    // Modify the element structure so that the interval begins at an element
-    // start and ends at an element end.
-    buffer.change(offset, length, ev);
-
-    Element root = getDefaultRootElement();
-    // Visit all paragraph elements within the specified interval
-    int paragraphCount =  root.getElementCount();
-    for (int pindex = 0; pindex < paragraphCount; pindex++)
+    // Exit early if length is 0, so no DocumentEvent is created or fired.
+    if (length == 0)
+      return;
+    try
       {
-        Element paragraph = root.getElement(pindex);
-        // Skip paragraphs that lie outside the interval.
-        if ((paragraph.getStartOffset() > offset + length)
-            || (paragraph.getEndOffset() < offset))
-          continue;
-
-        // Visit content elements within this paragraph
-        int contentCount = paragraph.getElementCount();
-        for (int cindex = 0; cindex < contentCount; cindex++)
+        // Must obtain a write lock for this method.  writeLock() and
+        // writeUnlock() should always be in try/finally block to make
+        // sure that locking happens in a balanced manner.
+        writeLock();
+        DefaultDocumentEvent ev = 
+          new DefaultDocumentEvent(
+                                   offset, 
+                                   length, 
+                                   DocumentEvent.EventType.CHANGE);
+
+        // Modify the element structure so that the interval begins at an
+        // element
+        // start and ends at an element end.
+        buffer.change(offset, length, ev);
+
+        Element root = getDefaultRootElement();
+        // Visit all paragraph elements within the specified interval
+        int end = offset + length;
+        Element curr;
+        for (int pos = offset; pos < end; )
           {
-            Element content = paragraph.getElement(cindex);
-            // Skip content that lies outside the interval.
-            if ((content.getStartOffset() > offset + length)
-                || (content.getEndOffset() < offset))
-              continue;
-
-            if (content instanceof AbstractElement)
-              {
-                AbstractElement el = (AbstractElement) content;
-                if (replace)
-                  el.removeAttributes(el);
-                el.addAttributes(attributes);
-              }
-            else
-              throw new AssertionError("content elements are expected to be"
-                                       + "instances of "
-                      + "javax.swing.text.AbstractDocument.AbstractElement");
+            // Get the CharacterElement at offset pos.
+            curr = getCharacterElement(pos);
+            if (pos == curr.getEndOffset())
+              break;
+            
+            MutableAttributeSet a = (MutableAttributeSet) curr.getAttributes();
+            ev.addEdit(new AttributeUndoableEdit(curr, attributes, replace));
+            // If replace is true, remove all the old attributes.
+            if (replace)
+              a.removeAttributes(a);
+            // Add all the new attributes.
+            a.addAttributes(attributes);
+            // Increment pos so we can check the next CharacterElement.
+            pos = curr.getEndOffset();
           }
+        fireChangedUpdate(ev);
+        fireUndoableEditUpdate(new UndoableEditEvent(this, ev));
+      }
+    finally
+      {
+        writeUnlock();
       }
-
-    fireChangedUpdate(ev);
   }
   
   /**
@@ -1167,14 +1598,36 @@ public class DefaultStyledDocument extends AbstractDocument
   public void setLogicalStyle(int position, Style style)
   {
     Element el = getParagraphElement(position);
-    if (el instanceof AbstractElement)
-      {
-        AbstractElement ael = (AbstractElement) el;
-        ael.setResolveParent(style);
-      }
-    else
-      throw new AssertionError("paragraph elements are expected to be"
-         + "instances of javax.swing.text.AbstractDocument.AbstractElement");
+    // getParagraphElement doesn't return null but subclasses might so
+    // we check for null here.
+    if (el == null)
+      return;
+    try
+    {
+      writeLock();    
+      if (el instanceof AbstractElement)
+        {
+          AbstractElement ael = (AbstractElement) el;
+          ael.setResolveParent(style);
+          int start = el.getStartOffset();
+          int end = el.getEndOffset();
+          DefaultDocumentEvent ev = 
+            new DefaultDocumentEvent (
+                                      start, 
+                                      end - start, 
+                                      DocumentEvent.EventType.CHANGE);
+          // FIXME: Add an UndoableEdit to this event and fire it.
+          fireChangedUpdate(ev);
+        }
+      else
+        throw new 
+        AssertionError("paragraph elements are expected to be"
+                       + "instances of AbstractDocument.AbstractElement");
+    }
+    finally
+    {
+      writeUnlock();
+    }
   }
 
   /**
@@ -1190,15 +1643,47 @@ public class DefaultStyledDocument extends AbstractDocument
                                      AttributeSet attributes,
                                      boolean replace)
   {
-    int index = offset;
-    while (index < offset + length)
+    try
+      {
+        // Must obtain a write lock for this method.  writeLock() and
+        // writeUnlock() should always be in try/finally blocks to make
+        // sure that locking occurs in a balanced manner.
+        writeLock();
+        
+        // Create a DocumentEvent to use for changedUpdate().
+        DefaultDocumentEvent ev = 
+          new DefaultDocumentEvent (
+                                    offset, 
+                                    length, 
+                                    DocumentEvent.EventType.CHANGE);
+        
+        // Have to iterate through all the _paragraph_ elements that are
+        // contained or partially contained in the interval
+        // (offset, offset + length).
+        Element rootElement = getDefaultRootElement();
+        int startElement = rootElement.getElementIndex(offset);
+        int endElement = rootElement.getElementIndex(offset + length - 1);
+        if (endElement < startElement)
+          endElement = startElement;
+        
+        for (int i = startElement; i <= endElement; i++)
+          {
+            Element par = rootElement.getElement(i);
+            MutableAttributeSet a = (MutableAttributeSet) par.getAttributes();
+            // Add the change to the DocumentEvent.
+            ev.addEdit(new AttributeUndoableEdit(par, attributes, replace));
+            // If replace is true remove the old attributes.
+            if (replace)
+              a.removeAttributes(a);
+            // Add the new attributes.
+            a.addAttributes(attributes);
+          }
+        fireChangedUpdate(ev);
+        fireUndoableEditUpdate(new UndoableEditEvent(this, ev));
+      }
+    finally
       {
-        AbstractElement par = (AbstractElement) getParagraphElement(index);
-        AttributeContext ctx = getAttributeContext();
-        if (replace)
-          par.removeAttributes(par);
-        par.addAttributes(attributes);
-        index = par.getElementCount();
+        writeUnlock();
       }
   }
 
@@ -1212,9 +1697,14 @@ public class DefaultStyledDocument extends AbstractDocument
   protected void insertUpdate(DefaultDocumentEvent ev, AttributeSet attr)
   {
     super.insertUpdate(ev, attr);
+    // If the attribute set is null, use an empty attribute set.
+    if (attr == null)
+      attr = SimpleAttributeSet.EMPTY;
     int offset = ev.getOffset();
     int length = ev.getLength();
     int endOffset = offset + length;
+    AttributeSet paragraphAttributes = 
+      getParagraphElement(endOffset).getAttributes();    
     Segment txt = new Segment();
     try
       {
@@ -1229,66 +1719,141 @@ public class DefaultStyledDocument extends AbstractDocument
 
     int len = 0;
     Vector specs = new Vector();
-
+    ElementSpec finalStartTag = null;
+    short finalStartDirection = ElementSpec.OriginateDirection;
+    boolean prevCharWasNewline = false;
     Element prev = getCharacterElement(offset);
-    Element next = getCharacterElement(endOffset);
+    Element next = getCharacterElement(endOffset);    
+    Element prevParagraph = getParagraphElement(offset);
+    Element paragraph = getParagraphElement(endOffset);
+    
+    int segmentEnd = txt.offset + txt.count;
+    
+    // Check to see if we're inserting immediately after a newline.
+    if (offset > 0)
+      {
+        try
+        {
+          String s = getText(offset - 1, 1);
+          if (s.equals("\n"))
+            {
+              finalStartDirection = 
+                handleInsertAfterNewline(specs, offset, endOffset,
+                                         prevParagraph,
+                                         paragraph,
+                                         paragraphAttributes);
+              
+              prevCharWasNewline = true;
+              // Find the final start tag from the ones just created.
+              for (int i = 0; i < specs.size(); i++)
+                if (((ElementSpec) specs.get(i)).getType() 
+                    == ElementSpec.StartTagType)
+                  finalStartTag = (ElementSpec)specs.get(i);
+            }
+        }
+        catch (BadLocationException ble)
+        {          
+          // This shouldn't happen.
+          AssertionError ae = new AssertionError();
+          ae.initCause(ble);
+          throw ae;
+        }        
+      }
 
-    for (int i = offset; i < endOffset; ++i)
+        
+    for (int i = txt.offset; i < segmentEnd; ++i)
       {
         len++;
         if (txt.array[i] == '\n')
           {
-            ElementSpec spec = new ElementSpec(attr, ElementSpec.ContentType,
-                                               len);
-
-            // If we are at the last index, then check if we could probably be
-            // joined with the next element.
-            if (i == endOffset - 1)
-              {
-                if (next.getAttributes().isEqual(attr))
-                  spec.setDirection(ElementSpec.JoinNextDirection);
-              }
-            // If we are at the first new element, then check if it could be
-            // joined with the previous element.
-            else if (specs.size() == 0)
-              {
-                if (prev.getAttributes().isEqual(attr))
-                    spec.setDirection(ElementSpec.JoinPreviousDirection);
-              }
-
-            specs.add(spec);
+            // Add the ElementSpec for the content.
+            specs.add(new ElementSpec(attr, ElementSpec.ContentType, len));            
 
             // Add ElementSpecs for the newline.
-            ElementSpec endTag = new ElementSpec(null, ElementSpec.EndTagType);
-            specs.add(endTag);
-            ElementSpec startTag = new ElementSpec(null,
+            specs.add(new ElementSpec(null, ElementSpec.EndTagType));
+            finalStartTag = new ElementSpec(paragraphAttributes,
                                                    ElementSpec.StartTagType);
-            startTag.setDirection(ElementSpec.JoinFractureDirection);
-            specs.add(startTag);
-
+            specs.add(finalStartTag);
             len = 0;
-            offset += len;
           }
       }
 
     // Create last element if last character hasn't been a newline.
-    if (len > 0)
+    if (len > 0)                      
+      specs.add(new ElementSpec(attr, ElementSpec.ContentType, len));
+
+    // Set the direction of the last spec of type StartTagType.  
+    // If we are inserting after a newline then this value comes from 
+    // handleInsertAfterNewline.
+    if (finalStartTag != null)
+      {        
+        if (prevCharWasNewline)
+          finalStartTag.setDirection(finalStartDirection);
+        else if (prevParagraph.getEndOffset() != endOffset)
+          {
+            try
+              {
+                String last = getText(endOffset - 1, 1);
+                if (!last.equals("\n"))
+                  finalStartTag.setDirection(ElementSpec.JoinFractureDirection);
+              }
+            catch (BadLocationException ble)
+              {
+                // This shouldn't happen.
+                AssertionError ae = new AssertionError();
+                ae.initCause(ble);
+                throw ae;
+              } 
+          }
+        else
+          {
+            // If there is an element AFTER this one, then set the 
+            // direction to JoinNextDirection.
+            Element parent = prevParagraph.getParentElement();
+            int index = parent.getElementIndex(offset);
+            if (index + 1 < parent.getElementCount()
+                && !parent.getElement(index + 1).isLeaf())
+              finalStartTag.setDirection(ElementSpec.JoinNextDirection);
+          }
+      }
+    
+    // If we are at the last index, then check if we could probably be
+    // joined with the next element.
+    // This means:
+    //  - we must be a ContentTag
+    //  - if there is a next Element, we must have the same attributes
+    //  - if there is no next Element, but one will be created,
+    //    we must have the same attributes as the higher-level run.
+    ElementSpec last = (ElementSpec) specs.lastElement();
+    if (last.getType() == ElementSpec.ContentType)
       {
-        ElementSpec spec = new ElementSpec(attr, ElementSpec.ContentType, len);
-        // If we are at the first new element, then check if it could be
-        // joined with the previous element.
-        if (specs.size() == 0)
+        Element currentRun = 
+          prevParagraph.getElement(prevParagraph.getElementIndex(offset));
+        if (currentRun.getEndOffset() == endOffset)
           {
-            if (prev.getAttributes().isEqual(attr))
-              spec.setDirection(ElementSpec.JoinPreviousDirection);
+            if (endOffset < getLength() && next.getAttributes().isEqual(attr)
+                && last.getType() == ElementSpec.ContentType)
+              last.setDirection(ElementSpec.JoinNextDirection);
+          }
+        else
+          {
+            if (finalStartTag != null
+                && finalStartTag.getDirection() == 
+                  ElementSpec.JoinFractureDirection
+                && currentRun.getAttributes().isEqual(attr))
+              {
+                last.setDirection(ElementSpec.JoinNextDirection);
+              }
           }
-        // Check if we could probably be joined with the next element.
-        else if (next.getAttributes().isEqual(attr))
-          spec.setDirection(ElementSpec.JoinNextDirection);
-
-        specs.add(spec);
       }
-
+    
+    // If we are at the first new element, then check if it could be
+    // joined with the previous element.
+    ElementSpec first = (ElementSpec) specs.firstElement();
+    if (prev.getAttributes().isEqual(attr)
+        && first.getType() == ElementSpec.ContentType)
+      first.setDirection(ElementSpec.JoinPreviousDirection);
+    
     ElementSpec[] elSpecs =
       (ElementSpec[]) specs.toArray(new ElementSpec[specs.size()]);
 
@@ -1296,6 +1861,47 @@ public class DefaultStyledDocument extends AbstractDocument
   }
 
   /**
+   * A helper method to set up the ElementSpec buffer for the special
+   * case of an insertion occurring immediately after a newline.
+   * @param specs the ElementSpec buffer to initialize.
+   */
+  short handleInsertAfterNewline(Vector specs, int offset, int endOffset,
+                                Element prevParagraph, Element paragraph,
+                                AttributeSet a)
+  {
+    if (prevParagraph.getParentElement() == paragraph.getParentElement())
+      {
+        specs.add(new ElementSpec(a, ElementSpec.EndTagType));
+        specs.add(new ElementSpec(a, ElementSpec.StartTagType));
+        if (prevParagraph.getEndOffset() != endOffset)
+          return ElementSpec.JoinFractureDirection;
+        // If there is an Element after this one, use JoinNextDirection.
+        Element parent = paragraph.getParentElement();
+        if (parent.getElementCount() > parent.getElementIndex(offset) + 1)
+          return ElementSpec.JoinNextDirection;
+      }
+    else
+      {
+        // TODO: What to do here?
+      }
+    return ElementSpec.OriginateDirection;
+  }
+  
+  /**
+   * Updates the document structure in response to text removal. This is
+   * forwarded to the {@link ElementBuffer} of this document. Any changes to
+   * the document structure are added to the specified document event and
+   * sent to registered listeners.
+   *
+   * @param ev the document event that records the changes to the document
+   */
+  protected void removeUpdate(DefaultDocumentEvent ev)
+  {
+    super.removeUpdate(ev);
+    buffer.remove(ev.getOffset(), ev.getLength(), ev);
+  }
+
+  /**
    * Returns an enumeration of all style names.
    *
    * @return an enumeration of all style names
@@ -1316,6 +1922,35 @@ public class DefaultStyledDocument extends AbstractDocument
     // Nothing to do here. This is intended to be overridden by subclasses.
   }
 
+  void printElements (Element start, int pad)
+  {
+    for (int i = 0; i < pad; i++)
+      System.out.print(" ");
+    if (pad == 0)
+      System.out.println ("ROOT ELEMENT ("+start.getStartOffset()+", "+start.getEndOffset()+")");
+    else if (start instanceof AbstractDocument.BranchElement)
+      System.out.println ("BranchElement ("+start.getStartOffset()+", "+start.getEndOffset()+")");
+    else
+      {
+        {
+          try
+            {
+              System.out.println ("LeafElement ("+start.getStartOffset()+", "
+                                  + start.getEndOffset()+"): "+ 
+                                  start.getDocument().
+                                  getText(start.getStartOffset(), 
+                                          start.getEndOffset() - 
+                                          start.getStartOffset()));
+            }
+          catch (BadLocationException ble)
+            {
+            }
+        }
+      }
+    for (int i = 0; i < start.getElementCount(); i ++)
+      printElements (start.getElement(i), pad+3);
+  }
+  
   /**
    * Inserts a bulk of structured content at once.
    *
@@ -1325,36 +1960,53 @@ public class DefaultStyledDocument extends AbstractDocument
   protected void insert(int offset, ElementSpec[] data)
     throws BadLocationException
   {
-    writeLock();
-    // First we insert the content.
-    int index = offset;
-    for (int i = 0; i < data.length; i++)
+    if (data == null || data.length == 0)
+      return;
+    try
       {
-        ElementSpec spec = data[i];
-        if (spec.getArray() != null && spec.getLength() > 0)
+        // writeLock() and writeUnlock() should always be in a try/finally
+        // block so that locking balance is guaranteed even if some 
+        // exception is thrown.
+        writeLock();
+        
+        // First we collect the content to be inserted.
+        StringBuffer contentBuffer = new StringBuffer();
+        for (int i = 0; i < data.length; i++)
           {
-            String insertString = new String(spec.getArray(), spec.getOffset(),
-                                             spec.getLength());
-            content.insertString(index, insertString);
+            // Collect all inserts into one so we can get the correct
+            // ElementEdit
+            ElementSpec spec = data[i];
+            if (spec.getArray() != null && spec.getLength() > 0)
+              contentBuffer.append(spec.getArray(), spec.getOffset(),
+                                   spec.getLength());
           }
-        index += spec.getLength();
+
+        int length = contentBuffer.length();
+
+        // If there was no content inserted then exit early.
+        if (length == 0)
+          return;
+        
+        UndoableEdit edit = content.insertString(offset,
+                                                 contentBuffer.toString());
+
+        // Create the DocumentEvent with the ElementEdit added
+        DefaultDocumentEvent ev = 
+          new DefaultDocumentEvent(offset,
+                                   length,
+                                   DocumentEvent.EventType.INSERT);
+        ev.addEdit(edit);
+
+        // Finally we must update the document structure and fire the insert
+        // update event.
+        buffer.insert(offset, length, data, ev);
+        fireInsertUpdate(ev);
+        fireUndoableEditUpdate(new UndoableEditEvent(this, ev));
       }
-    // Update the view structure.
-    DefaultDocumentEvent ev = new DefaultDocumentEvent(offset, index - offset,
-                                               DocumentEvent.EventType.INSERT);
-    for (int i = 0; i < data.length; i++)
+    finally
       {
-        ElementSpec spec = data[i];
-        AttributeSet atts = spec.getAttributes();
-        if (atts != null)
-          insertUpdate(ev, atts);
+        writeUnlock();
       }
-
-    // Finally we must update the document structure and fire the insert update
-    // event.
-    buffer.insert(offset, index - offset, data, ev);
-    fireInsertUpdate(ev);
-    writeUnlock();
   }
 
   /**
@@ -1382,4 +2034,9 @@ public class DefaultStyledDocument extends AbstractDocument
         throw err;
       }
   }
+  
+  static boolean attributeSetsAreSame (AttributeSet a, AttributeSet b)
+  {
+    return (a == null && b == null) || (a != null && a.isEqual(b));
+  }
 }