/* 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 "config.h" #include "katetextbuffer.h" #include "katetextloader.h" // this is unfortunate, but needed for performance #include "katedocument.h" #include "kateview.h" // fdatasync #include #include #include #include #if 0 #define EDIT_DEBUG kDebug() #else #define EDIT_DEBUG if (0) kDebug() #endif namespace Kate { TextBuffer::TextBuffer (KateDocument *parent, int blockSize) : QObject (parent) , m_document (parent) , m_history (*this) , m_blockSize (blockSize) , m_lines (0) , m_lastUsedBlock (0) , m_revision (0) , m_editingTransactions (0) , m_editingLastRevision (0) , m_editingLastLines (0) , m_editingMinimalLineChanged (-1) , m_editingMaximalLineChanged (-1) , m_fallbackTextCodec (0) , m_textCodec (0) , m_generateByteOrderMark (false) , m_endOfLineMode (eolUnix) , m_newLineAtEof (false) , m_lineLengthLimit (4096) { // minimal block size must be > 0 Q_ASSERT (m_blockSize > 0); // create initial state clear (); } TextBuffer::~TextBuffer () { // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect m_document = 0; // not allowed during editing Q_ASSERT (m_editingTransactions == 0); // kill all ranges, work on copy, they will remove themself from the hash QSet copyRanges = m_ranges; qDeleteAll (copyRanges); Q_ASSERT (m_ranges.empty()); // clean out all cursors and lines, only cursors belonging to range will survive foreach(TextBlock* block, m_blocks) block->deleteBlockContent (); // delete all blocks, now that all cursors are really deleted // else asserts in destructor of blocks will fail! qDeleteAll (m_blocks); m_blocks.clear (); // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks QSet copyCursors = m_invalidCursors; qDeleteAll (copyCursors); Q_ASSERT (m_invalidCursors.empty()); } void TextBuffer::invalidateRanges() { // invalidate all ranges, work on copy, they might delete themself... QSet copyRanges = m_ranges; foreach (TextRange *range, copyRanges) range->setRange (KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); } void TextBuffer::clear () { // not allowed during editing Q_ASSERT (m_editingTransactions == 0); invalidateRanges(); // new block for empty buffer TextBlock *newBlock = new TextBlock (this, 0); newBlock->appendLine (QString()); // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range foreach(TextBlock* block, m_blocks) block->clearBlockContent (newBlock); // kill all buffer blocks qDeleteAll (m_blocks); m_blocks.clear (); // insert one block with one empty line m_blocks.append (newBlock); // reset lines and last used block m_lines = 1; m_lastUsedBlock = 0; // reset revision m_revision = 0; // reset bom detection m_generateByteOrderMark = false; // clear edit history m_history.clear (); // we got cleared emit cleared (); } TextLine TextBuffer::line (int line) const { // get block, this will assert on invalid line int blockIndex = blockForLine (line); // get line return m_blocks.at(blockIndex)->line (line); } QString TextBuffer::text () const { QString text; // combine all blocks foreach(TextBlock* block, m_blocks) block->text (text); // return generated string return text; } bool TextBuffer::startEditing () { // increment transaction counter ++m_editingTransactions; // if not first running transaction, do nothing if (m_editingTransactions > 1) return false; // reset information about edit... m_editingLastRevision = m_revision; m_editingLastLines = m_lines; m_editingMinimalLineChanged = -1; m_editingMaximalLineChanged = -1; // transaction has started emit editingStarted (); // first transaction started return true; } bool TextBuffer::finishEditing () { // only allowed if still transactions running Q_ASSERT (m_editingTransactions > 0); // decrement counter --m_editingTransactions; // if not last running transaction, do nothing if (m_editingTransactions > 0) return false; // assert that if buffer changed, the line ranges are set and valid! Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); Q_ASSERT (!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); // transaction has finished emit editingFinished (); // last transaction finished return true; } void TextBuffer::wrapLine (const KTextEditor::Cursor &position) { // debug output for REAL low-level debugging EDIT_DEBUG << "wrapLine" << position; // only allowed if editing transaction running Q_ASSERT (m_editingTransactions > 0); // get block, this will assert on invalid line int blockIndex = blockForLine (position.line()); /** * let the block handle the wrapLine * this can only lead to one more line in this block * no other blocks will change * this call will trigger fixStartLines */ ++m_lines; // first alter the line counter, as functions called will need the valid one m_blocks.at(blockIndex)->wrapLine (position, blockIndex); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) m_editingMinimalLineChanged = position.line(); if (position.line() <= m_editingMaximalLineChanged) ++m_editingMaximalLineChanged; else m_editingMaximalLineChanged = position.line() + 1; // balance the changed block if needed balanceBlock (blockIndex); // emit signal about done change emit lineWrapped (position); } void TextBuffer::unwrapLine (int line) { // debug output for REAL low-level debugging EDIT_DEBUG << "unwrapLine" << line; // only allowed if editing transaction running Q_ASSERT (m_editingTransactions > 0); // line 0 can't be unwrapped Q_ASSERT (line > 0); // get block, this will assert on invalid line int blockIndex = blockForLine (line); // is this the first line in the block? bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); /** * let the block handle the unwrapLine * this can either lead to one line less in this block or the previous one * the previous one could even end up with zero lines * this call will trigger fixStartLines */ m_blocks.at(blockIndex)->unwrapLine (line, (blockIndex > 0) ? m_blocks.at(blockIndex-1) : 0, firstLineInBlock ? (blockIndex - 1) : blockIndex); --m_lines; // decrement index for later fixup, if we modified the block in front of the found one if (firstLineInBlock) --blockIndex; // remember changes ++m_revision; // update changed line interval if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) m_editingMinimalLineChanged = line - 1; if (line <= m_editingMaximalLineChanged) --m_editingMaximalLineChanged; else m_editingMaximalLineChanged = line -1; // balance the changed block if needed balanceBlock (blockIndex); // emit signal about done change emit lineUnwrapped (line); } void TextBuffer::insertText (const KTextEditor::Cursor &position, const QString &text) { // debug output for REAL low-level debugging EDIT_DEBUG << "insertText" << position << text; // only allowed if editing transaction running Q_ASSERT (m_editingTransactions > 0); // skip work, if no text to insert if (text.isEmpty()) return; // get block, this will assert on invalid line int blockIndex = blockForLine (position.line()); // let the block handle the insertText m_blocks.at(blockIndex)->insertText (position, text); // remember changes ++m_revision; // update changed line interval if (position.line () < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) m_editingMinimalLineChanged = position.line (); if (position.line () > m_editingMaximalLineChanged) m_editingMaximalLineChanged = position.line (); // emit signal about done change emit textInserted (position, text); } void TextBuffer::removeText (const KTextEditor::Range &range) { // debug output for REAL low-level debugging EDIT_DEBUG << "removeText" << range; // only allowed if editing transaction running Q_ASSERT (m_editingTransactions > 0); // only ranges on one line are supported Q_ASSERT (range.start().line() == range.end().line()); // start column <= end column and >= 0 Q_ASSERT (range.start().column() <= range.end().column()); Q_ASSERT (range.start().column() >= 0); // skip work, if no text to remove if (range.isEmpty()) return; // get block, this will assert on invalid line int blockIndex = blockForLine (range.start().line()); // let the block handle the removeText, retrieve removed text QString text; m_blocks.at(blockIndex)->removeText (range, text); // remember changes ++m_revision; // update changed line interval if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) m_editingMinimalLineChanged = range.start().line(); if (range.start().line() > m_editingMaximalLineChanged) m_editingMaximalLineChanged = range.start().line(); // emit signal about done change emit textRemoved (range, text); } int TextBuffer::blockForLine (int line) const { // only allow valid lines if ((line < 0) || (line >= lines())) qFatal ("out of range line requested in text buffer (%d out of [0, %d[)", line, lines()); // we need blocks and last used block should not be negative Q_ASSERT (!m_blocks.isEmpty()); Q_ASSERT (m_lastUsedBlock >= 0); /** * shortcut: try last block first */ if (m_lastUsedBlock < m_blocks.size()) { /** * check if block matches * if yes, just return again this block */ TextBlock* block = m_blocks[m_lastUsedBlock]; const int start = block->startLine(); const int lines = block->lines (); if (start <= line && line < (start + lines)) return m_lastUsedBlock; } /** * search for right block * use binary search * if we leave this loop not by returning the found element we have an error */ int blockStart = 0; int blockEnd = m_blocks.size() - 1; while (blockEnd >= blockStart) { // get middle and ensure it is OK int middle = blockStart + ((blockEnd - blockStart) / 2); Q_ASSERT (middle >= 0); Q_ASSERT (middle < m_blocks.size()); // facts bout this block TextBlock* block = m_blocks[middle]; const int start = block->startLine(); const int lines = block->lines (); // right block found, remember it and return it if (start <= line && line < (start + lines)) { m_lastUsedBlock = middle; return middle; } // half our stuff ;) if (line < start) blockEnd = middle - 1; else blockStart = middle + 1; } // we should always find a block qFatal ("line requested in text buffer (%d out of [0, %d[), no block found", line, lines()); return -1; } void TextBuffer::fixStartLines (int startBlock) { // only allow valid start block Q_ASSERT (startBlock >= 0); Q_ASSERT (startBlock < m_blocks.size()); // new start line for next block TextBlock* block = m_blocks.at(startBlock); int newStartLine = block->startLine () + block->lines (); // fixup block for (int index = startBlock + 1; index < m_blocks.size(); ++index) { // set new start line block = m_blocks.at(index); block->setStartLine (newStartLine); // calculate next start line newStartLine += block->lines (); } } void TextBuffer::balanceBlock (int index) { /** * two cases, too big or too small block */ TextBlock *blockToBalance = m_blocks.at(index); // first case, too big one, split it if (blockToBalance->lines () >= 2 * m_blockSize) { // half the block int halfSize = blockToBalance->lines () / 2; // create and insert new block behind current one, already set right start line TextBlock *newBlock = blockToBalance->splitBlock (halfSize); Q_ASSERT (newBlock); m_blocks.insert (m_blocks.begin() + index + 1, newBlock); // split is done return; } // second case: possibly too small block // if only one block, no chance to unite // same if this is first block, we always append to previous one if (index == 0) return; // block still large enough, do nothing if (2 * blockToBalance->lines () > m_blockSize) return; // unite small block with predecessor TextBlock *targetBlock = m_blocks.at(index-1); // merge block blockToBalance->mergeBlock (targetBlock); // delete old block delete blockToBalance; m_blocks.erase (m_blocks.begin() + index); } void TextBuffer::debugPrint (const QString &title) const { // print header with title printf ("%s (lines: %d bs: %d)\n", qPrintable (title), m_lines, m_blockSize); // print all blocks for (int i = 0; i < m_blocks.size(); ++i) m_blocks.at(i)->debugPrint (i); } bool TextBuffer::load (const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, bool enforceTextCodec) { // fallback codec must exist Q_ASSERT (m_fallbackTextCodec); // codec must be set! Q_ASSERT (m_textCodec); /** * first: clear buffer in any case! */ clear (); /** * construct the file loader for the given file, with correct prober type */ Kate::TextLoader file (filename); /** * triple play, maximal three loading rounds * 0) use the given encoding, be done, if no encoding errors happen * 1) use BOM to decided if unicode or if that fails, use encoding prober, if no encoding errors happen, be done * 2) use fallback encoding, be done, if no encoding errors happen * 3) use again given encoding, be done in any case */ for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { /** * kill all blocks beside first one */ for (int b = 1; b < m_blocks.size(); ++b) { TextBlock* block = m_blocks.at(b); block->clearLines (); delete block; } m_blocks.resize (1); /** * remove lines in first block */ m_blocks.last()->clearLines (); m_lines = 0; /** * try to open file, with given encoding * in round 0 + 3 use the given encoding from user * in round 1 use 0, to trigger detection * in round 2 use fallback */ QTextCodec *codec = m_textCodec; if (i == 1) codec = 0; else if (i == 2) codec = m_fallbackTextCodec; if (!file.open (codec)) { // create one dummy textline, in any case m_blocks.last()->appendLine (QString()); m_lines++; return false; } // read in all lines... encodingErrors = false; while ( !file.eof() ) { // read line int offset = 0, length = 0; bool currentError = !file.readLine (offset, length); encodingErrors = encodingErrors || currentError; // bail out on encoding error, if not last round! if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { kDebug (13020) << "Failed try to load file" << filename << "with codec" << (file.textCodec() ? file.textCodec()->name() : "(null)"); break; } // get unicode data for this line const QChar *unicodeData = file.unicode () + offset; /** * split lines, if too large */ do { /** * calculate line length */ int lineLength = length; if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { /** * search for place to wrap */ int spacePosition = m_lineLengthLimit-1; for (int testPosition = m_lineLengthLimit-1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit/10))); --testPosition) { /** * wrap place found? */ if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { spacePosition = testPosition; break; } } /** * wrap the line */ lineLength = spacePosition+1; length -= lineLength; tooLongLinesWrapped = true; } else { /** * be done after this round */ length = 0; } /** * construct new text line with content from file * move data pointer */ QString textLine (unicodeData, lineLength); unicodeData += lineLength; /** * ensure blocks aren't too large */ if (m_blocks.last()->lines() >= m_blockSize) m_blocks.append (new TextBlock (this, m_blocks.last()->startLine() + m_blocks.last()->lines())); /** * append line to last block */ m_blocks.last()->appendLine (textLine); ++m_lines; } while (length > 0); } // if no encoding error, break out of reading loop if (!encodingErrors) { // remember used codec, might change bom setting setTextCodec (file.textCodec ()); break; } } // save sha1sum of file on disk setDigest (file.digest ()); // remember if BOM was found if (file.byteOrderMarkFound ()) setGenerateByteOrderMark (true); // remember eol mode, if any found in file if (file.eol() != eolUnknown) setEndOfLineMode (file.eol()); // assert that one line is there! Q_ASSERT (m_lines > 0); // report CODEC + ERRORS kDebug (13020) << "Loaded file " << filename << "with codec" << file.textCodec()->name() << (encodingErrors ? "with" : "without") << "encoding errors"; // report BOM kDebug (13020) << (file.byteOrderMarkFound () ? "Found" : "Didn't find") << "byte order mark"; // emit success emit loaded (filename, encodingErrors); // file loading worked, modulo encoding problems return true; } const QByteArray &TextBuffer::digest () const { return m_digest; } void TextBuffer::setDigest (const QByteArray & sha1sum) { m_digest = sha1sum; } void TextBuffer::setTextCodec (QTextCodec *codec) { m_textCodec = codec; // enforce bom for some encodings int mib = m_textCodec->mibEnum (); if (mib == 1013 || mib == 1014 || mib == 1015) // utf16 setGenerateByteOrderMark (true); if (mib == 1017 || mib == 1018 || mib == 1019) // utf32 setGenerateByteOrderMark (true); } bool TextBuffer::save (const QString &filename) { // codec must be set! Q_ASSERT (m_textCodec); /** * use KSaveFile for save write + rename */ KSaveFile saveFile (filename); /** * allow fallback if directory not writable * fixes bug 312415 */ saveFile.setDirectWriteFallback (true); /** * try to open or fail */ if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } /** * construct stream */ QTextStream stream (&saveFile); // set the correct codec stream.setCodec (m_textCodec); // generate byte order mark? stream.setGenerateByteOrderMark (generateByteOrderMark()); // our loved eol string ;) QString eol = "\n"; //m_doc->config()->eolString (); if (endOfLineMode() == eolDos) eol = QString ("\r\n"); else if (endOfLineMode() == eolMac) eol = QString ("\r"); // just dump the lines out ;) for (int i = 0; i < m_lines; ++i) { // get line to save Kate::TextLine textline = line (i); stream << textline->text(); // append correct end of line string if ((i+1) < m_lines) stream << eol; } if (m_newLineAtEof) { Q_ASSERT(m_lines > 0); // see .h file const Kate::TextLine lastLine = line (m_lines - 1); const int firstChar = lastLine->firstChar(); if (firstChar > -1 || lastLine->length() > 0) { stream << eol; } } // flush stream stream.flush (); // flush file if (!saveFile.flush()) return false; // ensure that the file is written to disk #ifdef HAVE_FDATASYNC fdatasync (saveFile.handle()); #else fsync (saveFile.handle()); #endif // did save work? // only finalize if stream status == OK bool ok = (stream.status() == QTextStream::Ok) && saveFile.finalize(); // remember this revision as last saved if we had success! if (ok) m_history.setLastSavedRevision (); // report CODEC + ERRORS kDebug (13020) << "Saved file " << filename << "with codec" << m_textCodec->name() << (ok ? "without" : "with") << "errors"; if (ok) markModifiedLinesAsSaved(); // emit signal on success if (ok) emit saved (filename); // return success or not return ok; } void TextBuffer::notifyAboutRangeChange (KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute) { /** * ignore calls if no document is around */ if (!m_document) return; /** * update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really * just create 20k ranges in a go and you wait seconds on a decent machine */ const QList &views = m_document->views (); foreach(KTextEditor::View* curView, views) { // filter wrong views if (view && view != curView) continue; // notify view, it is really a kate view static_cast (curView)->notifyAboutRangeChange (startLine, endLine, rangeWithAttribute); } } void TextBuffer::markModifiedLinesAsSaved() { foreach(TextBlock* block, m_blocks) block->markModifiedLinesAsSaved (); } QList TextBuffer::rangesForLine (int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const { // get block, this will assert on invalid line const int blockIndex = blockForLine (line); // get the ranges of the right block QList rightRanges; foreach (const QSet &ranges, m_blocks.at(blockIndex)->rangesForLine (line)) { foreach (TextRange * const range, ranges) { /** * we want only ranges with attributes, but this one has none */ if (rangesWithAttributeOnly && !range->hasAttribute()) continue; /** * we want ranges for no view, but this one's attribute is only valid for views */ if (!view && range->attributeOnlyForViews()) continue; /** * the range's attribute is not valid for this view */ if (range->view() && range->view() != view) continue; /** * if line is in the range, ok */ if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) rightRanges.append (range); } } // return right ranges return rightRanges; } }