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 | |