kde-workspace/kate/part/buffer/katetextfolding.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

939 lines
23 KiB
C++

/* This file is part of the Kate project.
*
* Copyright (C) 2013 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 "katetextfolding.h"
#include "katetextbuffer.h"
#include "katetextrange.h"
namespace Kate {
TextFolding::FoldingRange::FoldingRange (TextBuffer &buffer, const KTextEditor::Range &range, FoldingRangeFlags _flags)
: start (new TextCursor (buffer, range.start(), KTextEditor::MovingCursor::MoveOnInsert))
, end (new TextCursor (buffer, range.end(), KTextEditor::MovingCursor::MoveOnInsert))
, parent (0)
, flags (_flags)
, id (-1)
{
}
TextFolding::FoldingRange::~FoldingRange ()
{
/**
* kill all our data!
* this will recurse all sub-structures!
*/
delete start;
delete end;
qDeleteAll (nestedRanges);
}
TextFolding::TextFolding (TextBuffer &buffer)
: QObject ()
, m_buffer (buffer)
, m_idCounter (-1)
{
/**
* connect needed signals from buffer
*/
connect (&m_buffer, SIGNAL(cleared()), SLOT(clear()));
}
TextFolding::~TextFolding ()
{
/**
* only delete the folding ranges, the folded ranges and mapped ranges are the same objects
*/
qDeleteAll (m_foldingRanges);
}
void TextFolding::clear ()
{
/**
* reset counter
*/
m_idCounter = -1;
/**
* no ranges, no work
*/
if (m_foldingRanges.isEmpty()) {
/**
* assert all stuff is consistent and return!
*/
Q_ASSERT (m_idToFoldingRange.isEmpty());
Q_ASSERT (m_foldedFoldingRanges.isEmpty());
return;
}
/**
* cleanup
*/
m_idToFoldingRange.clear();
m_foldedFoldingRanges.clear();
qDeleteAll (m_foldingRanges);
m_foldingRanges.clear ();
/**
* folding changed!
*/
emit foldingRangesChanged ();
}
qint64 TextFolding::newFoldingRange (const KTextEditor::Range &range, FoldingRangeFlags flags)
{
/**
* sort out invalid and empty ranges
* that makes no sense, they will never grow again!
*/
if (!range.isValid() || range.isEmpty())
return -1;
/**
* create new folding region that we want to insert
* this will internally create moving cursors!
*/
FoldingRange *newRange = new FoldingRange (m_buffer, range, flags);
/**
* the construction of the text cursors might have invalidated this
* check and bail out if that happens
* bail out, too, if it can't be inserted!
*/
if ( !newRange->start->isValid()
|| !newRange->end->isValid()
|| !insertNewFoldingRange (0 /* no parent here */, m_foldingRanges, newRange)) {
/**
* cleanup and be done
*/
delete newRange;
return -1;
}
/**
* set id, catch overflows, even if they shall not happen
*/
newRange->id = ++m_idCounter;
if (newRange->id < 0)
newRange->id = m_idCounter = 0;
/**
* remember the range
*/
m_idToFoldingRange.insert (newRange->id, newRange);
/**
* update our folded ranges vector!
*/
bool updated = updateFoldedRangesForNewRange (newRange);
/**
* emit that something may have changed
* do that only, if updateFoldedRangesForNewRange did not already do the job!
*/
if (!updated)
emit foldingRangesChanged ();
/**
* all went fine, newRange is now registered internally!
*/
return newRange->id;
}
KTextEditor::Range TextFolding::foldingRange(qint64 id) const
{
FoldingRange* range = m_idToFoldingRange.value (id);
if (!range)
return KTextEditor::Range::invalid();
return KTextEditor::Range(range->start->toCursor(), range->end->toCursor());
}
bool TextFolding::foldRange (qint64 id)
{
/**
* try to find the range, else bail out
*/
FoldingRange *range = m_idToFoldingRange.value (id);
if (!range)
return false;
/**
* already folded? nothing to do
*/
if (range->flags & Folded)
return true;
/**
* fold and be done
*/
range->flags |= Folded;
updateFoldedRangesForNewRange (range);
return true;
}
bool TextFolding::unfoldRange (qint64 id, bool remove)
{
/**
* try to find the range, else bail out
*/
FoldingRange *range = m_idToFoldingRange.value (id);
if (!range)
return false;
/**
* nothing to do?
* range is already unfolded and we need not to remove it!
*/
if (!remove && !(range->flags & Folded))
return true;
/**
* do we need to delete the range?
*/
const bool deleteRange = remove || !(range->flags & Persistent);
/**
* first: remove the range, if forced or non-persistent!
*/
if (deleteRange) {
/**
* remove from outside visible mapping!
*/
m_idToFoldingRange.remove (id);
/**
* remove from folding vectors!
* FIXME: OPTIMIZE
*/
FoldingRange::Vector &parentVector = range->parent ? range->parent->nestedRanges : m_foldingRanges;
FoldingRange::Vector newParentVector;
Q_FOREACH (FoldingRange *curRange, parentVector) {
/**
* insert our nested ranges and reparent them
*/
if (curRange == range) {
Q_FOREACH (FoldingRange *newRange, range->nestedRanges) {
newRange->parent = range->parent;
newParentVector.push_back (newRange);
}
continue;
}
/**
* else just transfer elements
*/
newParentVector.push_back (curRange);
}
parentVector = newParentVector;
}
/**
* second: unfold the range, if needed!
*/
bool updated = false;
if (range->flags & Folded) {
range->flags &= ~Folded;
updated = updateFoldedRangesForRemovedRange (range);
}
/**
* emit that something may have changed
* do that only, if updateFoldedRangesForRemoveRange did not already do the job!
*/
if (!updated)
emit foldingRangesChanged ();
/**
* really delete the range, if needed!
*/
if (deleteRange) {
/**
* clear ranges first, they got moved!
*/
range->nestedRanges.clear ();
delete range;
}
/**
* be done ;)
*/
return true;
}
bool TextFolding::isLineVisible (int line, qint64 *foldedRangeId) const
{
/**
* skip if nothing folded
*/
if (m_foldedFoldingRanges.isEmpty())
return true;
/**
* search upper bound, index to item with start line higher than our one
*/
FoldingRange::Vector::const_iterator upperBound = qUpperBound (m_foldedFoldingRanges.begin(), m_foldedFoldingRanges.end(), line, compareRangeByStartWithLine);
if (upperBound != m_foldedFoldingRanges.begin())
--upperBound;
/**
* check if we overlap with the range in front of us
*/
const bool hidden = (((*upperBound)->end->line() >= line) && (line > (*upperBound)->start->line()));
/**
* fill in folded range id, if needed
*/
if (foldedRangeId)
(*foldedRangeId) = hidden ? (*upperBound)->id : -1;
/**
* visible == !hidden
*/
return !hidden;
}
void TextFolding::ensureLineIsVisible (int line)
{
/**
* skip if nothing folded
*/
if (m_foldedFoldingRanges.isEmpty())
return;
/**
* while not visible, unfold
*/
qint64 foldedRangeId = -1;
while (!isLineVisible (line, &foldedRangeId)) {
/**
* id should be valid!
*/
Q_ASSERT (foldedRangeId >= 0);
/**
* unfold shall work!
*/
const bool unfolded = unfoldRange (foldedRangeId);
(void) unfolded;
Q_ASSERT (unfolded);
}
}
int TextFolding::visibleLines () const
{
/**
* start with all lines we have
*/
int visibleLines = m_buffer.lines();
/**
* skip if nothing folded
*/
if (m_foldedFoldingRanges.isEmpty())
return visibleLines;
/**
* count all folded lines and subtract them from visible lines
*/
Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges)
visibleLines -= (range->end->line() - range->start->line());
/**
* be done, assert we did no trash
*/
Q_ASSERT (visibleLines > 0);
return visibleLines;
}
int TextFolding::lineToVisibleLine (int line) const
{
/**
* valid input needed!
*/
Q_ASSERT (line >= 0);
/**
* start with identity
*/
int visibleLine = line;
/**
* skip if nothing folded or first line
*/
if (m_foldedFoldingRanges.isEmpty() || (line == 0))
return visibleLine;
/**
* walk over all folded ranges until we reach the line
* keep track of seen visible lines, for the case we want to convert a hidden line!
*/
int seenVisibleLines = 0;
int lastLine = 0;
Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) {
/**
* abort if we reach our line!
*/
if (range->start->line() >= line)
break;
/**
* count visible lines
*/
seenVisibleLines += (range->start->line() - lastLine);
lastLine = range->end->line();
/**
* we might be contained in the region, then we return last visible line
*/
if (line <= range->end->line())
return seenVisibleLines;
/**
* subtrace folded lines
*/
visibleLine -= (range->end->line() - range->start->line());
}
/**
* be done, assert we did no trash
*/
Q_ASSERT (visibleLine >= 0);
return visibleLine;
}
int TextFolding::visibleLineToLine (int visibleLine) const
{
/**
* valid input needed!
*/
Q_ASSERT (visibleLine >= 0);
/**
* start with identity
*/
int line = visibleLine;
/**
* skip if nothing folded or first line
*/
if (m_foldedFoldingRanges.isEmpty() || (visibleLine == 0))
return line;
/**
* last visible line seen, as line in buffer
*/
int seenVisibleLines = 0;
int lastLine = 0;
int lastLineVisibleLines = 0;
Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) {
/**
* else compute visible lines and move last seen
*/
lastLineVisibleLines = seenVisibleLines;
seenVisibleLines += (range->start->line() - lastLine);
/**
* bail out if enough seen
*/
if (seenVisibleLines >= visibleLine)
break;
lastLine = range->end->line();
}
/**
* check if still no enough visible!
*/
if (seenVisibleLines < visibleLine)
lastLineVisibleLines = seenVisibleLines;
/**
* compute visible line
*/
line = (lastLine + (visibleLine - lastLineVisibleLines));
Q_ASSERT (line >= 0);
return line;
}
QVector<QPair<qint64, TextFolding::FoldingRangeFlags> > TextFolding::foldingRangesStartingOnLine (int line) const
{
/**
* results vector
*/
QVector<QPair<qint64, TextFolding::FoldingRangeFlags> > results;
/**
* recursively do binary search
*/
foldingRangesStartingOnLine (results, m_foldingRanges, line);
/**
* return found results
*/
return results;
}
void TextFolding::foldingRangesStartingOnLine (QVector<QPair<qint64, FoldingRangeFlags> > &results, const TextFolding::FoldingRange::Vector &ranges, int line) const
{
/**
* early out for no folds
*/
if (ranges.isEmpty())
return;
/**
* first: lower bound of start
*/
FoldingRange::Vector::const_iterator lowerBound = qLowerBound (ranges.begin(), ranges.end(), line, compareRangeByLineWithStart);
/**
* second: upper bound of end
*/
FoldingRange::Vector::const_iterator upperBound = qUpperBound (ranges.begin(), ranges.end(), line, compareRangeByStartWithLine);
/**
* we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us!
*/
if ((lowerBound != ranges.begin()) && ((*(lowerBound-1))->end->line() >= line))
--lowerBound;
/**
* for all of them, check if we start at right line and recurse
*/
for (FoldingRange::Vector::const_iterator it = lowerBound; it != upperBound; ++it) {
/**
* this range already ok? add it to results
*/
if ((*it)->start->line() == line)
results.push_back (qMakePair((*it)->id, (*it)->flags));
/**
* recurse anyway
*/
foldingRangesStartingOnLine (results, (*it)->nestedRanges, line);
}
}
QString TextFolding::debugDump () const
{
/**
* dump toplevel ranges recursively
*/
return QString ("tree %1 - folded %2").arg (debugDump (m_foldingRanges, true)).arg(debugDump (m_foldedFoldingRanges, false));
}
void TextFolding::debugPrint (const QString &title) const
{
// print title + content
printf ("%s\n %s\n", qPrintable (title), qPrintable(debugDump()));
}
QString TextFolding::debugDump (const TextFolding::FoldingRange::Vector &ranges, bool recurse)
{
/**
* dump all ranges recursively
*/
QString dump;
Q_FOREACH (FoldingRange *range, ranges) {
if (!dump.isEmpty())
dump += " ";
dump += QString ("[%1:%2 %3%4 ").arg (range->start->line()).arg(range->start->column()).arg((range->flags & Persistent) ? "p" : "").arg((range->flags & Folded) ? "f" : "");
/**
* recurse
*/
if (recurse) {
QString inner = debugDump (range->nestedRanges, recurse);
if (!inner.isEmpty())
dump += inner + " ";
}
dump += QString ("%1:%2]").arg (range->end->line()).arg(range->end->column());
}
return dump;
}
bool TextFolding::insertNewFoldingRange (FoldingRange *parent, FoldingRange::Vector &existingRanges, FoldingRange *newRange)
{
/**
* existing ranges are non-overlapping and sorted
* that means, we can search for lower bound of start of range and upper bound of end of range to find all "overlapping" ranges.
*/
/**
* first: lower bound of start
*/
FoldingRange::Vector::iterator lowerBound = qLowerBound (existingRanges.begin(), existingRanges.end(), newRange, compareRangeByStart);
/**
* second: upper bound of end
*/
FoldingRange::Vector::iterator upperBound = qUpperBound (existingRanges.begin(), existingRanges.end(), newRange, compareRangeByEnd);
/**
* we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us!
*/
if ((lowerBound != existingRanges.begin()) && ((*(lowerBound-1))->end->toCursor() > newRange->start->toCursor()))
--lowerBound;
/**
* now: first case, we overlap with nothing or hit exactly one range!
*/
if (lowerBound == upperBound) {
/**
* nothing we overlap with?
* then just insert and be done!
*/
if ((lowerBound == existingRanges.end()) || (newRange->start->toCursor() >= (*lowerBound)->end->toCursor()) || (newRange->end->toCursor() <= (*lowerBound)->start->toCursor())) {
/**
* insert + fix parent
*/
existingRanges.insert (lowerBound, newRange);
newRange->parent = parent;
/**
* all done
*/
return true;
}
/**
* we are contained in this one range?
* then recurse!
*/
if ((newRange->start->toCursor() >= (*lowerBound)->start->toCursor()) && (newRange->end->toCursor() <= (*lowerBound)->end->toCursor()))
return insertNewFoldingRange ((*lowerBound), (*lowerBound)->nestedRanges, newRange);
/**
* else: we might contain at least this fold, or many more, if this if block is not taken at all
* use the general code that checks for "we contain stuff" below!
*/
}
/**
* check if we contain other folds!
*/
FoldingRange::Vector::iterator it = lowerBound;
bool includeUpperBound = false;
FoldingRange::Vector nestedRanges;
while (it != existingRanges.end()) {
/**
* do we need to take look at upper bound, too?
* if not break
*/
if (it == upperBound) {
if (newRange->end->toCursor() <= (*upperBound)->start->toCursor())
break;
else
includeUpperBound = true;
}
/**
* if one region is not contained in the new one, abort!
* then this is not well nested!
*/
if (!((newRange->start->toCursor() <= (*it)->start->toCursor()) && (newRange->end->toCursor() >= (*it)->end->toCursor())))
return false;
/**
* include into new nested ranges
*/
nestedRanges.push_back ((*it));
/**
* end reached
*/
if (it == upperBound)
break;
/**
* else increment
*/
++it;
}
/**
* if we arrive here, all is nicely nested into our new range
* remove the contained ones here, insert new range with new nested ranges we already constructed
*/
it = existingRanges.erase (lowerBound, includeUpperBound ? (upperBound+1) : upperBound);
existingRanges.insert (it, newRange);
newRange->nestedRanges = nestedRanges;
/**
* correct parent mapping!
*/
newRange->parent = parent;
Q_FOREACH (FoldingRange *range, newRange->nestedRanges)
range->parent = newRange;
/**
* all nice
*/
return true;
}
bool TextFolding::compareRangeByStart (FoldingRange *a, FoldingRange *b)
{
return a->start->toCursor() < b->start->toCursor();
}
bool TextFolding::compareRangeByEnd (FoldingRange *a, FoldingRange *b)
{
return a->end->toCursor() < b->end->toCursor();
}
bool TextFolding::compareRangeByStartWithLine (int line, FoldingRange *range)
{
return (line < range->start->line());
}
bool TextFolding::compareRangeByLineWithStart (FoldingRange *range, int line)
{
return (range->start->line() < line);
}
bool TextFolding::updateFoldedRangesForNewRange (TextFolding::FoldingRange *newRange)
{
/**
* not folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector
*/
if (!(newRange->flags & Folded))
return false;
/**
* any of the parents folded? not interesting, too!
*/
TextFolding::FoldingRange *parent = newRange->parent;
while (parent) {
/**
* parent folded => be done
*/
if (parent->flags & Folded)
return false;
/**
* walk up
*/
parent = parent->parent;
}
/**
* ok, if we arrive here, we are a folded range and we have no folded parent
* we now want to add this range to the m_foldedFoldingRanges vector, just removing any ranges that is included in it!
* TODO: OPTIMIZE
*/
FoldingRange::Vector newFoldedFoldingRanges;
bool newRangeInserted = false;
Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) {
/**
* contained? kill
*/
if ((newRange->start->toCursor() <= range->start->toCursor()) && (newRange->end->toCursor() >= range->end->toCursor()))
continue;
/**
* range is behind newRange?
* insert newRange if not already done
*/
if (!newRangeInserted && (range->start->toCursor() >= newRange->end->toCursor())) {
newFoldedFoldingRanges.push_back (newRange);
newRangeInserted = true;
}
/**
* just transfer range
*/
newFoldedFoldingRanges.push_back (range);
}
/**
* last: insert new range, if not done
*/
if (!newRangeInserted)
newFoldedFoldingRanges.push_back (newRange);
/**
* fixup folded ranges
*/
m_foldedFoldingRanges = newFoldedFoldingRanges;
/**
* folding changed!
*/
emit foldingRangesChanged ();
/**
* all fine, stuff done, signal emitted
*/
return true;
}
bool TextFolding::updateFoldedRangesForRemovedRange (TextFolding::FoldingRange *oldRange)
{
/**
* folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector
*/
if (oldRange->flags & Folded)
return false;
/**
* any of the parents folded? not interesting, too!
*/
TextFolding::FoldingRange *parent = oldRange->parent;
while (parent) {
/**
* parent folded => be done
*/
if (parent->flags & Folded)
return false;
/**
* walk up
*/
parent = parent->parent;
}
/**
* ok, if we arrive here, we are a unfolded range and we have no folded parent
* we now want to remove this range from the m_foldedFoldingRanges vector and include our nested folded ranges!
* TODO: OPTIMIZE
*/
FoldingRange::Vector newFoldedFoldingRanges;
Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) {
/**
* right range? insert folded nested ranges
*/
if (range == oldRange) {
appendFoldedRanges (newFoldedFoldingRanges, oldRange->nestedRanges);
continue;
}
/**
* just transfer range
*/
newFoldedFoldingRanges.push_back (range);
}
/**
* fixup folded ranges
*/
m_foldedFoldingRanges = newFoldedFoldingRanges;
/**
* folding changed!
*/
emit foldingRangesChanged ();
/**
* all fine, stuff done, signal emitted
*/
return true;
}
void TextFolding::appendFoldedRanges (TextFolding::FoldingRange::Vector &newFoldedFoldingRanges, const TextFolding::FoldingRange::Vector &ranges) const
{
/**
* search for folded ranges and append them
*/
Q_FOREACH (FoldingRange *range, ranges) {
/**
* itself folded? append
*/
if (range->flags & Folded) {
newFoldedFoldingRanges.push_back (range);
continue;
}
/**
* else: recurse!
*/
appendFoldedRanges (newFoldedFoldingRanges, range->nestedRanges);
}
}
QVariantList TextFolding::exportFoldingRanges () const
{
QVariantList folds;
exportFoldingRanges (m_foldingRanges, folds);
return folds;
}
void TextFolding::exportFoldingRanges (const TextFolding::FoldingRange::Vector &ranges, QVariantList &folds)
{
/**
* dump all ranges recursively
*/
Q_FOREACH (FoldingRange *range, ranges) {
/**
* construct one range and dump to folds
*/
QVariantMap rangeMap;
rangeMap["startLine"] = range->start->line();
rangeMap["startColumn"] = range->start->column();
rangeMap["endLine"] = range->end->line();
rangeMap["endColumn"] = range->end->column();
rangeMap["flags"] = (int)range->flags;
folds.append (rangeMap);
/**
* recurse
*/
exportFoldingRanges (range->nestedRanges, folds);
}
}
void TextFolding::importFoldingRanges (const QVariantList &folds)
{
/**
* try to create all folding ranges
*/
Q_FOREACH (QVariant rangeVariant, folds) {
/**
* get map
*/
QVariantMap rangeMap = rangeVariant.toMap ();
/**
* construct range start/end
*/
KTextEditor::Cursor start (rangeMap["startLine"].toInt(), rangeMap["startColumn"].toInt());
KTextEditor::Cursor end (rangeMap["endLine"].toInt(), rangeMap["endColumn"].toInt());
/**
* get flags
*/
int rawFlags = rangeMap["flags"].toInt();
FoldingRangeFlags flags;
if (rawFlags & Persistent)
flags = Persistent;
if (rawFlags & Folded)
flags = Folded;
/**
* create folding range
*/
newFoldingRange (KTextEditor::Range (start, end), flags);
}
}
}