]> git.lyx.org Git - features.git/commitdiff
Fix bug 880, or the multi-paragraph change tracking patch
authorMartin Vermeer <martin.vermeer@hut.fi>
Sat, 11 Mar 2006 13:31:41 +0000 (13:31 +0000)
committerMartin Vermeer <martin.vermeer@hut.fi>
Sat, 11 Mar 2006 13:31:41 +0000 (13:31 +0000)
git-svn-id: svn://svn.lyx.org/lyx/lyx-devel/trunk@13339 a592a061-630c-0410-9148-cb99ea01b6c8

src/ChangeLog
src/CutAndPaste.C
src/lyxfind.C
src/lyxtext.h
src/paragraph.C
src/paragraph.h
src/paragraph_funcs.C
src/paragraph_pimpl.C
src/paragraph_pimpl.h
src/text.C

index e46c766da7b9e9595844794ea63ba164ca69f51e..a0b9e02f18c86975fae697dab46e6850af68ac8e 100644 (file)
@@ -1,3 +1,19 @@
+2006-03-11  Martin Vermeer  <martin.vermeer@hut.fi>
+
+       * paragraph.[Ch] (write, lookupChange, lookupChangeFull;
+       added setChangeFull):
+       * paragraph_pimpl.[Ch] (trackChanges, cleanChanges, acceptChange,
+       rejectChange, erase; added setChangeFull):
+       * CutAndPaste.C (eraseSelectionHelper):
+       * lyxtext.h:
+       * text.C (readParToken, readParagraph, breakParagraph,
+       acceptChange, rejectChange, backspace, currentState; 
+       added backspacePos0):
+       * paragraph_funcs.C (breakParagraphConservative, mergeParagraph):
+       * lyxfind.C (findChange, findNextChange): fix bug 880: Change
+       tracked paragraphs should still allow a paragraph split (and related
+       things, i.e., multi-paragraph change tracking)
+
 2006-03-10  Martin Vermeer  <martin.vermeer@hut.fi>
 
        * BufferView_pimpl.C: fix bug 2212: First change is skipped is
index ce12c429600895f3577819d51bcb2e103b54b812..4dcf4f1806a1ca864c058f04d146dcd4bc1f4ce4 100644 (file)
@@ -271,64 +271,35 @@ PitPosPair eraseSelectionHelper(BufferParams const & params,
                return PitPosPair(endpit, endpos);
        }
 
-       bool all_erased = true;
-
-        // Clear fragments of the first par in selection
-       pars[startpit].erase(startpos, pars[startpit].size());
-       if (pars[startpit].size() != startpos)
-               all_erased = false;
-
-        // Clear fragments of the last par in selection
-       endpos -= pars[endpit].erase(0, endpos);
-       if (endpos != 0)
-               all_erased = false;
-
-        // Erase all the "middle" paragraphs.
-       if (params.tracking_changes) {
-               // Look through the deleted pars if any, erasing as needed
-               for (pit_type pit = startpit + 1; pit != endpit;) {
-                       // "erase" the contents of the par
-                       pars[pit].erase(0, pars[pit].size());
-                       if (pars[pit].empty()) {
-                               // remove the par if it's now empty
-                               pars.erase(pars.begin() + pit);
-                               --endpit;
-                       } else {
-                               ++pit;
-                               all_erased = false;
-                       }
-               }
-       } else {
-               pars.erase(pars.begin() + startpit + 1, pars.begin() + endpit);
-               endpit = startpit + 1;
-       }
-       
-#if 0 // FIXME: why for cut but not copy ?
-       // the cut selection should begin with standard layout
-       if (realcut) {
-               buf->params().clear();
-               buf->bibkey = 0;
-               buf->layout(textclasslist[buffer->params.textclass].defaultLayoutName());
-       }
-#endif
-
-       if (startpit + 1 == pit_type(pars.size()))
-               return PitPosPair(endpit, endpos);
-
-       if (doclear) {
-               pars[startpit + 1].stripLeadingSpaces();
-       }
-
-       // Merge first and last paragraph, if possible
-       if (all_erased &&
-           (pars[startpit].hasSameLayout(pars[startpit + 1]) ||
-            pars[startpit + 1].empty())) {
-               mergeParagraph(params, pars, startpit);
-               // This because endpar gets deleted here!
-               endpit = startpit;
-               endpos = startpos;
+       // A paragraph break has to be physically removed by merging, but
+       // only if either (1) change tracking is off, or (2) the para break
+       // is "blue"
+       for (pit_type pit = startpit; pit != endpit + 1;) {
+               bool const merge = !params.tracking_changes ||
+                       pars[pit].lookupChange(pars[pit].size()) ==
+                       Change::INSERTED;
+               pos_type const left  = ( pit == startpit ? startpos : 0 );
+               pos_type const right = ( pit == endpit ? endpos :
+                               pars[pit].size() + 1 );
+               // Logical erase only:
+               pars[pit].erase(left, right);
+               // Separate handling of para break:
+               if (merge && pit != endpit &&
+                  pars[pit].hasSameLayout(pars[pit + 1])) {
+                       pos_type const thissize = pars[pit].size();
+                       if (doclear)
+                               pars[pit + 1].stripLeadingSpaces();
+                       mergeParagraph(params, pars, pit);
+                       --endpit;
+                       if (pit == endpit)
+                               endpos += thissize;
+               } else 
+                       ++pit;
        }
 
+       // Ensure legal cursor pos:
+       endpit = startpit;
+       endpos = startpos;
        return PitPosPair(endpit, endpos);
 }
 
