1 | /* |
2 | SPDX-FileCopyrightText: 2009-2010 Bernhard Beschow <bbeschow@cs.tu-berlin.de> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #include "kateundomanager.h" |
8 | |
9 | #include <ktexteditor/view.h> |
10 | |
11 | #include "katedocument.h" |
12 | #include "katepartdebug.h" |
13 | #include "kateview.h" |
14 | |
15 | #include <QBitArray> |
16 | |
17 | KateUndoManager::KateUndoManager(KTextEditor::DocumentPrivate *doc) |
18 | : QObject(doc) |
19 | , m_document(doc) |
20 | { |
21 | connect(sender: this, signal: &KateUndoManager::undoEnd, context: this, slot: &KateUndoManager::undoChanged); |
22 | connect(sender: this, signal: &KateUndoManager::redoEnd, context: this, slot: &KateUndoManager::undoChanged); |
23 | |
24 | connect(sender: doc, signal: &KTextEditor::DocumentPrivate::viewCreated, context: this, slot: &KateUndoManager::viewCreated); |
25 | |
26 | // Before reload save history |
27 | connect(sender: doc, signal: &KTextEditor::DocumentPrivate::aboutToReload, context: this, slot: [this] { |
28 | savedUndoItems = std::move(undoItems); |
29 | savedRedoItems = std::move(redoItems); |
30 | docChecksumBeforeReload = m_document->checksum(); |
31 | }); |
32 | |
33 | // After reload restore it only if checksum of the doc is same |
34 | connect(sender: doc, signal: &KTextEditor::DocumentPrivate::loaded, context: this, slot: [this](KTextEditor::Document *doc) { |
35 | if (doc && !doc->checksum().isEmpty() && !docChecksumBeforeReload.isEmpty() && doc->checksum() == docChecksumBeforeReload) { |
36 | undoItems = std::move(savedUndoItems); |
37 | redoItems = std::move(savedRedoItems); |
38 | Q_EMIT undoChanged(); |
39 | } |
40 | docChecksumBeforeReload.clear(); |
41 | savedUndoItems.clear(); |
42 | savedRedoItems.clear(); |
43 | }); |
44 | } |
45 | |
46 | KateUndoManager::~KateUndoManager() = default; |
47 | |
48 | void KateUndoManager::viewCreated(KTextEditor::Document *, KTextEditor::View *newView) const |
49 | { |
50 | connect(sender: newView, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateUndoManager::undoCancel); |
51 | } |
52 | |
53 | void KateUndoManager::editStart() |
54 | { |
55 | if (!m_isActive) { |
56 | return; |
57 | } |
58 | |
59 | // editStart() and editEnd() must be called in alternating fashion |
60 | Q_ASSERT(!m_editCurrentUndo.has_value()); // make sure to enter a clean state |
61 | |
62 | const KTextEditor::Cursor cursorPosition = activeView() ? activeView()->cursorPosition() : KTextEditor::Cursor::invalid(); |
63 | const KTextEditor::Range primarySelectionRange = activeView() ? activeView()->selectionRange() : KTextEditor::Range::invalid(); |
64 | QList<KTextEditor::ViewPrivate::PlainSecondaryCursor> secondaryCursors; |
65 | if (activeView()) { |
66 | secondaryCursors = activeView()->plainSecondaryCursors(); |
67 | } |
68 | |
69 | // new current undo item |
70 | m_editCurrentUndo = KateUndoGroup(cursorPosition, primarySelectionRange, secondaryCursors); |
71 | |
72 | Q_ASSERT(m_editCurrentUndo.has_value()); // a new undo group must be created by this method |
73 | } |
74 | |
75 | void KateUndoManager::editEnd() |
76 | { |
77 | if (!m_isActive) { |
78 | return; |
79 | } |
80 | |
81 | // editStart() and editEnd() must be called in alternating fashion |
82 | Q_ASSERT(m_editCurrentUndo.has_value()); // an undo group must have been created by editStart() |
83 | |
84 | const KTextEditor::Cursor cursorPosition = activeView() ? activeView()->cursorPosition() : KTextEditor::Cursor::invalid(); |
85 | const KTextEditor::Range selectionRange = activeView() ? activeView()->selectionRange() : KTextEditor::Range::invalid(); |
86 | |
87 | QList<KTextEditor::ViewPrivate::PlainSecondaryCursor> secondaryCursors; |
88 | if (activeView()) { |
89 | secondaryCursors = activeView()->plainSecondaryCursors(); |
90 | } |
91 | |
92 | m_editCurrentUndo->editEnd(cursorPosition, selectionRange, secondaryCursors); |
93 | |
94 | bool changedUndo = false; |
95 | |
96 | if (m_editCurrentUndo->isEmpty()) { |
97 | m_editCurrentUndo.reset(); |
98 | } else if (!undoItems.empty() && undoItems.back().merge(newGroup: &*m_editCurrentUndo, complex: m_undoComplexMerge)) { |
99 | m_editCurrentUndo.reset(); |
100 | } else { |
101 | undoItems.push_back(x: std::move(*m_editCurrentUndo)); |
102 | changedUndo = true; |
103 | } |
104 | |
105 | m_editCurrentUndo.reset(); |
106 | |
107 | if (changedUndo) { |
108 | Q_EMIT undoChanged(); |
109 | } |
110 | |
111 | Q_ASSERT(!m_editCurrentUndo.has_value()); // must be 0 after calling this method |
112 | } |
113 | |
114 | void KateUndoManager::inputMethodStart() |
115 | { |
116 | setActive(false); |
117 | m_document->editStart(); |
118 | } |
119 | |
120 | void KateUndoManager::inputMethodEnd() |
121 | { |
122 | m_document->editEnd(); |
123 | setActive(true); |
124 | } |
125 | |
126 | void KateUndoManager::startUndo() |
127 | { |
128 | setActive(false); |
129 | m_document->editStart(); |
130 | } |
131 | |
132 | void KateUndoManager::endUndo() |
133 | { |
134 | m_document->editEnd(); |
135 | setActive(true); |
136 | } |
137 | |
138 | void KateUndoManager::slotTextInserted(int line, int col, const QString &s, const Kate::TextLine &tl) |
139 | { |
140 | if (!m_editCurrentUndo.has_value() || s.isEmpty()) { // do we care about notifications? |
141 | return; |
142 | } |
143 | |
144 | UndoItem item; |
145 | item.type = UndoItem::editInsertText; |
146 | item.line = line; |
147 | item.col = col; |
148 | item.text = s; |
149 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
150 | |
151 | if (tl.markedAsModified()) { |
152 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
153 | } else { |
154 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
155 | } |
156 | addUndoItem(undo: std::move(item)); |
157 | } |
158 | |
159 | void KateUndoManager::slotTextRemoved(int line, int col, const QString &s, const Kate::TextLine &tl) |
160 | { |
161 | if (!m_editCurrentUndo.has_value() || s.isEmpty()) { // do we care about notifications? |
162 | return; |
163 | } |
164 | |
165 | UndoItem item; |
166 | item.type = UndoItem::editRemoveText; |
167 | item.line = line; |
168 | item.col = col; |
169 | item.text = s; |
170 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
171 | |
172 | if (tl.markedAsModified()) { |
173 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
174 | } else { |
175 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
176 | } |
177 | addUndoItem(undo: std::move(item)); |
178 | } |
179 | |
180 | void KateUndoManager::slotMarkLineAutoWrapped(int line, bool autowrapped) |
181 | { |
182 | if (m_editCurrentUndo.has_value()) { // do we care about notifications? |
183 | UndoItem item; |
184 | item.type = UndoItem::editMarkLineAutoWrapped; |
185 | item.line = line; |
186 | item.autowrapped = autowrapped; |
187 | addUndoItem(undo: std::move(item)); |
188 | } |
189 | } |
190 | |
191 | void KateUndoManager::slotLineWrapped(int line, int col, int length, bool newLine, const Kate::TextLine &tl) |
192 | { |
193 | if (!m_editCurrentUndo.has_value()) { // do we care about notifications? |
194 | return; |
195 | } |
196 | |
197 | UndoItem item; |
198 | item.type = UndoItem::editWrapLine; |
199 | item.line = line; |
200 | item.col = col; |
201 | item.len = length; |
202 | item.newLine = newLine; |
203 | |
204 | if (length > 0 || tl.markedAsModified()) { |
205 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
206 | } else if (tl.markedAsSavedOnDisk()) { |
207 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Saved); |
208 | } |
209 | |
210 | if (col > 0 || length == 0 || tl.markedAsModified()) { |
211 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine2Modified); |
212 | } else if (tl.markedAsSavedOnDisk()) { |
213 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine2Saved); |
214 | } |
215 | |
216 | if (tl.markedAsModified()) { |
217 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
218 | } else if ((length > 0 && col > 0) || tl.markedAsSavedOnDisk()) { |
219 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
220 | } |
221 | |
222 | addUndoItem(undo: std::move(item)); |
223 | } |
224 | |
225 | void KateUndoManager::slotLineUnWrapped(int line, int col, int length, bool lineRemoved, const Kate::TextLine &tl, const Kate::TextLine &nextLine) |
226 | { |
227 | if (!m_editCurrentUndo.has_value()) { // do we care about notifications? |
228 | return; |
229 | } |
230 | |
231 | UndoItem item; |
232 | item.type = UndoItem::editUnWrapLine; |
233 | item.line = line; |
234 | item.col = col; |
235 | item.len = length; |
236 | item.removeLine = lineRemoved; |
237 | |
238 | const int len1 = tl.length(); |
239 | const int len2 = nextLine.length(); |
240 | |
241 | if (len1 > 0 && len2 > 0) { |
242 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
243 | |
244 | if (tl.markedAsModified()) { |
245 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
246 | } else { |
247 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
248 | } |
249 | |
250 | if (nextLine.markedAsModified()) { |
251 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Modified); |
252 | } else { |
253 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Saved); |
254 | } |
255 | } else if (len1 == 0) { |
256 | if (nextLine.markedAsModified()) { |
257 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
258 | } else if (nextLine.markedAsSavedOnDisk()) { |
259 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Saved); |
260 | } |
261 | |
262 | if (tl.markedAsModified()) { |
263 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
264 | } else { |
265 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
266 | } |
267 | |
268 | if (nextLine.markedAsModified()) { |
269 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Modified); |
270 | } else if (nextLine.markedAsSavedOnDisk()) { |
271 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Saved); |
272 | } |
273 | } else { // len2 == 0 |
274 | if (nextLine.markedAsModified()) { |
275 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
276 | } else if (nextLine.markedAsSavedOnDisk()) { |
277 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Saved); |
278 | } |
279 | |
280 | if (tl.markedAsModified()) { |
281 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
282 | } else if (tl.markedAsSavedOnDisk()) { |
283 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
284 | } |
285 | |
286 | if (nextLine.markedAsModified()) { |
287 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Modified); |
288 | } else { |
289 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Saved); |
290 | } |
291 | } |
292 | |
293 | addUndoItem(undo: std::move(item)); |
294 | } |
295 | |
296 | void KateUndoManager::slotLineInserted(int line, const QString &s) |
297 | { |
298 | if (m_editCurrentUndo.has_value()) { // do we care about notifications? |
299 | UndoItem item; |
300 | item.type = UndoItem::editInsertLine; |
301 | item.line = line; |
302 | item.text = s; |
303 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
304 | addUndoItem(undo: std::move(item)); |
305 | } |
306 | } |
307 | |
308 | void KateUndoManager::slotLineRemoved(int line, const QString &s, const Kate::TextLine &tl) |
309 | { |
310 | if (m_editCurrentUndo.has_value()) { // do we care about notifications? |
311 | UndoItem item; |
312 | item.type = UndoItem::editRemoveLine; |
313 | item.line = line; |
314 | item.text = s; |
315 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
316 | |
317 | if (tl.markedAsModified()) { |
318 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified); |
319 | } else { |
320 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved); |
321 | } |
322 | addUndoItem(undo: std::move(item)); |
323 | } |
324 | } |
325 | |
326 | void KateUndoManager::undoCancel() |
327 | { |
328 | // Don't worry about this when an edit is in progress |
329 | if (m_document->isEditRunning()) { |
330 | return; |
331 | } |
332 | |
333 | undoSafePoint(); |
334 | } |
335 | |
336 | void KateUndoManager::undoSafePoint() |
337 | { |
338 | if (!m_editCurrentUndo.has_value() && !undoItems.empty()) { |
339 | undoItems.back().safePoint(); |
340 | } else if (m_editCurrentUndo.has_value()) { |
341 | m_editCurrentUndo.value().safePoint(); |
342 | } |
343 | } |
344 | |
345 | void KateUndoManager::addUndoItem(UndoItem undo) |
346 | { |
347 | Q_ASSERT(m_editCurrentUndo.has_value()); // make sure there is an undo group for our item |
348 | |
349 | m_editCurrentUndo->addItem(u: std::move(undo)); |
350 | |
351 | // Clear redo buffer |
352 | redoItems.clear(); |
353 | } |
354 | |
355 | void KateUndoManager::setActive(bool enabled) |
356 | { |
357 | Q_ASSERT(!m_editCurrentUndo.has_value()); // must not already be in edit mode |
358 | Q_ASSERT(m_isActive != enabled); |
359 | |
360 | m_isActive = enabled; |
361 | |
362 | Q_EMIT isActiveChanged(enabled); |
363 | } |
364 | |
365 | uint KateUndoManager::undoCount() const |
366 | { |
367 | return undoItems.size(); |
368 | } |
369 | |
370 | uint KateUndoManager::redoCount() const |
371 | { |
372 | return redoItems.size(); |
373 | } |
374 | |
375 | void KateUndoManager::undo() |
376 | { |
377 | Q_ASSERT(!m_editCurrentUndo.has_value()); // undo is not supported while we care about notifications (call editEnd() first) |
378 | |
379 | if (!undoItems.empty()) { |
380 | Q_EMIT undoStart(document()); |
381 | |
382 | undoItems.back().undo(manager: this, view: activeView()); |
383 | redoItems.push_back(x: std::move(undoItems.back())); |
384 | undoItems.pop_back(); |
385 | updateModified(); |
386 | |
387 | Q_EMIT undoEnd(document()); |
388 | } |
389 | } |
390 | |
391 | void KateUndoManager::redo() |
392 | { |
393 | Q_ASSERT(!m_editCurrentUndo.has_value()); // redo is not supported while we care about notifications (call editEnd() first) |
394 | |
395 | if (!redoItems.empty()) { |
396 | Q_EMIT redoStart(document()); |
397 | |
398 | redoItems.back().redo(manager: this, view: activeView()); |
399 | undoItems.push_back(x: std::move(redoItems.back())); |
400 | redoItems.pop_back(); |
401 | updateModified(); |
402 | |
403 | Q_EMIT redoEnd(document()); |
404 | } |
405 | } |
406 | |
407 | void KateUndoManager::updateModified() |
408 | { |
409 | /* |
410 | How this works: |
411 | |
412 | After noticing that there where to many scenarios to take into |
413 | consideration when using 'if's to toggle the "Modified" flag |
414 | I came up with this baby, flexible and repetitive calls are |
415 | minimal. |
416 | |
417 | A numeric unique pattern is generated by toggling a set of bits, |
418 | each bit symbolizes a different state in the Undo Redo structure. |
419 | |
420 | undoItems.isEmpty() != null BIT 1 |
421 | redoItems.isEmpty() != null BIT 2 |
422 | docWasSavedWhenUndoWasEmpty == true BIT 3 |
423 | docWasSavedWhenRedoWasEmpty == true BIT 4 |
424 | lastUndoGroupWhenSavedIsLastUndo BIT 5 |
425 | lastUndoGroupWhenSavedIsLastRedo BIT 6 |
426 | lastRedoGroupWhenSavedIsLastUndo BIT 7 |
427 | lastRedoGroupWhenSavedIsLastRedo BIT 8 |
428 | |
429 | If you find a new pattern, please add it to the patterns array |
430 | */ |
431 | |
432 | unsigned char currentPattern = 0; |
433 | const unsigned char patterns[] = {5, 16, 21, 24, 26, 88, 90, 93, 133, 144, 149, 154, 165}; |
434 | const unsigned char patternCount = sizeof(patterns); |
435 | KateUndoGroup *undoLast = nullptr; |
436 | KateUndoGroup *redoLast = nullptr; |
437 | |
438 | if (undoItems.empty()) { |
439 | currentPattern |= 1; |
440 | } else { |
441 | undoLast = &undoItems.back(); |
442 | } |
443 | |
444 | if (redoItems.empty()) { |
445 | currentPattern |= 2; |
446 | } else { |
447 | redoLast = &redoItems.back(); |
448 | } |
449 | |
450 | if (docWasSavedWhenUndoWasEmpty) { |
451 | currentPattern |= 4; |
452 | } |
453 | if (docWasSavedWhenRedoWasEmpty) { |
454 | currentPattern |= 8; |
455 | } |
456 | if (lastUndoGroupWhenSaved == undoLast) { |
457 | currentPattern |= 16; |
458 | } |
459 | if (lastUndoGroupWhenSaved == redoLast) { |
460 | currentPattern |= 32; |
461 | } |
462 | if (lastRedoGroupWhenSaved == undoLast) { |
463 | currentPattern |= 64; |
464 | } |
465 | if (lastRedoGroupWhenSaved == redoLast) { |
466 | currentPattern |= 128; |
467 | } |
468 | |
469 | // This will print out the pattern information |
470 | |
471 | qCDebug(LOG_KTE) << "Pattern:" << static_cast<unsigned int>(currentPattern); |
472 | |
473 | for (uint patternIndex = 0; patternIndex < patternCount; ++patternIndex) { |
474 | if (currentPattern == patterns[patternIndex]) { |
475 | // Note: m_document->setModified() calls KateUndoManager::setModified! |
476 | m_document->setModified(false); |
477 | // (dominik) whenever the doc is not modified, succeeding edits |
478 | // should not be merged |
479 | undoSafePoint(); |
480 | qCDebug(LOG_KTE) << "setting modified to false!" ; |
481 | break; |
482 | } |
483 | } |
484 | } |
485 | |
486 | void KateUndoManager::clearUndo() |
487 | { |
488 | undoItems.clear(); |
489 | |
490 | lastUndoGroupWhenSaved = nullptr; |
491 | docWasSavedWhenUndoWasEmpty = false; |
492 | |
493 | Q_EMIT undoChanged(); |
494 | } |
495 | |
496 | void KateUndoManager::clearRedo() |
497 | { |
498 | redoItems.clear(); |
499 | |
500 | lastRedoGroupWhenSaved = nullptr; |
501 | docWasSavedWhenRedoWasEmpty = false; |
502 | |
503 | Q_EMIT undoChanged(); |
504 | } |
505 | |
506 | void KateUndoManager::setModified(bool modified) |
507 | { |
508 | if (!modified) { |
509 | if (!undoItems.empty()) { |
510 | lastUndoGroupWhenSaved = &undoItems.back(); |
511 | } |
512 | |
513 | if (!redoItems.empty()) { |
514 | lastRedoGroupWhenSaved = &redoItems.back(); |
515 | } |
516 | |
517 | docWasSavedWhenUndoWasEmpty = undoItems.empty(); |
518 | docWasSavedWhenRedoWasEmpty = redoItems.empty(); |
519 | } |
520 | } |
521 | |
522 | void KateUndoManager::updateLineModifications() |
523 | { |
524 | // change LineSaved flag of all undo & redo items to LineModified |
525 | for (KateUndoGroup &undoGroup : undoItems) { |
526 | undoGroup.flagSavedAsModified(); |
527 | } |
528 | |
529 | for (KateUndoGroup &undoGroup : redoItems) { |
530 | undoGroup.flagSavedAsModified(); |
531 | } |
532 | |
533 | // iterate all undo/redo items to find out, which item sets the flag LineSaved |
534 | QBitArray lines(document()->lines(), false); |
535 | for (int i = undoItems.size() - 1; i >= 0; --i) { |
536 | undoItems[i].markRedoAsSaved(lines); |
537 | } |
538 | |
539 | lines.fill(aval: false); |
540 | for (int i = redoItems.size() - 1; i >= 0; --i) { |
541 | redoItems[i].markUndoAsSaved(lines); |
542 | } |
543 | } |
544 | |
545 | void KateUndoManager::setUndoRedoCursorsOfLastGroup(const KTextEditor::Cursor undoCursor, const KTextEditor::Cursor redoCursor) |
546 | { |
547 | Q_ASSERT(!m_editCurrentUndo.has_value()); |
548 | if (!undoItems.empty()) { |
549 | KateUndoGroup &last = undoItems.back(); |
550 | last.setUndoCursor(undoCursor); |
551 | last.setRedoCursor(redoCursor); |
552 | } |
553 | } |
554 | |
555 | KTextEditor::Cursor KateUndoManager::lastRedoCursor() const |
556 | { |
557 | Q_ASSERT(!m_editCurrentUndo.has_value()); |
558 | if (!undoItems.empty()) { |
559 | undoItems.back().redoCursor(); |
560 | } |
561 | return KTextEditor::Cursor::invalid(); |
562 | } |
563 | |
564 | void KateUndoManager::updateConfig() |
565 | { |
566 | Q_EMIT undoChanged(); |
567 | } |
568 | |
569 | void KateUndoManager::setAllowComplexMerge(bool allow) |
570 | { |
571 | m_undoComplexMerge = allow; |
572 | } |
573 | |
574 | KTextEditor::ViewPrivate *KateUndoManager::activeView() |
575 | { |
576 | return static_cast<KTextEditor::ViewPrivate *>(m_document->activeView()); |
577 | } |
578 | |
579 | #include "moc_kateundomanager.cpp" |
580 | |