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
17KateUndoManager::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
46KateUndoManager::~KateUndoManager() = default;
47
48void KateUndoManager::viewCreated(KTextEditor::Document *, KTextEditor::View *newView) const
49{
50 connect(sender: newView, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateUndoManager::undoCancel);
51}
52
53void 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
75void 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
114void KateUndoManager::inputMethodStart()
115{
116 setActive(false);
117 m_document->editStart();
118}
119
120void KateUndoManager::inputMethodEnd()
121{
122 m_document->editEnd();
123 setActive(true);
124}
125
126void KateUndoManager::startUndo()
127{
128 setActive(false);
129 m_document->editStart();
130}
131
132void KateUndoManager::endUndo()
133{
134 m_document->editEnd();
135 setActive(true);
136}
137
138void 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
159void 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
180void 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
191void 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
225void 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
296void 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
308void 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
326void 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
336void 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
345void 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
355void 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
365uint KateUndoManager::undoCount() const
366{
367 return undoItems.size();
368}
369
370uint KateUndoManager::redoCount() const
371{
372 return redoItems.size();
373}
374
375void 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
391void 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
407void 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
486void KateUndoManager::clearUndo()
487{
488 undoItems.clear();
489
490 lastUndoGroupWhenSaved = nullptr;
491 docWasSavedWhenUndoWasEmpty = false;
492
493 Q_EMIT undoChanged();
494}
495
496void KateUndoManager::clearRedo()
497{
498 redoItems.clear();
499
500 lastRedoGroupWhenSaved = nullptr;
501 docWasSavedWhenRedoWasEmpty = false;
502
503 Q_EMIT undoChanged();
504}
505
506void 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
522void 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
545void 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
555KTextEditor::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
564void KateUndoManager::updateConfig()
565{
566 Q_EMIT undoChanged();
567}
568
569void KateUndoManager::setAllowComplexMerge(bool allow)
570{
571 m_undoComplexMerge = allow;
572}
573
574KTextEditor::ViewPrivate *KateUndoManager::activeView()
575{
576 return static_cast<KTextEditor::ViewPrivate *>(m_document->activeView());
577}
578
579#include "moc_kateundomanager.cpp"
580

source code of ktexteditor/src/undo/kateundomanager.cpp