/* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katetexthistory.h" #include "katetextbuffer.h" namespace Kate { TextHistory::TextHistory (TextBuffer &buffer) : m_buffer (buffer) , m_lastSavedRevision (-1) , m_firstHistoryEntryRevision (0) { // just call clear to init clear (); } TextHistory::~TextHistory () { } qint64 TextHistory::revision () const { // just output last revisions of buffer return m_buffer.revision (); } void TextHistory::clear () { // reset last saved revision m_lastSavedRevision = -1; // remove all history entries and add no-change dummy for first revision m_historyEntries.clear (); m_historyEntries.push_back (Entry ()); // first entry will again belong to first revision m_firstHistoryEntryRevision = 0; } void TextHistory::setLastSavedRevision () { // current revision was successful saved m_lastSavedRevision = revision (); } void TextHistory::wrapLine (const KTextEditor::Cursor &position) { // create and add new entry Entry entry; entry.type = Entry::WrapLine; entry.line = position.line (); entry.column = position.column (); addEntry (entry); } void TextHistory::unwrapLine (int line, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::UnwrapLine; entry.line = line; entry.column = 0; entry.oldLineLength = oldLineLength; addEntry (entry); } void TextHistory::insertText (const KTextEditor::Cursor &position, int length, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::InsertText; entry.line = position.line (); entry.column = position.column (); entry.length = length; entry.oldLineLength = oldLineLength; addEntry (entry); } void TextHistory::removeText (const KTextEditor::Range &range, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::RemoveText; entry.line = range.start().line (); entry.column = range.start().column (); entry.length = range.end().column() - range.start().column(); entry.oldLineLength = oldLineLength; addEntry (entry); } void TextHistory::addEntry (const Entry &entry) { /** * history should never be empty */ Q_ASSERT (!m_historyEntries.empty ()); /** * simple efficient check: if we only have one entry, and the entry is not referenced * just replace it with the new one and adjust the revision */ if ((m_historyEntries.size () == 1) && !m_historyEntries.first().referenceCounter) { /** * remember new revision for first element, it is the revision we get after this change */ m_firstHistoryEntryRevision = revision () + 1; /** * remember edit */ m_historyEntries.first() = entry; /** * be done... */ return; } /** * ok, we have more than one entry or the entry is referenced, just add up new entries */ m_historyEntries.push_back (entry); } void TextHistory::lockRevision (qint64 revision) { /** * some invariants must hold */ Q_ASSERT (!m_historyEntries.empty ()); Q_ASSERT (revision >= m_firstHistoryEntryRevision); Q_ASSERT (revision < (m_firstHistoryEntryRevision + m_historyEntries.size())); /** * increment revision reference counter */ Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; ++entry.referenceCounter; } void TextHistory::unlockRevision (qint64 revision) { /** * some invariants must hold */ Q_ASSERT (!m_historyEntries.empty ()); Q_ASSERT (revision >= m_firstHistoryEntryRevision); Q_ASSERT (revision < (m_firstHistoryEntryRevision + m_historyEntries.size())); /** * decrement revision reference counter */ Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; Q_ASSERT (entry.referenceCounter); --entry.referenceCounter; /** * clean up no longer used revisions... */ if (!entry.referenceCounter) { /** * search for now unused stuff */ int unreferencedEdits = 0; for (int i = 0; i + 1 < m_historyEntries.size(); ++i) { if (m_historyEntries.at(i).referenceCounter) break; // remember deleted count ++unreferencedEdits; } /** * remove unrefed from the list now */ if (unreferencedEdits > 0) { // remove stuff from history m_historyEntries.erase (m_historyEntries.begin(), m_historyEntries.begin() + unreferencedEdits); // patch first entry revision m_firstHistoryEntryRevision += unreferencedEdits; } } } void TextHistory::Entry::transformCursor (int &cursorLine, int &cursorColumn, bool moveOnInsert) const { /** * simple stuff, sort out generic things */ /** * no change, if this change is in line behind cursor */ if (line > cursorLine) return; /** * handle all history types */ switch (type) { /** * Wrap a line */ case WrapLine: /** * we wrap this line */ if (cursorLine == line) { /** * skip cursors with too small column */ if (cursorColumn <= column) { if (cursorColumn < column || !moveOnInsert) return; } /** * adjust column */ cursorColumn = cursorColumn - column; } /** * always increment cursor line */ cursorLine += 1; return; /** * Unwrap a line */ case UnwrapLine: /** * we unwrap this line, adjust column */ if (cursorLine == line) cursorColumn += oldLineLength; /** * decrease cursor line */ cursorLine -= 1; return; /** * Insert text */ case InsertText: /** * only interesting, if same line */ if (cursorLine != line) return; // skip cursors with too small column if (cursorColumn <= column) if (cursorColumn < column || !moveOnInsert) return; // patch column of cursor if (cursorColumn <= oldLineLength) cursorColumn += length; // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode else if (cursorColumn < oldLineLength + length) cursorColumn = oldLineLength + length; return; /** * Remove text */ case RemoveText: /** * only interesting, if same line */ if (cursorLine != line) return; // skip cursors with too small column if (cursorColumn <= column) return; // patch column of cursor if (cursorColumn <= column + length) cursorColumn = column; else cursorColumn -= length; return; /** * nothing */ default: return; } } void TextHistory::Entry::reverseTransformCursor (int &cursorLine, int &cursorColumn, bool moveOnInsert) const { /** * handle all history types */ switch (type) { /** * Wrap a line */ case WrapLine: /** * ignore this line */ if (cursorLine <= line) return; /** * next line is unwrapped */ if (cursorLine == line + 1) { /** * adjust column */ cursorColumn = cursorColumn + column; } /** * always decrement cursor line */ cursorLine -= 1; return; /** * Unwrap a line */ case UnwrapLine: /** * ignore lines before unwrapped one */ if (cursorLine < line - 1) return; /** * we unwrap this line, try to adjust cursor column if needed */ if (cursorLine == line - 1) { /** * skip cursors with to small columns */ if (cursorColumn <= oldLineLength) { if (cursorColumn < oldLineLength || !moveOnInsert) return; } cursorColumn -= oldLineLength; } /** * increase cursor line */ cursorLine += 1; return; /** * Insert text */ case InsertText: /** * only interesting, if same line */ if (cursorLine != line) return; // skip cursors with too small column if (cursorColumn <= column) return; // patch column of cursor if (cursorColumn - length < column) cursorColumn = column; else cursorColumn -= length; return; /** * Remove text */ case RemoveText: /** * only interesting, if same line */ if (cursorLine != line) return; // skip cursors with too small column if (cursorColumn <= column) if (cursorColumn < column || !moveOnInsert) return; // patch column of cursor if (cursorColumn <= oldLineLength) cursorColumn += length; // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode else if (cursorColumn < oldLineLength + length) cursorColumn = oldLineLength + length; return; /** * nothing */ default: return; } } void TextHistory::transformCursor (int& line, int& column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { /** * -1 special meaning for from/toRevision */ if (fromRevision == -1) fromRevision = revision (); if (toRevision == -1) toRevision = revision (); /** * shortcut, same revision */ if (fromRevision == toRevision) return; /** * some invariants must hold */ Q_ASSERT (!m_historyEntries.empty ()); Q_ASSERT (fromRevision != toRevision); Q_ASSERT (fromRevision >= m_firstHistoryEntryRevision); Q_ASSERT (fromRevision < (m_firstHistoryEntryRevision + m_historyEntries.size())); Q_ASSERT (toRevision >= m_firstHistoryEntryRevision); Q_ASSERT (toRevision < (m_firstHistoryEntryRevision + m_historyEntries.size())); /** * transform cursor */ bool moveOnInsert = insertBehavior == KTextEditor::MovingCursor::MoveOnInsert; /** * forward or reverse transform? */ if (toRevision > fromRevision) { for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { const Entry &entry = m_historyEntries.at(rev); entry.transformCursor (line, column, moveOnInsert); } } else { for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { const Entry &entry = m_historyEntries.at(rev); entry.reverseTransformCursor (line, column, moveOnInsert); } } } void TextHistory::transformRange (KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { /** * invalidate on empty? */ bool invalidateIfEmpty = emptyBehavior == KTextEditor::MovingRange::InvalidateIfEmpty; if (invalidateIfEmpty && range.end() <= range.start()) { range = KTextEditor::Range::invalid(); return; } /** * -1 special meaning for from/toRevision */ if (fromRevision == -1) fromRevision = revision (); if (toRevision == -1) toRevision = revision (); /** * shortcut, same revision */ if (fromRevision == toRevision) return; /** * some invariants must hold */ Q_ASSERT (!m_historyEntries.empty ()); Q_ASSERT (fromRevision != toRevision); Q_ASSERT (fromRevision >= m_firstHistoryEntryRevision); Q_ASSERT (fromRevision < (m_firstHistoryEntryRevision + m_historyEntries.size())); Q_ASSERT (toRevision >= m_firstHistoryEntryRevision); Q_ASSERT (toRevision < (m_firstHistoryEntryRevision + m_historyEntries.size())); /** * transform cursors */ // first: copy cursors, without range association int startLine = range.start().line(), startColumn = range.start().column(), endLine = range.end().line(), endColumn = range.end().column(); bool moveOnInsertStart = !(insertBehaviors & KTextEditor::MovingRange::ExpandLeft); bool moveOnInsertEnd = (insertBehaviors & KTextEditor::MovingRange::ExpandRight); /** * forward or reverse transform? */ if (toRevision > fromRevision) { for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { const Entry &entry = m_historyEntries.at(rev); entry.transformCursor (startLine, startColumn, moveOnInsertStart); entry.transformCursor (endLine, endColumn, moveOnInsertEnd); // got empty? if(endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { if (invalidateIfEmpty) { range = KTextEditor::Range::invalid(); return; } else{ // else normalize them endLine = startLine; endColumn = startColumn; } } } } else { for (int rev = fromRevision - m_firstHistoryEntryRevision ; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { const Entry &entry = m_historyEntries.at(rev); entry.reverseTransformCursor (startLine, startColumn, moveOnInsertStart); entry.reverseTransformCursor (endLine, endColumn, moveOnInsertEnd); // got empty? if(endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { if (invalidateIfEmpty) { range = KTextEditor::Range::invalid(); return; } else{ // else normalize them endLine = startLine; endColumn = startColumn; } } } } // now, copy cursors back range.start().setLine(startLine); range.start().setColumn(startColumn); range.end().setLine(endLine); range.end().setColumn(endColumn); } }