index 2f16fd3d6ce1dd27ef31f39009a68675d4d4bdf7..a88ef7e836fef67270b072ce6d3a4df323ddcde9 100644 (file)
@@ -127,9 +127,8 @@ bool findBackwards(DocIterator & cur, MatchString const & match)
 
 bool findChange(DocIterator & cur)
 {
-       for (; cur; cur.forwardChar())
-               if (cur.inTexted() && cur.pos() != cur.paragraph().size() &&
-                   cur.paragraph().lookupChange(cur.pos())
+       for (; cur; cur.forwardPos())
+               if (cur.inTexted() && cur.paragraph().lookupChange(cur.pos())
                    != Change::UNCHANGED)
                        return true;
        return false;
@@ -344,25 +343,21 @@ bool findNextChange(BufferView * bv)
        if (!findChange(cur))
                return false;
 
-       Paragraph const & par = cur.paragraph();
-       const pos_type pos = cur.pos();
-
-       Change orig_change = par.lookupChangeFull(pos);
-       const pos_type parsize = par.size();
-       pos_type end = pos;
+       bv->cursor().setCursor(cur);
+       bv->cursor().resetAnchor();
+       
+       Change orig_change = cur.paragraph().lookupChangeFull(cur.pos());
 
-       for (; end != parsize; ++end) {
-               Change change = par.lookupChangeFull(end);
+       DocIterator et = doc_iterator_end(cur.inset());
+       for (; cur != et; cur.forwardPosNoDescend()) {
+               Change change = cur.paragraph().lookupChangeFull(cur.pos());
                if (change != orig_change) {
-                       // slight UI optimisation: for replacements, we get
-                       // text like : _old_new. Consider that as one change.
-                       if (!(orig_change.type == Change::DELETED &&
-                               change.type == Change::INSERTED))
-                               break;
+                       break;
                }
        }
-       pos_type length = end - pos;
-       bv->putSelectionAt(cur, length, false);
+       // Now put cursor to end of selection:
+       bv->cursor().setCursor(cur);
+       bv->cursor().setSelection();
        // if we used a lfun like in find/replace, dispatch would do
        // that for us
        bv->update();
index db3b180bc1f921fbb95047d7d1e6d931d71ebbc9..30f17b8e8941a0d6998e9aa38a947b9b6cfa0e7a 100644 (file)
@@ -223,6 +223,8 @@ public:
        ///
        bool Delete(LCursor & cur);
        ///
+       bool backspacePos0(LCursor & cur);
+       ///
        bool backspace(LCursor & cur);
        ///
        bool selectWordWhenUnderCursor(LCursor & cur, lyx::word_location);
index 8f136352fe39d09bee51dee4eb5dbc1f14f06abf..2cbbf6180bc674c7bf4dcdebc529d706d81fc090 100644 (file)
@@ -159,12 +159,15 @@ void Paragraph::write(Buffer const & buf, ostream & os,
        lyx::time_type const curtime(lyx::current_time());
 
        int column = 0;
-       for (pos_type i = 0; i < size(); ++i) {
+       for (pos_type i = 0; i <= size(); ++i) {
 
                Change change = pimpl_->lookupChangeFull(i);
                Changes::lyxMarkChange(os, column, curtime, running_change, change);
                running_change = change;
 
+               if (i == size())
+                       break;
+
                // Write font changes
                LyXFont font2 = getFontSettings(bparams, i);
                if (font2 != font1) {
@@ -223,15 +226,6 @@ void Paragraph::write(Buffer const & buf, ostream & os,
                }
        }
 
-       // to make reading work properly
-       if (!size()) {
-               running_change = pimpl_->lookupChange(0);
-               Changes::lyxMarkChange(os, column, curtime,
-                       Change(Change::UNCHANGED), running_change);
-       }
-       Changes::lyxMarkChange(os, column, curtime,
-               running_change, Change(Change::UNCHANGED));
-
        os << "\n\\end_layout\n";
 }
 
@@ -1639,14 +1633,14 @@ void Paragraph::cleanChanges()
 
 Change::Type Paragraph::lookupChange(lyx::pos_type pos) const
 {
-       BOOST_ASSERT(empty() || pos < size());
+       BOOST_ASSERT(pos <= size());
        return pimpl_->lookupChange(pos);
 }
 
 
 Change const Paragraph::lookupChangeFull(lyx::pos_type pos) const
 {
-       BOOST_ASSERT(empty() || pos < size());
+       BOOST_ASSERT(pos <= size());
        return pimpl_->lookupChangeFull(pos);
 }
 
@@ -1669,6 +1663,12 @@ void Paragraph::setChange(lyx::pos_type pos, Change::Type type)
 }
 
 
+void Paragraph::setChangeFull(lyx::pos_type pos, Change change)
+{
+       pimpl_->setChangeFull(pos, change);
+}
+
+
 void Paragraph::markErased(bool erased)
 {
        pimpl_->markErased(erased);
index 3cf48bea426535ca04029fe7b9d2a0586f6a9e27..27038e0ff19b24df5f4b36a59c98f6bce9280caa 100644 (file)
@@ -224,6 +224,9 @@ public:
 
        /// set change at pos
        void setChange(lyx::pos_type pos, Change::Type type);
+       
+       /// set full change at pos
+       void setChangeFull(lyx::pos_type pos, Change change);
 
        /// accept change
        void acceptChange(lyx::pos_type start, lyx::pos_type end);
index 36bed60fe606d3bc7e436d655f35f15f245cc165..ff134379b254041af21ac7601d2c666b942e677d 100644 (file)
@@ -213,6 +213,9 @@ void breakParagraphConservative(BufferParams const & bparams,
                        if (moveItem(par, tmp, bparams, i, j - pos, change))
                                ++j;
                }
+               // Move over end-of-par change attr
+               tmp.setChange(tmp.size(), par.lookupChange(par.size()));
+
                // If tracking changes, set all the text that is to be
                // erased to Type::INSERTED.
                for (pos_type k = pos_end; k >= pos; --k) {
@@ -233,12 +236,15 @@ void mergeParagraph(BufferParams const & bparams,
        pos_type pos_end = next.size() - 1;
        pos_type pos_insert = par.size();
 
+       Change::Type cr = next.lookupChange(next.size());
        // ok, now copy the paragraph
        for (pos_type i = 0, j = 0; i <= pos_end; ++i) {
                Change::Type change = next.lookupChange(i);
                if (moveItem(next, par, bparams, i, pos_insert + j, change))
                        ++j;
        }
+       // Move the change status of "carriage return" over
+       par.setChange(par.size(), cr);
 
        pars.erase(pars.begin() + par_offset + 1);
 }
index 9547fc9838bf2883a664d2d1137a77585e9d4850..76065063c52eac785de001ddf912de7d9a45f0b0 100644 (file)
@@ -99,7 +99,7 @@ void Paragraph::Pimpl::trackChanges(Change::Type type)
        lyxerr[Debug::CHANGES] << "track changes for par "
                << id_ << " type " << type << endl;
        changes_.reset(new Changes(type));
-       changes_->set(type, 0, size());
+       changes_->set(type, 0, size() + 1);
 }
 
 
@@ -116,7 +116,7 @@ void Paragraph::Pimpl::cleanChanges()
                return;
 
        changes_.reset(new Changes(Change::INSERTED));
-       changes_->set(Change::INSERTED, 0, size());
+       changes_->set(Change::INSERTED, 0, size() + 1);
 }
 
 
@@ -147,6 +147,14 @@ void Paragraph::Pimpl::setChange(pos_type pos, Change::Type type)
 }
 
 
+void Paragraph::Pimpl::setChangeFull(pos_type pos, Change change)
+{
+       if (!tracking())
+               return;
+
+       changes_->set(change, pos);
+}
+
 Change::Type Paragraph::Pimpl::lookupChange(pos_type pos) const
 {
        if (!tracking())
@@ -204,10 +212,14 @@ void Paragraph::Pimpl::acceptChange(pos_type start, pos_type end)
                                break;
 
                        case Change::DELETED:
-                               eraseIntern(i);
-                               changes_->erase(i);
-                               --end;
-                               --i;
+                               // Suppress access to nonexistent 
+                               // "end-of-paragraph char":
+                               if (i < size()) {
+                                       eraseIntern(i);
+                                       changes_->erase(i);
+                                       --end;
+                                       --i;
+                               }
                                break;
                }
        }
@@ -235,15 +247,18 @@ void Paragraph::Pimpl::rejectChange(pos_type start, pos_type end)
                                break;
 
                        case Change::INSERTED:
-                               eraseIntern(i);
-                               changes_->erase(i);
-                               --end;
-                               --i;
+                               if (i < size()) {
+                                       eraseIntern(i);
+                                       changes_->erase(i);
+                                       --end;
+                                       --i;
+                               }
                                break;
 
                        case Change::DELETED:
                                changes_->set(Change::UNCHANGED, i);
-                               if (owner_->isInset(i))
+                               // No real char at position size():
+                               if (i < size() && owner_->isInset(i))
                                        owner_->getInset(i)->markErased(false);
                                break;
                }
@@ -351,7 +366,7 @@ void Paragraph::Pimpl::eraseIntern(pos_type pos)
 
 bool Paragraph::Pimpl::erase(pos_type pos)
 {
-       BOOST_ASSERT(pos < size());
+       BOOST_ASSERT(pos <= size());
 
        if (tracking()) {
                Change::Type changetype(changes_->lookup(pos));
@@ -359,14 +374,19 @@ bool Paragraph::Pimpl::erase(pos_type pos)
 
                // only allow the actual removal if it was /new/ text
                if (changetype != Change::INSERTED) {
-                       if (owner_->isInset(pos))
+                       if (pos < size() && owner_->isInset(pos))
                                owner_->getInset(pos)->markErased(true);
                        return false;
                }
        }
 
-       eraseIntern(pos);
-       return true;
+       // Don't physically access nonexistent end-of-paragraph char
+       if (pos < size()) {
+               eraseIntern(pos);
+               return true;
+       }
+
+       return false;
 }
 
 
index ed2e809270980647de46efe745c3d88b8b34b4e6..488e7e4deddcc0ab7f0a50ccecf936f06d7628ca 100644 (file)
@@ -54,6 +54,8 @@ public:
        bool isChangeEdited(lyx::pos_type start, lyx::pos_type end) const;
        /// set change at pos
        void setChange(lyx::pos_type pos, Change::Type type);
+       /// set full change at pos
+       void setChangeFull(lyx::pos_type pos, Change change);
        /// mark as erased
        void markErased(bool);
        /// accept change
index 3e3c2afea33459c3d55012c99a884c67abce8d12..d3f8b3b2e16fb8bc7f59a41d34fd7be02ef25564 100644 (file)
@@ -315,8 +315,10 @@ void readParToken(Buffer const & buf, Paragraph & par, LyXLex & lex,
        } else if (token == "\\change_unchanged") {
                // Hack ! Needed for empty paragraphs :/
                // FIXME: is it still ??
+               /*
                if (!par.size())
                        par.cleanChanges();
+               */
                change = Change(Change::UNCHANGED);
        } else if (token == "\\change_inserted") {
                lex.eatLine();
@@ -375,6 +377,9 @@ void readParagraph(Buffer const & buf, Paragraph & par, LyXLex & lex)
                        break;
                }
        }
