kde-workspace/kate/part/buffer/katetextblock.cpp
Ivailo Monev f68295ea28 generic: move sub-projects from kde-baseapps [ci reset]
Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2022-05-14 21:56:54 +03:00

688 lines
19 KiB
C++

/* This file is part of the Kate project.
*
* Copyright (C) 2010 Christoph Cullmann <cullmann@kde.org>
*
* 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 "katetextblock.h"
#include "katetextbuffer.h"
namespace Kate {
TextBlock::TextBlock (TextBuffer *buffer, int startLine)
: m_buffer (buffer)
, m_startLine (startLine)
{
// reserve the block size
m_lines.reserve (m_buffer->m_blockSize);
}
TextBlock::~TextBlock ()
{
// blocks should be empty before they are deleted!
Q_ASSERT (m_lines.empty());
Q_ASSERT (m_cursors.empty());
// it only is a hint for ranges for this block, not the storage of them
}
void TextBlock::setStartLine (int startLine)
{
// allow only valid lines
Q_ASSERT (startLine >= 0);
Q_ASSERT (startLine < m_buffer->lines ());
m_startLine = startLine;
}
TextLine TextBlock::line (int line) const
{
// right input
Q_ASSERT (line >= startLine ());
// calc internal line
line = line - startLine ();
// in range
Q_ASSERT (line < m_lines.size ());
// get text line
return m_lines.at(line);
}
void TextBlock::appendLine (const QString &textOfLine)
{
m_lines.append (TextLine (new TextLineData(textOfLine)));
}
void TextBlock::clearLines ()
{
m_lines.clear ();
}
void TextBlock::text (QString &text) const
{
// combine all lines
for (int i = 0; i < m_lines.size(); ++i) {
// not first line, insert \n
if (i > 0 || startLine() > 0)
text.append ('\n');
text.append (m_lines.at(i)->text ());
}
}
void TextBlock::wrapLine (const KTextEditor::Cursor &position, int fixStartLinesStartIndex)
{
// calc internal line
int line = position.line () - startLine ();
// get text
QString &text = m_lines.at(line)->textReadWrite ();
// check if valid column
Q_ASSERT (position.column() >= 0);
Q_ASSERT (position.column() <= text.size());
// create new line and insert it
m_lines.insert (m_lines.begin() + line + 1, TextLine (new TextLineData()));
// cases for modification:
// 1. line is wrapped in the middle
// 2. if empty line is wrapped, mark new line as modified
// 3. line-to-be-wrapped is already modified
if (position.column() > 0 || text.size() == 0 || m_lines.at(line)->markedAsModified()) {
m_lines.at(line + 1)->markAsModified(true);
} else if (m_lines.at(line)->markedAsSavedOnDisk()) {
m_lines.at(line + 1)->markAsSavedOnDisk(true);
}
// perhaps remove some text from previous line and append it
if (position.column() < text.size ()) {
// text from old line moved first to new one
m_lines.at(line+1)->textReadWrite() = text.right (text.size() - position.column());
// now remove wrapped text from old line
text.chop (text.size() - position.column());
// mark line as modified
m_lines.at(line)->markAsModified(true);
}
/**
* fix all start lines
* we need to do this NOW, else the range update will FAIL!
* bug 313759
*/
m_buffer->fixStartLines (fixStartLinesStartIndex);
/**
* notify the text history
*/
m_buffer->history().wrapLine (position);
/**
* cursor and range handling below
*/
// no cursors will leave or join this block
// no cursors in this block, no work to do..
if (m_cursors.empty())
return;
// move all cursors on the line which has the text inserted
// remember all ranges modified
QSet<TextRange *> changedRanges;
foreach (TextCursor *cursor, m_cursors) {
// skip cursors on lines in front of the wrapped one!
if (cursor->lineInBlock() < line)
continue;
// either this is simple, line behind the wrapped one
if (cursor->lineInBlock() > line) {
// patch line of cursor
cursor->m_line++;
}
// this is the wrapped line
else {
// skip cursors with too small column
if (cursor->column() <= position.column()) {
if (cursor->column() < position.column() || !cursor->m_moveOnInsert)
continue;
}
// move cursor
// patch line of cursor
cursor->m_line++;
// patch column
cursor->m_column -= position.column();
}
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
// check validity of all ranges, might invalidate them...
foreach (TextRange *range, changedRanges)
range->checkValidity ();
}
void TextBlock::unwrapLine (int line, TextBlock *previousBlock, int fixStartLinesStartIndex)
{
// calc internal line
line = line - startLine ();
// two possiblities: either first line of this block or later line
if (line == 0) {
// we need previous block with at least one line
Q_ASSERT (previousBlock);
Q_ASSERT (previousBlock->lines () > 0);
// move last line of previous block to this one, might result in empty block
TextLine oldFirst = m_lines.at(0);
int lastLineOfPreviousBlock = previousBlock->lines ()-1;
TextLine newFirst = previousBlock->m_lines.last();
m_lines[0] = newFirst;
previousBlock->m_lines.erase (previousBlock->m_lines.begin() + (previousBlock->lines () - 1));
const int oldSizeOfPreviousLine = newFirst->text().size();
if (oldFirst->length() > 0) {
// append text
newFirst->textReadWrite().append (oldFirst->text());
// mark line as modified, since text was appended
newFirst->markAsModified(true);
}
// patch startLine of this block
--m_startLine;
/**
* fix all start lines
* we need to do this NOW, else the range update will FAIL!
* bug 313759
*/
m_buffer->fixStartLines (fixStartLinesStartIndex);
/**
* notify the text history in advance
*/
m_buffer->history().unwrapLine (startLine () + line, oldSizeOfPreviousLine);
/**
* cursor and range handling below
*/
// no cursors in this block and the previous one, no work to do..
if (m_cursors.empty() && previousBlock->m_cursors.empty())
return;
// move all cursors because of the unwrapped line
// remember all ranges modified
QSet<TextRange *> changedRanges;
foreach (TextCursor *cursor, m_cursors) {
// this is the unwrapped line
if (cursor->lineInBlock() == 0) {
// patch column
cursor->m_column += oldSizeOfPreviousLine;
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
}
// move cursors of the moved line from previous block to this block now
QSet<TextCursor *> newPreviousCursors;
foreach (TextCursor *cursor, previousBlock->m_cursors) {
if (cursor->lineInBlock() == lastLineOfPreviousBlock) {
cursor->m_line = 0;
cursor->m_block = this;
m_cursors.insert (cursor);
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
else
newPreviousCursors.insert (cursor);
}
previousBlock->m_cursors = newPreviousCursors;
// fixup the ranges that might be effected, because they moved from last line to this block
foreach (TextRange *range, changedRanges) {
// update both blocks
updateRange (range);
previousBlock->updateRange (range);
}
// check validity of all ranges, might invalidate them...
foreach (TextRange *range, changedRanges)
range->checkValidity ();
// be done
return;
}
// easy: just move text to previous line and remove current one
const int oldSizeOfPreviousLine = m_lines.at(line-1)->length();
const int sizeOfCurrentLine = m_lines.at(line)->length();
if (sizeOfCurrentLine > 0)
m_lines.at(line-1)->textReadWrite().append (m_lines.at(line)->text());
const bool lineChanged = (oldSizeOfPreviousLine > 0 && m_lines.at(line - 1)->markedAsModified())
|| (sizeOfCurrentLine > 0 && (oldSizeOfPreviousLine > 0 || m_lines.at(line)->markedAsModified()));
m_lines.at(line-1)->markAsModified(lineChanged);
if (oldSizeOfPreviousLine == 0 && m_lines.at(line)->markedAsSavedOnDisk())
m_lines.at(line-1)->markAsSavedOnDisk(true);
m_lines.erase (m_lines.begin () + line);
/**
* fix all start lines
* we need to do this NOW, else the range update will FAIL!
* bug 313759
*/
m_buffer->fixStartLines (fixStartLinesStartIndex);
/**
* notify the text history in advance
*/
m_buffer->history().unwrapLine (startLine () + line, oldSizeOfPreviousLine);
/**
* cursor and range handling below
*/
// no cursors in this block, no work to do..
if (m_cursors.empty())
return;
// move all cursors because of the unwrapped line
// remember all ranges modified
QSet<TextRange *> changedRanges;
foreach (TextCursor *cursor, m_cursors) {
// skip cursors in lines in front of removed one
if (cursor->lineInBlock() < line)
continue;
// this is the unwrapped line
if (cursor->lineInBlock() == line) {
// patch column
cursor->m_column += oldSizeOfPreviousLine;
}
// patch line of cursor
cursor->m_line--;
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
// check validity of all ranges, might invalidate them...
foreach (TextRange *range, changedRanges)
range->checkValidity ();
}
void TextBlock::insertText (const KTextEditor::Cursor &position, const QString &text)
{
// calc internal line
int line = position.line () - startLine ();
// get text
QString &textOfLine = m_lines.at(line)->textReadWrite ();
int oldLength = textOfLine.size ();
m_lines.at(line)->markAsModified(true);
// check if valid column
Q_ASSERT (position.column() >= 0);
Q_ASSERT (position.column() <= textOfLine.size());
// insert text
textOfLine.insert (position.column(), text);
/**
* notify the text history
*/
m_buffer->history().insertText (position, text.size(), oldLength);
/**
* cursor and range handling below
*/
// no cursors in this block, no work to do..
if (m_cursors.empty())
return;
// move all cursors on the line which has the text inserted
// remember all ranges modified
QSet<TextRange *> changedRanges;
foreach (TextCursor *cursor, m_cursors) {
// skip cursors not on this line!
if (cursor->lineInBlock() != line)
continue;
// skip cursors with too small column
if (cursor->column() <= position.column()) {
if (cursor->column() < position.column() || !cursor->m_moveOnInsert)
continue;
}
// patch column of cursor
if (cursor->m_column <= oldLength)
cursor->m_column += text.size ();
// special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode
else if (cursor->m_column < textOfLine.size())
cursor->m_column = textOfLine.size();
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
// check validity of all ranges, might invalidate them...
foreach (TextRange *range, changedRanges)
range->checkValidity ();
}
void TextBlock::removeText (const KTextEditor::Range &range, QString &removedText)
{
// calc internal line
int line = range.start().line () - startLine ();
// get text
QString &textOfLine = m_lines.at(line)->textReadWrite ();
int oldLength = textOfLine.size ();
// check if valid column
Q_ASSERT (range.start().column() >= 0);
Q_ASSERT (range.start().column() <= textOfLine.size());
Q_ASSERT (range.end().column() >= 0);
Q_ASSERT (range.end().column() <= textOfLine.size());
// get text which will be removed
removedText = textOfLine.mid (range.start().column(), range.end().column() - range.start().column());
// remove text
textOfLine.remove (range.start().column(), range.end().column() - range.start().column());
m_lines.at(line)->markAsModified(true);
/**
* notify the text history
*/
m_buffer->history().removeText (range, oldLength);
/**
* cursor and range handling below
*/
// no cursors in this block, no work to do..
if (m_cursors.empty())
return;
// move all cursors on the line which has the text removed
// remember all ranges modified
QSet<TextRange *> changedRanges;
foreach (TextCursor *cursor, m_cursors) {
// skip cursors not on this line!
if (cursor->lineInBlock() != line)
continue;
// skip cursors with too small column
if (cursor->column() <= range.start().column())
continue;
// patch column of cursor
if (cursor->column() <= range.end().column())
cursor->m_column = range.start().column ();
else
cursor->m_column -= (range.end().column() - range.start().column());
// remember range, if any
if (cursor->kateRange())
changedRanges.insert (cursor->kateRange());
}
// check validity of all ranges, might invalidate them...
foreach (TextRange *range, changedRanges)
range->checkValidity ();
}
void TextBlock::debugPrint (int blockIndex) const
{
// print all blocks
for (int i = 0; i < m_lines.size(); ++i)
printf ("%4d - %4d : %4d : '%s'\n", blockIndex, startLine() + i
, m_lines.at(i)->text().size(), qPrintable (m_lines.at(i)->text()));
}
TextBlock *TextBlock::splitBlock (int fromLine)
{
// half the block
int linesOfNewBlock = lines () - fromLine;
// create and insert new block
TextBlock *newBlock = new TextBlock (m_buffer, startLine() + fromLine);
// move lines
newBlock->m_lines.reserve (linesOfNewBlock);
for (int i = fromLine; i < m_lines.size(); ++i)
newBlock->m_lines.append (m_lines.at(i));
m_lines.resize (fromLine);
// move cursors
QSet<TextCursor*> oldBlockSet;
foreach (TextCursor *cursor, m_cursors) {
if (cursor->lineInBlock() >= fromLine) {
cursor->m_line = cursor->lineInBlock() - fromLine;
cursor->m_block = newBlock;
newBlock->m_cursors.insert (cursor);
}
else
oldBlockSet.insert (cursor);
}
m_cursors = oldBlockSet;
// fix ALL ranges!
QList<TextRange*> allRanges = m_uncachedRanges.toList() + m_cachedLineForRanges.keys();
foreach (TextRange *range, allRanges) {
// update both blocks
updateRange (range);
newBlock->updateRange (range);
}
// return the new generated block
return newBlock;
}
void TextBlock::mergeBlock (TextBlock *targetBlock)
{
// move cursors, do this first, now still lines() count is correct for target
foreach (TextCursor *cursor, m_cursors) {
cursor->m_line = cursor->lineInBlock() + targetBlock->lines ();
cursor->m_block = targetBlock;
targetBlock->m_cursors.insert (cursor);
}
m_cursors.clear ();
// move lines
targetBlock->m_lines.reserve (targetBlock->lines() + lines ());
for (int i = 0; i < m_lines.size(); ++i)
targetBlock->m_lines.append (m_lines.at(i));
m_lines.clear ();
// fix ALL ranges!
QList<TextRange*> allRanges = m_uncachedRanges.toList() + m_cachedLineForRanges.keys();
foreach(TextRange* range, allRanges) {
// update both blocks
updateRange (range);
targetBlock->updateRange (range);
}
}
void TextBlock::deleteBlockContent ()
{
// kill cursors, if not belonging to a range
QSet<TextCursor *> copy = m_cursors;
foreach (TextCursor *cursor, copy)
if (!cursor->kateRange())
delete cursor;
// kill lines
m_lines.clear ();
}
void TextBlock::clearBlockContent (TextBlock *targetBlock)
{
// move cursors, if not belonging to a range
QSet<TextCursor *> copy = m_cursors;
foreach (TextCursor *cursor, copy) {
if (!cursor->kateRange()) {
cursor->m_column = 0;
cursor->m_line = 0;
cursor->m_block = targetBlock;
targetBlock->m_cursors.insert (cursor);
m_cursors.remove (cursor);
}
}
// kill lines
m_lines.clear ();
}
void TextBlock::markModifiedLinesAsSaved ()
{
// mark all modified lines as saved
for (int i = 0; i < m_lines.size(); ++i) {
TextLine textLine = m_lines[i];
if (textLine->markedAsModified())
textLine->markAsSavedOnDisk(true);
}
}
void TextBlock::updateRange (TextRange* range)
{
/**
* get some simple facts about our nice range
*/
const int startLine = range->startInternal().lineInternal();
const int endLine = range->endInternal().lineInternal();
const bool isSingleLine = startLine == endLine;
/**
* perhaps remove range and be done
*/
if ((endLine < m_startLine) || (startLine >= (m_startLine + lines()))) {
removeRange (range);
return;
}
/**
* The range is still a single-line range, and is still cached to the correct line.
*/
if(isSingleLine && m_cachedLineForRanges.contains (range) && (m_cachedLineForRanges.value(range) == startLine - m_startLine))
return;
/**
* The range is still a multi-line range, and is already in the correct set.
*/
if(!isSingleLine && m_uncachedRanges.contains (range))
return;
/**
* remove, if already there!
*/
removeRange(range);
/**
* simple case: multi-line range
*/
if (!isSingleLine) {
/**
* The range cannot be cached per line, as it spans multiple lines
*/
m_uncachedRanges.insert(range);
return;
}
/**
* The range is contained by a single line, put it into the line-cache
*/
const int lineOffset = startLine - m_startLine;
/**
* enlarge cache if needed
*/
if (m_cachedRangesForLine.size() <= lineOffset)
m_cachedRangesForLine.resize(lineOffset+1);
/**
* insert into mapping
*/
m_cachedRangesForLine[lineOffset].insert(range);
m_cachedLineForRanges[range] = lineOffset;
}
void TextBlock::removeRange (TextRange* range)
{
/**
* uncached range? remove it and be done
*/
if(m_uncachedRanges.remove (range)) {
/**
* must be only uncached!
*/
Q_ASSERT (!m_cachedLineForRanges.contains(range));
return;
}
/**
* cached range?
*/
QHash<TextRange*, int>::iterator it = m_cachedLineForRanges.find(range);
if (it != m_cachedLineForRanges.end()) {
/**
* must be only cached!
*/
Q_ASSERT (!m_uncachedRanges.contains(range));
/**
* query the range from cache, must be there
*/
Q_ASSERT (m_cachedRangesForLine.at(*it).contains(range));
/**
* remove it and be done
*/
m_cachedRangesForLine[*it].remove(range);
m_cachedLineForRanges.erase(it);
return;
}
/**
* else: range was not for this block, just do nothing, removeRange should be "safe" to use
*/
}
}