X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FUndo.cpp;h=143af4e83ff0b9f29eac7ecc2f1abbf5e6c8ceb4;hb=28be7d552f62cc02fa86d7f79201d089bfb2d7b5;hp=20c76462e3b3324b5ab5e914df53e09ff1c89fcf;hpb=b06797a3125393b64c9d6d02eb4dfd66c30c8dd2;p=lyx.git diff --git a/src/Undo.cpp b/src/Undo.cpp index 20c76462e3..143af4e83f 100644 --- a/src/Undo.cpp +++ b/src/Undo.cpp @@ -4,10 +4,11 @@ * Licence details can be found in the file COPYING. * * \author Asger Alstrup - * \author Lars Gullik Bjønnes + * \author Lars Gullik Bjønnes * \author John Levon - * \author André Pönitz - * \author Jürgen Vigna + * \author André Pönitz + * \author Jürgen Vigna + * \author Abdelrazak Younes * * Full author contact details are available in file CREDITS. */ @@ -17,43 +18,288 @@ #include "Undo.h" #include "Buffer.h" +#include "BufferParams.h" #include "buffer_funcs.h" #include "Cursor.h" -#include "debug.h" -#include "BufferView.h" -#include "Text.h" +#include "CutAndPaste.h" +#include "ErrorList.h" #include "Paragraph.h" #include "ParagraphList.h" +#include "Text.h" #include "mathed/MathSupport.h" #include "mathed/MathData.h" #include "insets/Inset.h" +#include "insets/InsetText.h" + +#include "support/debug.h" +#include "support/gettext.h" +#include "support/lassert.h" +#include "support/lyxtime.h" #include +#include + +using namespace std; +using namespace lyx::support; namespace lyx { -using std::advance; -using std::endl; +/** +These are the elements put on the undo stack. Each object contains +complete paragraphs from some cell and sufficient information to +restore the cursor state. + +The cell is given by a DocIterator pointing to this cell, the +'interesting' range of paragraphs by counting them from begin and end +of cell, respectively. + +The cursor is also given as DocIterator and should point to some place +in the stored paragraph range. In case of math, we simply store the +whole cell, as there usually is just a simple paragraph in a cell. + +The idea is to store the contents of 'interesting' paragraphs in some +structure ('Undo') _before_ it is changed in some edit operation. +Obviously, the stored range should be as small as possible. However, +there is a lower limit: The StableDocIterator stored in the undo class +must be valid after the changes, too, as it will used as a pointer +where to insert the stored bits when performining undo. +*/ +struct UndoElement +{ + /// + UndoElement(UndoKind kin, CursorData const & cb, + StableDocIterator const & cel, + pit_type fro, pit_type en, ParagraphList * pl, MathData * ar, + bool lc, size_t gid) : + kind(kin), cur_before(cb), cell(cel), from(fro), end(en), + pars(pl), array(ar), bparams(0), + lyx_clean(lc), group_id(gid), time(current_time()) + { + } + /// + UndoElement(CursorData const & cb, BufferParams const & bp, + bool lc, size_t gid) : + kind(ATOMIC_UNDO), cur_before(cb), cell(), from(0), end(0), + pars(0), array(0), bparams(new BufferParams(bp)), + lyx_clean(lc), group_id(gid), time(current_time()) + { + } + /// + UndoElement(UndoElement const & ue) : time(current_time()) + { + kind = ue.kind; + cur_before = ue.cur_before; + cur_after = ue.cur_after; + cell = ue.cell; + from = ue.from; + end = ue.end; + pars = ue.pars; + array = ue.array; + bparams = ue.bparams + ? new BufferParams(*ue.bparams) : 0; + lyx_clean = ue.lyx_clean; + group_id = ue.group_id; + } + /// + ~UndoElement() + { + if (bparams) + delete bparams; + } + /// Which kind of operation are we recording for? + UndoKind kind; + /// the position of the cursor before recordUndo + CursorData cur_before; + /// the position of the cursor at the end of the undo group + CursorData cur_after; + /// the position of the cell described + StableDocIterator cell; + /// counted from begin of cell + pit_type from; + /// complement to end of this cell + pit_type end; + /// the contents of the saved Paragraphs (for texted) + ParagraphList * pars; + /// the contents of the saved MathData (for mathed) + MathData * array; + /// Only used in case of params undo + BufferParams const * bparams; + /// Was the buffer clean at this point? + bool lyx_clean; + /// the element's group id + size_t group_id; + /// timestamp + time_t time; +private: + /// Protect construction + UndoElement(); +}; + + +class UndoElementStack +{ +public: + /// limit is the maximum size of the stack + UndoElementStack(size_t limit = 100) { limit_ = limit; } + /// limit is the maximum size of the stack + ~UndoElementStack() { clear(); } + + /// Return the top element. + UndoElement & top() { return c_.front(); } + + /// Pop and throw away the top element. + void pop() { c_.pop_front(); } + + /// Return true if the stack is empty. + bool empty() const { return c_.empty(); } + + /// Clear all elements, deleting them. + void clear() { + for (size_t i = 0; i != c_.size(); ++i) { + delete c_[i].array; + delete c_[i].pars; + } + c_.clear(); + } + + /// Push an item on to the stack, deleting the bottom group on + /// overflow. + void push(UndoElement const & v) { + // Remove some entries if the limit has been reached. + // However, if the only group on the stack is the one + // we are currently populating, do nothing. + if (c_.size() >= limit_ + && c_.front().group_id != v.group_id) { + // remove a whole group at once. + const size_t gid = c_.back().group_id; + while (!c_.empty() && c_.back().group_id == gid) + c_.pop_back(); + } + c_.push_front(v); + } + + /// Mark all the elements of the stack as dirty + void markDirty() { + for (size_t i = 0; i != c_.size(); ++i) + c_[i].lyx_clean = false; + } + +private: + /// Internal contents. + std::deque c_; + /// The maximum number elements stored. + size_t limit_; +}; + + +struct Undo::Private +{ + Private(Buffer & buffer) : buffer_(buffer), undo_finished_(true), + group_id_(0), group_level_(0) {} + + // Do one undo/redo step + void doTextUndoOrRedo(CursorData & cur, UndoElementStack & stack, + UndoElementStack & otherStack); + // Apply one undo/redo group. Returns false if no undo possible. + bool textUndoOrRedo(CursorData & cur, bool isUndoOperation); + + /// + void doRecordUndo(UndoKind kind, + DocIterator const & cell, + pit_type first_pit, + pit_type last_pit, + CursorData const & cur, + UndoElementStack & stack); + /// + void recordUndo(UndoKind kind, + DocIterator const & cell, + pit_type first_pit, + pit_type last_pit, + CursorData const & cur); + /// + void doRecordUndoBufferParams(CursorData const & cur, UndoElementStack & stack); + /// + void recordUndoBufferParams(CursorData const & cur); + + /// + Buffer & buffer_; + /// Undo stack. + UndoElementStack undostack_; + /// Redo stack. + UndoElementStack redostack_; + + /// The flag used by Undo::finishUndo(). + bool undo_finished_; + + /// Current group Id. + size_t group_id_; + /// Current group nesting nevel. + size_t group_level_; + /// the position of cursor before the group was created + CursorData group_cur_before_; +}; + + +///////////////////////////////////////////////////////////////////// +// +// Undo +// +///////////////////////////////////////////////////////////////////// + + +Undo::Undo(Buffer & buffer) + : d(new Undo::Private(buffer)) +{} + + +Undo::~Undo() +{ + delete d; +} + + +void Undo::clear() +{ + d->undostack_.clear(); + d->redostack_.clear(); + d->undo_finished_ = true; + // We used to do that, but I believe it is better to keep + // groups (only used in Buffer::reload for now (JMarc) + //d->group_id_ = 0; + //d->group_level_ = 0; +} + +bool Undo::hasUndoStack() const +{ + return !d->undostack_.empty(); +} -namespace { -/// The flag used by finishUndo(). -bool undo_finished; +bool Undo::hasRedoStack() const +{ + return !d->redostack_.empty(); +} -std::ostream & operator<<(std::ostream & os, Undo const & undo) +void Undo::markDirty() { - return os << " from: " << undo.from << " end: " << undo.end - << " cell:\n" << undo.cell - << " cursor:\n" << undo.cursor; + d->undo_finished_ = true; + d->undostack_.markDirty(); + d->redostack_.markDirty(); } -bool samePar(StableDocIterator const & i1, StableDocIterator const & i2) +///////////////////////////////////////////////////////////////////// +// +// Undo::Private +// +/////////////////////////////////////////////////////////////////////// + +static bool samePar(StableDocIterator const & i1, StableDocIterator const & i2) { StableDocIterator tmpi2 = i2; tmpi2.pos() = i1.pos(); @@ -61,53 +307,59 @@ bool samePar(StableDocIterator const & i1, StableDocIterator const & i2) } -void doRecordUndo(Undo::undo_kind kind, +void Undo::Private::doRecordUndo(UndoKind kind, DocIterator const & cell, pit_type first_pit, pit_type last_pit, - DocIterator const & cur, - BufferParams const & bparams, - bool isFullBuffer, - limited_stack & stack) + CursorData const & cur_before, + UndoElementStack & stack) { + if (!group_level_) { + LYXERR0("There is no group open (creating one)"); + ++group_id_; + } + if (first_pit > last_pit) - std::swap(first_pit, last_pit); - // create the position information of the Undo entry - Undo undo; - undo.array = 0; - undo.pars = 0; - undo.kind = kind; - undo.cell = cell; - undo.cursor = cur; - undo.bparams = bparams ; - undo.isFullBuffer = isFullBuffer; - //lyxerr << "recordUndo: cur: " << cur << endl; - //lyxerr << "recordUndo: pos: " << cur.pos() << endl; - //lyxerr << "recordUndo: cell: " << cell << endl; - undo.from = first_pit; - undo.end = cell.lastpit() - last_pit; + swap(first_pit, last_pit); // Undo::ATOMIC are always recorded (no overlapping there). // As nobody wants all removed character appear one by one when undoing, // we want combine 'similar' non-ATOMIC undo recordings to one. - if (!undo_finished - && kind != Undo::ATOMIC + pit_type from = first_pit; + pit_type end = cell.lastpit() - last_pit; + if (!undo_finished_ + && kind != ATOMIC_UNDO && !stack.empty() - && samePar(stack.top().cell, undo.cell) - && stack.top().kind == undo.kind - && stack.top().from == undo.from - && stack.top().end == undo.end) + && !stack.top().bparams + && samePar(stack.top().cell, cell) + && stack.top().kind == kind + && stack.top().from == from + && stack.top().end == end + && stack.top().cur_after == cur_before + && current_time() - stack.top().time <= 2) { + // reset cur_after; it will be filled correctly by endUndoGroup. + stack.top().cur_after = CursorData(); + // update the timestamp of the undo element + stack.top().time = current_time(); return; + } + + LYXERR(Debug::UNDO, "Create undo element of group " << group_id_); + // create the position information of the Undo entry + UndoElement undo(kind, + group_cur_before_.empty() ? cur_before : group_cur_before_, + cell, from, end, 0, 0, buffer_.isClean(), group_id_); // fill in the real data to be saved if (cell.inMathed()) { // simply use the whole cell - undo.array = new MathData(cell.cell()); + MathData & ar = cell.cell(); + undo.array = new MathData(ar.buffer(), ar.begin(), ar.end()); } else { // some more effort needed here as 'the whole cell' of the // main Text _is_ the whole document. // record the relevant paragraphs Text const * text = cell.text(); - BOOST_ASSERT(text); + LBUFERR(text); ParagraphList const & plist = text->paragraphs(); ParagraphList::const_iterator first = plist.begin(); advance(first, first_pit); @@ -118,76 +370,113 @@ void doRecordUndo(Undo::undo_kind kind, // push the undo entry to undo stack stack.push(undo); - //lyxerr << "undo record: " << stack.top() << std::endl; + //lyxerr << "undo record: " << stack.top() << endl; +} + + +void Undo::Private::recordUndo(UndoKind kind, + DocIterator const & cell, + pit_type first_pit, pit_type last_pit, + CursorData const & cur) +{ + LASSERT(first_pit <= cell.lastpit(), return); + LASSERT(last_pit <= cell.lastpit(), return); + + doRecordUndo(kind, cell, first_pit, last_pit, cur, + undostack_); // next time we'll try again to combine entries if possible - undo_finished = false; + undo_finished_ = false; + + // If we ran recordUndo, it means that we plan to change the buffer + buffer_.markDirty(); + + redostack_.clear(); } -void recordUndo(Undo::undo_kind kind, - Cursor & cur, pit_type first_pit, pit_type last_pit, - limited_stack & stack) +void Undo::Private::doRecordUndoBufferParams(CursorData const & cur_before, + UndoElementStack & stack) { - BOOST_ASSERT(first_pit <= cur.lastpit()); - BOOST_ASSERT(last_pit <= cur.lastpit()); + if (!group_level_) { + LYXERR0("There is no group open (creating one)"); + ++group_id_; + } - doRecordUndo(kind, cur, first_pit, last_pit, cur, - cur.bv().buffer()->params(), false, stack); -} + LYXERR(Debug::UNDO, "Create full buffer undo element of group " << group_id_); + // create the position information of the Undo entry + UndoElement undo(group_cur_before_.empty() ? cur_before : group_cur_before_, + buffer_.params(), buffer_.isClean(), + group_id_); + // push the undo entry to undo stack + stack.push(undo); +} -// Returns false if no undo possible. -bool textUndoOrRedo(BufferView & bv, - limited_stack & stack, limited_stack & otherstack) +void Undo::Private::recordUndoBufferParams(CursorData const & cur) { - finishUndo(); + doRecordUndoBufferParams(cur, undostack_); - if (stack.empty()) { - // Nothing to do. - return false; - } + // next time we'll try again to combine entries if possible + undo_finished_ = false; + // If we ran recordUndo, it means that we plan to change the buffer + buffer_.markDirty(); + + redostack_.clear(); +} + + +void Undo::Private::doTextUndoOrRedo(CursorData & cur, UndoElementStack & stack, UndoElementStack & otherstack) +{ // Adjust undo stack and get hold of current undo data. - Undo undo = stack.top(); - stack.pop(); + UndoElement & undo = stack.top(); + LYXERR(Debug::UNDO, "Undo element of group " << undo.group_id); + // We'll pop the stack only when we're done with this element. So do NOT + // try to return early. // We will store in otherstack the part of the document under 'undo' - Buffer * buf = bv.buffer(); - DocIterator cell_dit = undo.cell.asDocIterator(&buf->inset()); - - doRecordUndo(Undo::ATOMIC, cell_dit, - undo.from, cell_dit.lastpit() - undo.end, bv.cursor(), - undo.bparams, undo.isFullBuffer, - otherstack); + DocIterator cell_dit = undo.cell.asDocIterator(&buffer_); + + if (undo.bparams) + doRecordUndoBufferParams(undo.cur_after, otherstack); + else { + LATTEST(undo.end <= cell_dit.lastpit()); + doRecordUndo(ATOMIC_UNDO, cell_dit, + undo.from, cell_dit.lastpit() - undo.end, undo.cur_after, + otherstack); + } + otherstack.top().cur_after = undo.cur_before; // This does the actual undo/redo. - //lyxerr << "undo, performing: " << undo << std::endl; - bool labelsUpdateNeeded = false; - DocIterator dit = undo.cell.asDocIterator(&buf->inset()); - if (undo.isFullBuffer) { - BOOST_ASSERT(undo.pars); - // This is a full document - otherstack.top().bparams = buf->params(); - buf->params() = undo.bparams; - std::swap(buf->paragraphs(), *undo.pars); - delete undo.pars; - undo.pars = 0; + //LYXERR0("undo, performing: " << undo); + DocIterator dit = undo.cell.asDocIterator(&buffer_); + if (undo.bparams) { + // This is a params undo element + delete otherstack.top().bparams; + otherstack.top().bparams = new BufferParams(buffer_.params()); + DocumentClassConstPtr olddc = buffer_.params().documentClassPtr(); + buffer_.params() = *undo.bparams; + // The error list is not supposed to be helpful here. + ErrorList el; + cap::switchBetweenClasses(olddc, buffer_.params().documentClassPtr(), + static_cast(buffer_.inset()), el); + LATTEST(el.empty()); } else if (dit.inMathed()) { // We stored the full cell here as there is not much to be // gained by storing just 'a few' paragraphs (most if not // all math inset cells have just one paragraph!) - //lyxerr << "undo.array: " << *undo.array <paragraphs(); // remove new stuff between first and last @@ -210,108 +499,159 @@ bool textUndoOrRedo(BufferView & bv, plist.insert(first, undo.pars->begin(), undo.pars->end()); delete undo.pars; undo.pars = 0; - labelsUpdateNeeded = true; } - BOOST_ASSERT(undo.pars == 0); - BOOST_ASSERT(undo.array == 0); - - // Set cursor - Cursor & cur = bv.cursor(); - cur.setCursor(undo.cursor.asDocIterator(&buf->inset())); - cur.selection() = false; - cur.resetAnchor(); - cur.fixIfBroken(); - - if (labelsUpdateNeeded) - updateLabels(*buf); - finishUndo(); - return true; + + // We'll clean up in release mode. + LASSERT(undo.pars == 0, undo.pars = 0); + LASSERT(undo.array == 0, undo.array = 0); + + if (!undo.cur_before.empty()) + cur = undo.cur_before; + if (undo.lyx_clean) + buffer_.markClean(); + else + buffer_.markDirty(); + // Now that we're done with undo, we pop it off the stack. + stack.pop(); } -} // namespace anon +bool Undo::Private::textUndoOrRedo(CursorData & cur, bool isUndoOperation) +{ + undo_finished_ = true; + + UndoElementStack & stack = isUndoOperation ? undostack_ : redostack_; + + if (stack.empty()) + // Nothing to do. + return false; -void finishUndo() + UndoElementStack & otherstack = isUndoOperation ? redostack_ : undostack_; + + const size_t gid = stack.top().group_id; + while (!stack.empty() && stack.top().group_id == gid) + doTextUndoOrRedo(cur, stack, otherstack); + + // Adapt the new material to current buffer. + buffer_.setBuffersForInsets(); // FIXME This shouldn't be here. + return true; +} + + +void Undo::finishUndo() { // Make sure the next operation will be stored. - undo_finished = true; + d->undo_finished_ = true; } -bool textUndo(BufferView & bv) +bool Undo::textUndo(CursorData & cur) { - return textUndoOrRedo(bv, bv.buffer()->undostack(), - bv.buffer()->redostack()); + return d->textUndoOrRedo(cur, true); } -bool textRedo(BufferView & bv) +bool Undo::textRedo(CursorData & cur) { - return textUndoOrRedo(bv, bv.buffer()->redostack(), - bv.buffer()->undostack()); + return d->textUndoOrRedo(cur, false); } -void recordUndo(Undo::undo_kind kind, - Cursor & cur, pit_type first, pit_type last) +void Undo::beginUndoGroup() { - Buffer * buf = cur.bv().buffer(); - recordUndo(kind, cur, first, last, buf->undostack()); - buf->redostack().clear(); - //lyxerr << "undostack:\n"; - //for (size_t i = 0, n = buf->undostack().size(); i != n && i < 6; ++i) - // lyxerr << " " << i << ": " << buf->undostack()[i] << std::endl; + if (d->group_level_ == 0) { + // create a new group + ++d->group_id_; + LYXERR(Debug::UNDO, "+++++++Creating new group " << d->group_id_); + } + ++d->group_level_; } -void recordUndo(Cursor & cur, Undo::undo_kind kind) +void Undo::beginUndoGroup(CursorData const & cur_before) { - recordUndo(kind, cur, cur.pit(), cur.pit()); + beginUndoGroup(); + if (d->group_cur_before_.empty()) + d->group_cur_before_ = cur_before; } -void recordUndoInset(Cursor & cur, Undo::undo_kind kind) +void Undo::endUndoGroup() { - Cursor c = cur; - c.pop(); - Buffer * buf = cur.bv().buffer(); - doRecordUndo(kind, c, c.pit(), c.pit(), cur, - buf->params(), false, buf->undostack()); + if (d->group_level_ == 0) { + LYXERR0("There is no undo group to end here"); + return; + } + --d->group_level_; + if (d->group_level_ == 0) { + // real end of the group + d->group_cur_before_ = CursorData(); + LYXERR(Debug::UNDO, "-------End of group " << d->group_id_); + } } -void recordUndoSelection(Cursor & cur, Undo::undo_kind kind) +void Undo::endUndoGroup(CursorData const & cur_after) { - recordUndo(kind, cur, cur.selBegin().pit(), cur.selEnd().pit()); + endUndoGroup(); + if (!d->undostack_.empty() && d->undostack_.top().cur_after.empty()) + d->undostack_.top().cur_after = cur_after; } -void recordUndo(Cursor & cur, Undo::undo_kind kind, pit_type from) +void Undo::recordUndo(CursorData const & cur, UndoKind kind) { - recordUndo(kind, cur, cur.pit(), from); + d->recordUndo(kind, cur, cur.pit(), cur.pit(), cur); } -void recordUndo(Cursor & cur, Undo::undo_kind kind, - pit_type from, pit_type to) +void Undo::recordUndo(CursorData const & cur, pit_type from, pit_type to) { - recordUndo(kind, cur, from, to); + d->recordUndo(ATOMIC_UNDO, cur, from, to, cur); } -void recordUndoFullDocument(BufferView * bv) +void Undo::recordUndoInset(CursorData const & cur, Inset const * inset) { - Buffer * buf = bv->buffer(); - doRecordUndo( - Undo::ATOMIC, - doc_iterator_begin(buf->inset()), - 0, buf->paragraphs().size() - 1, - bv->cursor(), - buf->params(), - true, - buf->undostack() - ); - undo_finished = false; + if (!inset || inset == &cur.inset()) { + DocIterator c = cur; + c.pop_back(); + d->recordUndo(ATOMIC_UNDO, c, c.pit(), c.pit(), cur); + } else if (inset == cur.nextInset()) + recordUndo(cur); + else + LYXERR0("Inset not found, no undo stack added."); +} + + +void Undo::recordUndoBufferParams(CursorData const & cur) +{ + d->recordUndoBufferParams(cur); +} + + +void Undo::recordUndoFullBuffer(CursorData const & cur) +{ + // This one may happen outside of the main undo group, so we + // put it in its own subgroup to avoid complaints. + beginUndoGroup(); + d->recordUndo(ATOMIC_UNDO, doc_iterator_begin(&d->buffer_), + 0, d->buffer_.paragraphs().size() - 1, cur); + d->recordUndoBufferParams(cur); + endUndoGroup(); +} + +/// UndoGroupHelper class stuff + +void UndoGroupHelper::resetBuffer(Buffer * buf) +{ + if (buf == buffer_) + return; + if (buffer_) + buffer_->undo().endUndoGroup(); + buffer_ = buf; + if (buffer_) + buffer_->undo().beginUndoGroup(); }