| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2013 Christoph Cullmann <cullmann@kde.org> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "katetextfolding.h" |
| 8 | #include "katedocument.h" |
| 9 | #include "katetextbuffer.h" |
| 10 | #include "katetextrange.h" |
| 11 | |
| 12 | #include <QJsonObject> |
| 13 | |
| 14 | namespace Kate |
| 15 | { |
| 16 | TextFolding::FoldingRange::FoldingRange(TextBuffer &buffer, KTextEditor::Range range, FoldingRangeFlags _flags) |
| 17 | : start(new TextCursor(&buffer, range.start(), KTextEditor::MovingCursor::MoveOnInsert)) |
| 18 | , end(new TextCursor(&buffer, range.end(), KTextEditor::MovingCursor::MoveOnInsert)) |
| 19 | , parent(nullptr) |
| 20 | , flags(_flags) |
| 21 | , id(-1) |
| 22 | { |
| 23 | } |
| 24 | |
| 25 | TextFolding::FoldingRange::~FoldingRange() |
| 26 | { |
| 27 | // kill all our data! |
| 28 | // this will recurse all sub-structures! |
| 29 | delete start; |
| 30 | delete end; |
| 31 | qDeleteAll(c: nestedRanges); |
| 32 | } |
| 33 | |
| 34 | TextFolding::TextFolding(TextBuffer &buffer) |
| 35 | : QObject() |
| 36 | , m_buffer(buffer) |
| 37 | , m_idCounter(-1) |
| 38 | { |
| 39 | // connect needed signals from buffer |
| 40 | connect(sender: &m_buffer, signal: &TextBuffer::cleared, context: this, slot: &TextFolding::clear); |
| 41 | } |
| 42 | |
| 43 | TextFolding::~TextFolding() |
| 44 | { |
| 45 | // only delete the folding ranges, the folded ranges and mapped ranges are the same objects |
| 46 | qDeleteAll(c: m_foldingRanges); |
| 47 | } |
| 48 | |
| 49 | void TextFolding::clear() |
| 50 | { |
| 51 | // reset counter |
| 52 | m_idCounter = -1; |
| 53 | clearFoldingRanges(); |
| 54 | } |
| 55 | |
| 56 | void TextFolding::clearFoldingRanges() |
| 57 | { |
| 58 | // no ranges, no work |
| 59 | if (m_foldingRanges.isEmpty()) { |
| 60 | // assert all stuff is consistent and return! |
| 61 | Q_ASSERT(m_idToFoldingRange.isEmpty()); |
| 62 | Q_ASSERT(m_foldedFoldingRanges.isEmpty()); |
| 63 | return; |
| 64 | } |
| 65 | |
| 66 | // cleanup |
| 67 | m_idToFoldingRange.clear(); |
| 68 | m_foldedFoldingRanges.clear(); |
| 69 | qDeleteAll(c: m_foldingRanges); |
| 70 | m_foldingRanges.clear(); |
| 71 | |
| 72 | // folding changed! |
| 73 | Q_EMIT foldingRangesChanged(); |
| 74 | } |
| 75 | |
| 76 | qint64 TextFolding::newFoldingRange(KTextEditor::Range range, FoldingRangeFlags flags) |
| 77 | { |
| 78 | // sort out invalid and empty ranges |
| 79 | // that makes no sense, they will never grow again! |
| 80 | if (!range.isValid() || range.isEmpty()) { |
| 81 | return -1; |
| 82 | } |
| 83 | |
| 84 | // create new folding region that we want to insert |
| 85 | // this will internally create moving cursors! |
| 86 | FoldingRange *newRange = new FoldingRange(m_buffer, range, flags); |
| 87 | |
| 88 | // the construction of the text cursors might have invalidated this |
| 89 | // check and bail out if that happens |
| 90 | // bail out, too, if it can't be inserted! |
| 91 | if (!newRange->start->isValid() || !newRange->end->isValid() || !insertNewFoldingRange(parent: nullptr /* no parent here */, existingRanges&: m_foldingRanges, newRange)) { |
| 92 | // cleanup and be done |
| 93 | delete newRange; |
| 94 | return -1; |
| 95 | } |
| 96 | |
| 97 | // set id, catch overflows, even if they shall not happen |
| 98 | newRange->id = ++m_idCounter; |
| 99 | if (newRange->id < 0) { |
| 100 | newRange->id = m_idCounter = 0; |
| 101 | } |
| 102 | |
| 103 | // remember the range |
| 104 | m_idToFoldingRange.insert(key: newRange->id, value: newRange); |
| 105 | |
| 106 | // update our folded ranges vector! |
| 107 | bool updated = updateFoldedRangesForNewRange(newRange); |
| 108 | |
| 109 | // emit that something may have changed |
| 110 | // do that only, if updateFoldedRangesForNewRange did not already do the job! |
| 111 | if (!updated) { |
| 112 | Q_EMIT foldingRangesChanged(); |
| 113 | } |
| 114 | |
| 115 | // all went fine, newRange is now registered internally! |
| 116 | return newRange->id; |
| 117 | } |
| 118 | |
| 119 | KTextEditor::Range TextFolding::foldingRange(qint64 id) const |
| 120 | { |
| 121 | FoldingRange *range = m_idToFoldingRange.value(key: id); |
| 122 | if (!range) { |
| 123 | return KTextEditor::Range::invalid(); |
| 124 | } |
| 125 | |
| 126 | return KTextEditor::Range(range->start->toCursor(), range->end->toCursor()); |
| 127 | } |
| 128 | |
| 129 | bool TextFolding::foldRange(qint64 id) |
| 130 | { |
| 131 | // try to find the range, else bail out |
| 132 | FoldingRange *range = m_idToFoldingRange.value(key: id); |
| 133 | if (!range) { |
| 134 | return false; |
| 135 | } |
| 136 | |
| 137 | // already folded? nothing to do |
| 138 | if (range->flags & Folded) { |
| 139 | return true; |
| 140 | } |
| 141 | |
| 142 | // fold and be done |
| 143 | range->flags |= Folded; |
| 144 | updateFoldedRangesForNewRange(newRange: range); |
| 145 | return true; |
| 146 | } |
| 147 | |
| 148 | bool TextFolding::unfoldRange(qint64 id, bool remove) |
| 149 | { |
| 150 | // try to find the range, else bail out |
| 151 | FoldingRange *range = m_idToFoldingRange.value(key: id); |
| 152 | if (!range) { |
| 153 | return false; |
| 154 | } |
| 155 | |
| 156 | // nothing to do? |
| 157 | // range is already unfolded and we need not to remove it! |
| 158 | if (!remove && !(range->flags & Folded)) { |
| 159 | return true; |
| 160 | } |
| 161 | |
| 162 | // do we need to delete the range? |
| 163 | const bool deleteRange = remove || !(range->flags & Persistent); |
| 164 | |
| 165 | // first: remove the range, if forced or non-persistent! |
| 166 | if (deleteRange) { |
| 167 | // remove from outside visible mapping! |
| 168 | m_idToFoldingRange.remove(key: id); |
| 169 | |
| 170 | // remove from folding vectors! |
| 171 | // FIXME: OPTIMIZE |
| 172 | FoldingRange::Vector &parentVector = range->parent ? range->parent->nestedRanges : m_foldingRanges; |
| 173 | FoldingRange::Vector newParentVector; |
| 174 | for (FoldingRange *curRange : std::as_const(t&: parentVector)) { |
| 175 | // insert our nested ranges and reparent them |
| 176 | if (curRange == range) { |
| 177 | for (FoldingRange *newRange : std::as_const(t&: range->nestedRanges)) { |
| 178 | newRange->parent = range->parent; |
| 179 | newParentVector.push_back(t: newRange); |
| 180 | } |
| 181 | |
| 182 | continue; |
| 183 | } |
| 184 | |
| 185 | // else just transfer elements |
| 186 | newParentVector.push_back(t: curRange); |
| 187 | } |
| 188 | parentVector = newParentVector; |
| 189 | } |
| 190 | |
| 191 | // second: unfold the range, if needed! |
| 192 | bool updated = false; |
| 193 | if (range->flags & Folded) { |
| 194 | range->flags &= ~Folded; |
| 195 | updated = updateFoldedRangesForRemovedRange(oldRange: range); |
| 196 | } |
| 197 | |
| 198 | // emit that something may have changed |
| 199 | // do that only, if updateFoldedRangesForRemoveRange did not already do the job! |
| 200 | if (!updated) { |
| 201 | Q_EMIT foldingRangesChanged(); |
| 202 | } |
| 203 | |
| 204 | // really delete the range, if needed! |
| 205 | if (deleteRange) { |
| 206 | // clear ranges first, they got moved! |
| 207 | range->nestedRanges.clear(); |
| 208 | delete range; |
| 209 | } |
| 210 | |
| 211 | // be done ;) |
| 212 | return true; |
| 213 | } |
| 214 | |
| 215 | bool TextFolding::isLineVisible(int line, qint64 *foldedRangeId) const |
| 216 | { |
| 217 | // skip if nothing folded |
| 218 | if (m_foldedFoldingRanges.isEmpty()) { |
| 219 | return true; |
| 220 | } |
| 221 | |
| 222 | // search upper bound, index to item with start line higher than our one |
| 223 | FoldingRange::Vector::const_iterator upperBound = |
| 224 | std::upper_bound(first: m_foldedFoldingRanges.begin(), last: m_foldedFoldingRanges.end(), val: line, comp: compareRangeByStartWithLine); |
| 225 | if (upperBound != m_foldedFoldingRanges.begin()) { |
| 226 | --upperBound; |
| 227 | } |
| 228 | |
| 229 | // check if we overlap with the range in front of us |
| 230 | const bool hidden = (((*upperBound)->end->line() >= line) && (line > (*upperBound)->start->line())); |
| 231 | |
| 232 | // fill in folded range id, if needed |
| 233 | if (foldedRangeId) { |
| 234 | (*foldedRangeId) = hidden ? (*upperBound)->id : -1; |
| 235 | } |
| 236 | |
| 237 | // visible == !hidden |
| 238 | return !hidden; |
| 239 | } |
| 240 | |
| 241 | void TextFolding::ensureLineIsVisible(int line) |
| 242 | { |
| 243 | // skip if nothing folded |
| 244 | if (m_foldedFoldingRanges.isEmpty()) { |
| 245 | return; |
| 246 | } |
| 247 | |
| 248 | // while not visible, unfold |
| 249 | qint64 foldedRangeId = -1; |
| 250 | while (!isLineVisible(line, foldedRangeId: &foldedRangeId)) { |
| 251 | // id should be valid! |
| 252 | Q_ASSERT(foldedRangeId >= 0); |
| 253 | |
| 254 | // unfold shall work! |
| 255 | const bool unfolded = unfoldRange(id: foldedRangeId); |
| 256 | (void)unfolded; |
| 257 | Q_ASSERT(unfolded); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | int TextFolding::visibleLines() const |
| 262 | { |
| 263 | // start with all lines we have |
| 264 | int visibleLines = m_buffer.lines(); |
| 265 | |
| 266 | // skip if nothing folded |
| 267 | if (m_foldedFoldingRanges.isEmpty()) { |
| 268 | return visibleLines; |
| 269 | } |
| 270 | |
| 271 | // count all folded lines and subtract them from visible lines |
| 272 | for (FoldingRange *range : m_foldedFoldingRanges) { |
| 273 | visibleLines -= (range->end->line() - range->start->line()); |
| 274 | } |
| 275 | |
| 276 | // be done, assert we did no trash |
| 277 | Q_ASSERT(visibleLines > 0); |
| 278 | return visibleLines; |
| 279 | } |
| 280 | |
| 281 | int TextFolding::lineToVisibleLine(int line) const |
| 282 | { |
| 283 | // valid input needed! |
| 284 | Q_ASSERT(line >= 0); |
| 285 | |
| 286 | // start with identity |
| 287 | int visibleLine = line; |
| 288 | |
| 289 | // skip if nothing folded or first line |
| 290 | if (m_foldedFoldingRanges.isEmpty() || (line == 0)) { |
| 291 | return visibleLine; |
| 292 | } |
| 293 | |
| 294 | // walk over all folded ranges until we reach the line |
| 295 | // keep track of seen visible lines, for the case we want to convert a hidden line! |
| 296 | int seenVisibleLines = 0; |
| 297 | int lastLine = 0; |
| 298 | for (FoldingRange *range : m_foldedFoldingRanges) { |
| 299 | // abort if we reach our line! |
| 300 | if (range->start->line() >= line) { |
| 301 | break; |
| 302 | } |
| 303 | |
| 304 | // count visible lines |
| 305 | seenVisibleLines += (range->start->line() - lastLine); |
| 306 | lastLine = range->end->line(); |
| 307 | |
| 308 | // we might be contained in the region, then we return last visible line |
| 309 | if (line <= range->end->line()) { |
| 310 | return seenVisibleLines; |
| 311 | } |
| 312 | |
| 313 | // subtrace folded lines |
| 314 | visibleLine -= (range->end->line() - range->start->line()); |
| 315 | } |
| 316 | |
| 317 | // be done, assert we did no trash |
| 318 | Q_ASSERT(visibleLine >= 0); |
| 319 | return visibleLine; |
| 320 | } |
| 321 | |
| 322 | int TextFolding::visibleLineToLine(int visibleLine) const |
| 323 | { |
| 324 | // valid input needed! |
| 325 | Q_ASSERT(visibleLine >= 0); |
| 326 | |
| 327 | // start with identity |
| 328 | int line = visibleLine; |
| 329 | |
| 330 | // skip if nothing folded or first line |
| 331 | if (m_foldedFoldingRanges.isEmpty() || (visibleLine == 0)) { |
| 332 | return line; |
| 333 | } |
| 334 | |
| 335 | // last visible line seen, as line in buffer |
| 336 | int seenVisibleLines = 0; |
| 337 | int lastLine = 0; |
| 338 | int lastLineVisibleLines = 0; |
| 339 | for (FoldingRange *range : m_foldedFoldingRanges) { |
| 340 | // else compute visible lines and move last seen |
| 341 | lastLineVisibleLines = seenVisibleLines; |
| 342 | seenVisibleLines += (range->start->line() - lastLine); |
| 343 | |
| 344 | // bail out if enough seen |
| 345 | if (seenVisibleLines >= visibleLine) { |
| 346 | break; |
| 347 | } |
| 348 | |
| 349 | lastLine = range->end->line(); |
| 350 | } |
| 351 | |
| 352 | // check if still no enough visible! |
| 353 | if (seenVisibleLines < visibleLine) { |
| 354 | lastLineVisibleLines = seenVisibleLines; |
| 355 | } |
| 356 | |
| 357 | // compute visible line |
| 358 | line = (lastLine + (visibleLine - lastLineVisibleLines)); |
| 359 | Q_ASSERT(line >= 0); |
| 360 | return line; |
| 361 | } |
| 362 | |
| 363 | QList<QPair<qint64, TextFolding::FoldingRangeFlags>> TextFolding::foldingRangesStartingOnLine(int line) const |
| 364 | { |
| 365 | // results vector |
| 366 | QList<QPair<qint64, TextFolding::FoldingRangeFlags>> results; |
| 367 | |
| 368 | // recursively do binary search |
| 369 | foldingRangesStartingOnLine(results, ranges: m_foldingRanges, line); |
| 370 | |
| 371 | // return found results |
| 372 | return results; |
| 373 | } |
| 374 | |
| 375 | void TextFolding::foldingRangesStartingOnLine(QList<QPair<qint64, FoldingRangeFlags>> &results, const TextFolding::FoldingRange::Vector &ranges, int line) const |
| 376 | { |
| 377 | // early out for no folds |
| 378 | if (ranges.isEmpty()) { |
| 379 | return; |
| 380 | } |
| 381 | |
| 382 | // first: lower bound of start |
| 383 | FoldingRange::Vector::const_iterator lowerBound = std::lower_bound(first: ranges.begin(), last: ranges.end(), val: line, comp: compareRangeByLineWithStart); |
| 384 | |
| 385 | // second: upper bound of end |
| 386 | FoldingRange::Vector::const_iterator upperBound = std::upper_bound(first: ranges.begin(), last: ranges.end(), val: line, comp: compareRangeByStartWithLine); |
| 387 | |
| 388 | // 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! |
| 389 | if ((lowerBound != ranges.begin()) && ((*(lowerBound - 1))->end->line() >= line)) { |
| 390 | --lowerBound; |
| 391 | } |
| 392 | |
| 393 | // for all of them, check if we start at right line and recurse |
| 394 | for (FoldingRange::Vector::const_iterator it = lowerBound; it != upperBound; ++it) { |
| 395 | // this range already ok? add it to results |
| 396 | if ((*it)->start->line() == line) { |
| 397 | results.push_back(t: qMakePair(value1&: (*it)->id, value2&: (*it)->flags)); |
| 398 | } |
| 399 | |
| 400 | // recurse anyway |
| 401 | foldingRangesStartingOnLine(results, ranges: (*it)->nestedRanges, line); |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | QList<QPair<qint64, TextFolding::FoldingRangeFlags>> TextFolding::foldingRangesForParentRange(qint64 parentRangeId) const |
| 406 | { |
| 407 | // toplevel ranges requested or real parent? |
| 408 | const FoldingRange::Vector *ranges = nullptr; |
| 409 | if (parentRangeId == -1) { |
| 410 | ranges = &m_foldingRanges; |
| 411 | } else if (FoldingRange *range = m_idToFoldingRange.value(key: parentRangeId)) { |
| 412 | ranges = &range->nestedRanges; |
| 413 | } |
| 414 | |
| 415 | // no ranges => nothing to do |
| 416 | QList<QPair<qint64, FoldingRangeFlags>> results; |
| 417 | if (!ranges) { |
| 418 | return results; |
| 419 | } |
| 420 | |
| 421 | // else convert ranges to id + flags and pass that back |
| 422 | for (FoldingRange::Vector::const_iterator it = ranges->begin(); it != ranges->end(); ++it) { |
| 423 | results.append(t: qMakePair(value1&: (*it)->id, value2&: (*it)->flags)); |
| 424 | } |
| 425 | return results; |
| 426 | } |
| 427 | |
| 428 | QString TextFolding::debugDump() const |
| 429 | { |
| 430 | // dump toplevel ranges recursively |
| 431 | return QStringLiteral("tree %1 - folded %2" ).arg(args: debugDump(ranges: m_foldingRanges, recurse: true), args: debugDump(ranges: m_foldedFoldingRanges, recurse: false)); |
| 432 | } |
| 433 | |
| 434 | void TextFolding::debugPrint(const QString &title) const |
| 435 | { |
| 436 | // print title + content |
| 437 | printf(format: "%s\n %s\n" , qPrintable(title), qPrintable(debugDump())); |
| 438 | } |
| 439 | |
| 440 | void TextFolding::editEnd(int startLine, int endLine, std::function<bool(int)> isLineFoldingStart) |
| 441 | { |
| 442 | // search upper bound, index to item with start line higher than our one |
| 443 | auto foldIt = std::upper_bound(first: m_foldedFoldingRanges.begin(), last: m_foldedFoldingRanges.end(), val: startLine, comp: compareRangeByStartWithLine); |
| 444 | if (foldIt != m_foldedFoldingRanges.begin()) { |
| 445 | --foldIt; |
| 446 | } |
| 447 | |
| 448 | // handle all ranges until we go behind the last line |
| 449 | bool anyUpdate = false; |
| 450 | while (foldIt != m_foldedFoldingRanges.end() && (*foldIt)->start->line() <= endLine) { |
| 451 | // shall we keep this folding? |
| 452 | if (isLineFoldingStart((*foldIt)->start->line())) { |
| 453 | ++foldIt; |
| 454 | continue; |
| 455 | } |
| 456 | |
| 457 | // else kill it |
| 458 | m_foldingRanges.removeOne(t: *foldIt); |
| 459 | m_idToFoldingRange.remove(key: (*foldIt)->id); |
| 460 | delete *foldIt; |
| 461 | foldIt = m_foldedFoldingRanges.erase(pos: foldIt); |
| 462 | anyUpdate = true; |
| 463 | } |
| 464 | |
| 465 | // ensure we do the proper updates outside |
| 466 | // we might change some range outside of the lines we edited |
| 467 | if (anyUpdate) { |
| 468 | Q_EMIT foldingRangesChanged(); |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | QString TextFolding::debugDump(const TextFolding::FoldingRange::Vector &ranges, bool recurse) |
| 473 | { |
| 474 | // dump all ranges recursively |
| 475 | QString dump; |
| 476 | for (FoldingRange *range : ranges) { |
| 477 | if (!dump.isEmpty()) { |
| 478 | dump += QLatin1Char(' '); |
| 479 | } |
| 480 | |
| 481 | const QString persistent = (range->flags & Persistent) ? QStringLiteral("p" ) : QString(); |
| 482 | const QString folded = (range->flags & Folded) ? QStringLiteral("f" ) : QString(); |
| 483 | dump += QStringLiteral("[%1:%2 %3%4 " ).arg(a: range->start->line()).arg(a: range->start->column()).arg(args: persistent, args: folded); |
| 484 | |
| 485 | // recurse |
| 486 | if (recurse) { |
| 487 | QString inner = debugDump(ranges: range->nestedRanges, recurse); |
| 488 | if (!inner.isEmpty()) { |
| 489 | dump += inner + QLatin1Char(' '); |
| 490 | } |
| 491 | } |
| 492 | |
| 493 | dump += QStringLiteral("%1:%2]" ).arg(a: range->end->line()).arg(a: range->end->column()); |
| 494 | } |
| 495 | return dump; |
| 496 | } |
| 497 | |
| 498 | bool TextFolding::insertNewFoldingRange(FoldingRange *parent, FoldingRange::Vector &existingRanges, FoldingRange *newRange) |
| 499 | { |
| 500 | // existing ranges are non-overlapping and sorted |
| 501 | // that means, we can search for lower bound of start of range and upper bound of end of range to find all "overlapping" ranges. |
| 502 | |
| 503 | // first: lower bound of start |
| 504 | FoldingRange::Vector::iterator lowerBound = std::lower_bound(first: existingRanges.begin(), last: existingRanges.end(), val: newRange, comp: compareRangeByStart); |
| 505 | |
| 506 | // second: upper bound of end |
| 507 | FoldingRange::Vector::iterator upperBound = std::upper_bound(first: existingRanges.begin(), last: existingRanges.end(), val: newRange, comp: compareRangeByEnd); |
| 508 | |
| 509 | // 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! |
| 510 | if ((lowerBound != existingRanges.begin()) && ((*(lowerBound - 1))->end->toCursor() > newRange->start->toCursor())) { |
| 511 | --lowerBound; |
| 512 | } |
| 513 | |
| 514 | // now: first case, we overlap with nothing or hit exactly one range! |
| 515 | if (lowerBound == upperBound) { |
| 516 | // nothing we overlap with? |
| 517 | // then just insert and be done! |
| 518 | if ((lowerBound == existingRanges.end()) || (newRange->start->toCursor() >= (*lowerBound)->end->toCursor()) |
| 519 | || (newRange->end->toCursor() <= (*lowerBound)->start->toCursor())) { |
| 520 | // insert + fix parent |
| 521 | existingRanges.insert(before: lowerBound, t: newRange); |
| 522 | newRange->parent = parent; |
| 523 | |
| 524 | // all done |
| 525 | return true; |
| 526 | } |
| 527 | |
| 528 | // we are contained in this one range? |
| 529 | // then recurse! |
| 530 | if ((newRange->start->toCursor() >= (*lowerBound)->start->toCursor()) && (newRange->end->toCursor() <= (*lowerBound)->end->toCursor())) { |
| 531 | return insertNewFoldingRange(parent: (*lowerBound), existingRanges&: (*lowerBound)->nestedRanges, newRange); |
| 532 | } |
| 533 | |
| 534 | // else: we might contain at least this fold, or many more, if this if block is not taken at all |
| 535 | // use the general code that checks for "we contain stuff" below! |
| 536 | } |
| 537 | |
| 538 | // check if we contain other folds! |
| 539 | FoldingRange::Vector::iterator it = lowerBound; |
| 540 | bool includeUpperBound = false; |
| 541 | FoldingRange::Vector nestedRanges; |
| 542 | while (it != existingRanges.end()) { |
| 543 | // do we need to take look at upper bound, too? |
| 544 | // if not break |
| 545 | if (it == upperBound) { |
| 546 | if (newRange->end->toCursor() <= (*upperBound)->start->toCursor()) { |
| 547 | break; |
| 548 | } else { |
| 549 | includeUpperBound = true; |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | // if one region is not contained in the new one, abort! |
| 554 | // then this is not well nested! |
| 555 | if (!((newRange->start->toCursor() <= (*it)->start->toCursor()) && (newRange->end->toCursor() >= (*it)->end->toCursor()))) { |
| 556 | return false; |
| 557 | } |
| 558 | |
| 559 | // include into new nested ranges |
| 560 | nestedRanges.push_back(t: (*it)); |
| 561 | |
| 562 | // end reached |
| 563 | if (it == upperBound) { |
| 564 | break; |
| 565 | } |
| 566 | |
| 567 | // else increment |
| 568 | ++it; |
| 569 | } |
| 570 | |
| 571 | // if we arrive here, all is nicely nested into our new range |
| 572 | // remove the contained ones here, insert new range with new nested ranges we already constructed |
| 573 | it = existingRanges.erase(abegin: lowerBound, aend: includeUpperBound ? (upperBound + 1) : upperBound); |
| 574 | existingRanges.insert(before: it, t: newRange); |
| 575 | newRange->nestedRanges = nestedRanges; |
| 576 | |
| 577 | // correct parent mapping! |
| 578 | newRange->parent = parent; |
| 579 | for (FoldingRange *range : std::as_const(t&: newRange->nestedRanges)) { |
| 580 | range->parent = newRange; |
| 581 | } |
| 582 | |
| 583 | // all nice |
| 584 | return true; |
| 585 | } |
| 586 | |
| 587 | bool TextFolding::compareRangeByStart(FoldingRange *a, FoldingRange *b) |
| 588 | { |
| 589 | return a->start->toCursor() < b->start->toCursor(); |
| 590 | } |
| 591 | |
| 592 | bool TextFolding::compareRangeByEnd(FoldingRange *a, FoldingRange *b) |
| 593 | { |
| 594 | return a->end->toCursor() < b->end->toCursor(); |
| 595 | } |
| 596 | |
| 597 | bool TextFolding::compareRangeByStartWithLine(int line, FoldingRange *range) |
| 598 | { |
| 599 | return (line < range->start->line()); |
| 600 | } |
| 601 | |
| 602 | bool TextFolding::compareRangeByLineWithStart(FoldingRange *range, int line) |
| 603 | { |
| 604 | return (range->start->line() < line); |
| 605 | } |
| 606 | |
| 607 | bool TextFolding::updateFoldedRangesForNewRange(TextFolding::FoldingRange *newRange) |
| 608 | { |
| 609 | // not folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector |
| 610 | if (!(newRange->flags & Folded)) { |
| 611 | return false; |
| 612 | } |
| 613 | |
| 614 | // any of the parents folded? not interesting, too! |
| 615 | TextFolding::FoldingRange *parent = newRange->parent; |
| 616 | while (parent) { |
| 617 | // parent folded => be done |
| 618 | if (parent->flags & Folded) { |
| 619 | return false; |
| 620 | } |
| 621 | |
| 622 | // walk up |
| 623 | parent = parent->parent; |
| 624 | } |
| 625 | |
| 626 | // ok, if we arrive here, we are a folded range and we have no folded parent |
| 627 | // we now want to add this range to the m_foldedFoldingRanges vector, just removing any ranges that is included in it! |
| 628 | // TODO: OPTIMIZE |
| 629 | FoldingRange::Vector newFoldedFoldingRanges; |
| 630 | bool newRangeInserted = false; |
| 631 | for (FoldingRange *range : std::as_const(t&: m_foldedFoldingRanges)) { |
| 632 | // contained? kill |
| 633 | if ((newRange->start->toCursor() <= range->start->toCursor()) && (newRange->end->toCursor() >= range->end->toCursor())) { |
| 634 | continue; |
| 635 | } |
| 636 | |
| 637 | // range is behind newRange? |
| 638 | // insert newRange if not already done |
| 639 | if (!newRangeInserted && (range->start->toCursor() >= newRange->end->toCursor())) { |
| 640 | newFoldedFoldingRanges.push_back(t: newRange); |
| 641 | newRangeInserted = true; |
| 642 | } |
| 643 | |
| 644 | // just transfer range |
| 645 | newFoldedFoldingRanges.push_back(t: range); |
| 646 | } |
| 647 | |
| 648 | // last: insert new range, if not done |
| 649 | if (!newRangeInserted) { |
| 650 | newFoldedFoldingRanges.push_back(t: newRange); |
| 651 | } |
| 652 | |
| 653 | // fixup folded ranges |
| 654 | m_foldedFoldingRanges = newFoldedFoldingRanges; |
| 655 | |
| 656 | // folding changed! |
| 657 | Q_EMIT foldingRangesChanged(); |
| 658 | |
| 659 | // all fine, stuff done, signal emitted |
| 660 | return true; |
| 661 | } |
| 662 | |
| 663 | bool TextFolding::updateFoldedRangesForRemovedRange(TextFolding::FoldingRange *oldRange) |
| 664 | { |
| 665 | // folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector |
| 666 | if (oldRange->flags & Folded) { |
| 667 | return false; |
| 668 | } |
| 669 | |
| 670 | // any of the parents folded? not interesting, too! |
| 671 | TextFolding::FoldingRange *parent = oldRange->parent; |
| 672 | while (parent) { |
| 673 | // parent folded => be done |
| 674 | if (parent->flags & Folded) { |
| 675 | return false; |
| 676 | } |
| 677 | |
| 678 | // walk up |
| 679 | parent = parent->parent; |
| 680 | } |
| 681 | |
| 682 | // ok, if we arrive here, we are a unfolded range and we have no folded parent |
| 683 | // we now want to remove this range from the m_foldedFoldingRanges vector and include our nested folded ranges! |
| 684 | // TODO: OPTIMIZE |
| 685 | FoldingRange::Vector newFoldedFoldingRanges; |
| 686 | for (FoldingRange *range : std::as_const(t&: m_foldedFoldingRanges)) { |
| 687 | // right range? insert folded nested ranges |
| 688 | if (range == oldRange) { |
| 689 | appendFoldedRanges(newFoldedFoldingRanges, ranges: oldRange->nestedRanges); |
| 690 | continue; |
| 691 | } |
| 692 | |
| 693 | // just transfer range |
| 694 | newFoldedFoldingRanges.push_back(t: range); |
| 695 | } |
| 696 | |
| 697 | // fixup folded ranges |
| 698 | m_foldedFoldingRanges = newFoldedFoldingRanges; |
| 699 | |
| 700 | // folding changed! |
| 701 | Q_EMIT foldingRangesChanged(); |
| 702 | |
| 703 | // all fine, stuff done, signal emitted |
| 704 | return true; |
| 705 | } |
| 706 | |
| 707 | void TextFolding::appendFoldedRanges(TextFolding::FoldingRange::Vector &newFoldedFoldingRanges, const TextFolding::FoldingRange::Vector &ranges) const |
| 708 | { |
| 709 | // search for folded ranges and append them |
| 710 | for (FoldingRange *range : ranges) { |
| 711 | // itself folded? append |
| 712 | if (range->flags & Folded) { |
| 713 | newFoldedFoldingRanges.push_back(t: range); |
| 714 | continue; |
| 715 | } |
| 716 | |
| 717 | // else: recurse! |
| 718 | appendFoldedRanges(newFoldedFoldingRanges, ranges: range->nestedRanges); |
| 719 | } |
| 720 | } |
| 721 | |
| 722 | QJsonDocument TextFolding::exportFoldingRanges() const |
| 723 | { |
| 724 | QJsonObject obj; |
| 725 | QJsonArray array; |
| 726 | exportFoldingRanges(ranges: m_foldingRanges, folds&: array); |
| 727 | obj.insert(QStringLiteral("ranges" ), value: array); |
| 728 | obj.insert(QStringLiteral("checksum" ), value: QString::fromLocal8Bit(ba: m_buffer.digest().toHex())); |
| 729 | QJsonDocument folds; |
| 730 | folds.setObject(obj); |
| 731 | return folds; |
| 732 | } |
| 733 | |
| 734 | void TextFolding::exportFoldingRanges(const TextFolding::FoldingRange::Vector &ranges, QJsonArray &folds) |
| 735 | { |
| 736 | // dump all ranges recursively |
| 737 | for (FoldingRange *range : ranges) { |
| 738 | // construct one range and dump to folds |
| 739 | QJsonObject rangeMap; |
| 740 | rangeMap[QStringLiteral("startLine" )] = range->start->line(); |
| 741 | rangeMap[QStringLiteral("startColumn" )] = range->start->column(); |
| 742 | rangeMap[QStringLiteral("endLine" )] = range->end->line(); |
| 743 | rangeMap[QStringLiteral("endColumn" )] = range->end->column(); |
| 744 | rangeMap[QStringLiteral("flags" )] = (int)range->flags; |
| 745 | folds.append(value: rangeMap); |
| 746 | |
| 747 | // recurse |
| 748 | exportFoldingRanges(ranges: range->nestedRanges, folds); |
| 749 | } |
| 750 | } |
| 751 | |
| 752 | void TextFolding::importFoldingRanges(const QJsonDocument &folds) |
| 753 | { |
| 754 | clearFoldingRanges(); |
| 755 | |
| 756 | const auto checksum = QByteArray::fromHex(hexEncoded: folds.object().value(QStringLiteral("checksum" )).toString().toLocal8Bit()); |
| 757 | if (checksum != m_buffer.digest()) { |
| 758 | return; |
| 759 | } |
| 760 | |
| 761 | // try to create all folding ranges |
| 762 | const auto jsonRanges = folds.object().value(QStringLiteral("ranges" )).toArray(); |
| 763 | for (const auto &rangeVariant : jsonRanges) { |
| 764 | // get map |
| 765 | const auto rangeMap = rangeVariant.toObject(); |
| 766 | |
| 767 | // construct range start/end |
| 768 | const KTextEditor::Cursor start(rangeMap[QStringLiteral("startLine" )].toInt(), rangeMap[QStringLiteral("startColumn" )].toInt()); |
| 769 | const KTextEditor::Cursor end(rangeMap[QStringLiteral("endLine" )].toInt(), rangeMap[QStringLiteral("endColumn" )].toInt()); |
| 770 | |
| 771 | // check validity (required when loading a possibly broken folding state from disk) |
| 772 | auto doc = m_buffer.document(); |
| 773 | if (start >= end || !doc->isValidTextPosition(cursor: start) || !doc->isValidTextPosition(cursor: end)) { |
| 774 | continue; |
| 775 | } |
| 776 | |
| 777 | // get flags |
| 778 | const int rawFlags = rangeMap[QStringLiteral("flags" )].toInt(); |
| 779 | FoldingRangeFlags flags; |
| 780 | if (rawFlags & Persistent) { |
| 781 | flags = Persistent; |
| 782 | } |
| 783 | if (rawFlags & Folded) { |
| 784 | flags = Folded; |
| 785 | } |
| 786 | |
| 787 | // create folding range |
| 788 | newFoldingRange(range: KTextEditor::Range(start, end), flags); |
| 789 | } |
| 790 | } |
| 791 | |
| 792 | } |
| 793 | |
| 794 | #include "moc_katetextfolding.cpp" |
| 795 | |