+       // Final change goes to paragraph break:
+       par.setChangeFull(par.size(), change);
+       
        // Initialize begin_of_body_ on load; redoParagraph maintains
        par.setBeginOfBody();
 }
@@ -1026,14 +1031,10 @@ namespace {
 void LyXText::breakParagraph(LCursor & cur, bool keep_layout)
 {
        BOOST_ASSERT(this == cur.text());
-       // allow only if at start or end, or all previous is new text
+
        Paragraph & cpar = cur.paragraph();
        pit_type cpit = cur.pit();
 
-       if (cur.pos() != 0 && cur.pos() != cur.lastpos()
-           && cpar.isChangeEdited(0, cur.pos()))
-               return;
-
        LyXTextClass const & tclass = cur.buffer().params().getLyXTextClass();
        LyXLayout_ptr const & layout = cpar.layout();
 
@@ -1088,6 +1089,12 @@ void LyXText::breakParagraph(LCursor & cur, bool keep_layout)
 
        updateCounters(cur.buffer());
 
+       // Mark "carriage return" as inserted if change tracking:
+       if (cur.buffer().params().tracking_changes) {
+               cur.paragraph().setChange(cur.paragraph().size(), 
+                       Change::INSERTED);
+       }
+
        // This check is necessary. Otherwise the new empty paragraph will
        // be deleted automatically. And it is more friendly for the user!
        if (cur.pos() != 0 || isempty)
@@ -1392,18 +1399,34 @@ void LyXText::acceptChange(LCursor & cur)
        if (!cur.selection() && cur.lastpos() != 0)
                return;
 
-       CursorSlice const & startc = cur.selBegin();
-       CursorSlice const & endc = cur.selEnd();
-       if (startc.pit() == endc.pit()) {
-               recordUndoSelection(cur, Undo::INSERT);
-               pars_[startc.pit()].acceptChange(startc.pos(), endc.pos());
-               finishUndo();
-               cur.clearSelection();
-               setCursorIntern(cur, startc.pit(), 0);
+       recordUndoSelection(cur, Undo::INSERT);
+       
+       DocIterator it = cur.selectionBegin();
+       DocIterator et = cur.selectionEnd();
+       pit_type pit = it.pit();
+       Change::Type const type = pars_[pit].lookupChange(it.pos());
+       for (; pit <= et.pit(); ++pit) {
+               pos_type left  = ( pit == it.pit() ? it.pos() : 0 );
+               pos_type right = 
+                   ( pit == et.pit() ? et.pos() : pars_[pit].size() + 1 );
+               pars_[pit].acceptChange(left, right);
        }
-#ifdef WITH_WARNINGS
-#warning handle multi par selection
-#endif
+       if (type == Change::DELETED) {
+               ParagraphList & plist = paragraphs();
+               if (it.pit() + 1 < et.pit())
+                       pars_.erase(plist.begin() + it.pit() + 1,
+                                   plist.begin() + et.pit());
+               
+               // Paragraph merge if appropriate:
+               if (pars_[it.pit()].lookupChange(pars_[it.pit()].size()) 
+                       == Change::DELETED) {
+                       setCursorIntern(cur, it.pit() + 1, 0);
+                       backspacePos0(cur);
+               }
+       }
+       finishUndo();
+       cur.clearSelection();
+       setCursorIntern(cur, it.pit(), 0);
 }
 
 
@@ -1413,18 +1436,33 @@ void LyXText::rejectChange(LCursor & cur)
        if (!cur.selection() && cur.lastpos() != 0)
                return;
 
-       CursorSlice const & startc = cur.selBegin();
-       CursorSlice const & endc = cur.selEnd();
-       if (startc.pit() == endc.pit()) {
-               recordUndoSelection(cur, Undo::INSERT);
-               pars_[startc.pit()].rejectChange(startc.pos(), endc.pos());
-               finishUndo();
-               cur.clearSelection();
-               setCursorIntern(cur, startc.pit(), 0);
+       recordUndoSelection(cur, Undo::INSERT);
+
+       DocIterator it = cur.selectionBegin();
+       DocIterator et = cur.selectionEnd();
+       pit_type pit = it.pit();
+       Change::Type const type = pars_[pit].lookupChange(it.pos());
+       for (; pit <= et.pit(); ++pit) {
+               pos_type left  = ( pit == it.pit() ? it.pos() : 0 );
+               pos_type right = 
+                   ( pit == et.pit() ? et.pos() : pars_[pit].size() + 1 );
+               pars_[pit].rejectChange(left, right);
        }
-#ifdef WITH_WARNINGS
-#warning handle multi par selection
-#endif
+       if (type == Change::INSERTED) {
+               ParagraphList & plist = paragraphs();
+               if (it.pit() + 1 < et.pit())
+                       pars_.erase(plist.begin() + it.pit() + 1,
+                                   plist.begin() + et.pit());
+               // Paragraph merge if appropriate:
+               if (pars_[it.pit()].lookupChange(pars_[it.pit()].size()) 
+                       == Change::INSERTED) {
+                       setCursorIntern(cur, it.pit() + 1, 0);
+                       backspacePos0(cur);
+               }
+       }
+       finishUndo();
+       cur.clearSelection();
+       setCursorIntern(cur, it.pit(), 0);
 }
 
 
@@ -1556,86 +1594,100 @@ bool LyXText::Delete(LCursor & cur)
 }
 
 
-bool LyXText::backspace(LCursor & cur)
+bool LyXText::backspacePos0(LCursor & cur)
 {
        BOOST_ASSERT(this == cur.text());
        bool needsUpdate = false;
-       if (cur.pos() == 0) {
-               // The cursor is at the beginning of a paragraph, so
-               // the the backspace will collapse two paragraphs into
-               // one.
 
-               // but it's not allowed unless it's new
-               Paragraph & par = cur.paragraph();
-               if (par.isChangeEdited(0, par.size()))
-                       return false;
+       Paragraph & par = cur.paragraph();
+       // is it an empty paragraph?
+       pos_type lastpos = cur.lastpos();
+       if (lastpos == 0 || (lastpos == 1 && par.isSeparator(0))) {
+               // This is an empty paragraph and we delete it just
+               // by moving the cursor one step
+               // left and let the DeleteEmptyParagraphMechanism
+               // handle the actual deletion of the paragraph.
 
-               // we may paste some paragraphs
-
-               // is it an empty paragraph?
-               pos_type lastpos = cur.lastpos();
-               if (lastpos == 0 || (lastpos == 1 && par.isSeparator(0))) {
-                       // This is an empty paragraph and we delete it just
-                       // by moving the cursor one step
-                       // left and let the DeleteEmptyParagraphMechanism
-                       // handle the actual deletion of the paragraph.
-
-                       if (cur.pit() != 0) {
-                                // For KeepEmpty layouts we need to get
-                                // rid of the keepEmpty setting first.
-                                // And the only way to do this is to
-                                // reset the layout to something
-                                // else: f.ex. the default layout.
-                                if (par.allowEmpty()) {
-                                        Buffer & buf = cur.buffer();
-                                        BufferParams const & bparams = buf.params();
-                                        par.layout(bparams.getLyXTextClass().defaultLayout());
-                                }
-                                
-                               cursorLeft(cur);
-                               return true;
+               if (cur.pit() != 0) {
+                       // For KeepEmpty layouts we need to get
+                       // rid of the keepEmpty setting first.
+                       // And the only way to do this is to
+                       // reset the layout to something
+                       // else: f.ex. the default layout.
+                       if (par.allowEmpty()) {
+                               Buffer & buf = cur.buffer();
+                               BufferParams const & bparams = buf.params();
+                               par.layout(bparams.getLyXTextClass().defaultLayout());
                        }
+                                
+                       cursorLeft(cur);
+                       return true;
                }
+       }
 
-               if (cur.pit() != 0)
-                       recordUndo(cur, Undo::DELETE, cur.pit() - 1);
+       if (cur.pit() != 0)
+               recordUndo(cur, Undo::DELETE, cur.pit() - 1);
+
+       pit_type tmppit = cur.pit();
+       // We used to do cursorLeftIntern() here, but it is
+       // not a good idea since it triggers the auto-delete
+       // mechanism. So we do a cursorLeftIntern()-lite,
+       // without the dreaded mechanism. (JMarc)
+       if (cur.pit() != 0) {
+               // steps into the above paragraph.
+               setCursorIntern(cur, cur.pit() - 1,
+                               pars_[cur.pit() - 1].size(),
+                               false);
+       }
 
-               pit_type tmppit = cur.pit();
-               // We used to do cursorLeftIntern() here, but it is
-               // not a good idea since it triggers the auto-delete
-               // mechanism. So we do a cursorLeftIntern()-lite,
-               // without the dreaded mechanism. (JMarc)
-               if (cur.pit() != 0) {
-                       // steps into the above paragraph.
-                       setCursorIntern(cur, cur.pit() - 1,
-                                       pars_[cur.pit() - 1].size(),
-                                       false);
-               }
+       // Pasting is not allowed, if the paragraphs have different
+       // layout. I think it is a real bug of all other
+       // word processors to allow it. It confuses the user.
+       // Correction: Pasting is always allowed with standard-layout
+       // Correction (Jug 20050717): Remove check about alignment!
+       Buffer & buf = cur.buffer();
+       BufferParams const & bufparams = buf.params();
+       LyXTextClass const & tclass = bufparams.getLyXTextClass();
+       pit_type const cpit = cur.pit();
 
-               // Pasting is not allowed, if the paragraphs have different
-               // layout. I think it is a real bug of all other
-               // word processors to allow it. It confuses the user.
-               // Correction: Pasting is always allowed with standard-layout
-               // Correction (Jug 20050717): Remove check about alignment!
-               Buffer & buf = cur.buffer();
-               BufferParams const & bufparams = buf.params();
-               LyXTextClass const & tclass = bufparams.getLyXTextClass();
-               pit_type const cpit = cur.pit();
-
-               if (cpit != tmppit
-                   && (pars_[cpit].layout() == pars_[tmppit].layout()
-                       || pars_[tmppit].layout() == tclass.defaultLayout()))
-               {
-                       mergeParagraph(bufparams, pars_, cpit);
-                       needsUpdate = true;
+       if (cpit != tmppit
+           && (pars_[cpit].layout() == pars_[tmppit].layout()
+               || pars_[tmppit].layout() == tclass.defaultLayout()))
+       {
+               mergeParagraph(bufparams, pars_, cpit);
+               needsUpdate = true;
 
-                       if (cur.pos() != 0 && pars_[cpit].isSeparator(cur.pos() - 1))
+               if (cur.pos() != 0 && pars_[cpit].isSeparator(cur.pos() - 1))
                                --cur.pos();
 
-                       // the counters may have changed
-                       updateCounters(cur.buffer());
-                       setCursor(cur, cur.pit(), cur.pos(), false);
+               // the counters may have changed
+               updateCounters(cur.buffer());
+               setCursor(cur, cur.pit(), cur.pos(), false);
+       }
+       return needsUpdate;
+}
+
+
+bool LyXText::backspace(LCursor & cur)
+{
+       BOOST_ASSERT(this == cur.text());
+       bool needsUpdate = false;
+       if (cur.pos() == 0) {
+               // The cursor is at the beginning of a paragraph, so
+               // the the backspace will collapse two paragraphs into
+               // one.
+
+               if (cur.buffer().params().tracking_changes) {
+                       // Previous paragraph, mark "carriage return" as
+                       // deleted:
+                       Paragraph & par = pars_[cur.pit() - 1];
+                       par.setChange(par.size(), Change::DELETED);
+                       setCursorIntern(cur, cur.pit() - 1, par.size());
+                       return false;
                }
+
+               needsUpdate = backspacePos0(cur);
+
        } else {
                // this is the code for a normal backspace, not pasting
                // any paragraphs
@@ -2182,9 +2234,11 @@ string LyXText::currentState(LCursor & cur)
        std::ostringstream os;
 
        bool const show_change = buf.params().tracking_changes
-               && cur.pos() != cur.lastpos()
                && par.lookupChange(cur.pos()) != Change::UNCHANGED;
 
+       if (buf.params().tracking_changes)
+               os << "[C] ";
+
        if (show_change) {
                Change change = par.lookupChangeFull(cur.pos());
                Author const & a = buf.params().authors().get(change.author);