1 | /* |
2 | SPDX-FileCopyrightText: 2010-2018 Dominik Haumann <dhaumann@kde.org> |
3 | SPDX-FileCopyrightText: 2010 Diana-Victoria Tiriplica <diana.tiriplica@gmail.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "config.h" |
9 | |
10 | #include "katebuffer.h" |
11 | #include "kateconfig.h" |
12 | #include "katedocument.h" |
13 | #include "katepartdebug.h" |
14 | #include "kateswapdiffcreator.h" |
15 | #include "kateswapfile.h" |
16 | #include "katetextbuffer.h" |
17 | #include "kateundomanager.h" |
18 | #include "ktexteditor/message.h" |
19 | #include <ktexteditor/view.h> |
20 | |
21 | #include <KLocalizedString> |
22 | #include <KStandardGuiItem> |
23 | |
24 | #include <QApplication> |
25 | #include <QCryptographicHash> |
26 | #include <QDir> |
27 | #include <QFileInfo> |
28 | |
29 | #ifndef Q_OS_WIN |
30 | #include <unistd.h> |
31 | #endif |
32 | |
33 | // swap file version header |
34 | const static char swapFileVersionString[] = "Kate Swap File 2.0" ; |
35 | |
36 | // tokens for swap files |
37 | const static qint8 EA_StartEditing = 'S'; |
38 | const static qint8 EA_FinishEditing = 'E'; |
39 | const static qint8 EA_WrapLine = 'W'; |
40 | const static qint8 EA_UnwrapLine = 'U'; |
41 | const static qint8 EA_InsertText = 'I'; |
42 | const static qint8 EA_RemoveText = 'R'; |
43 | |
44 | namespace Kate |
45 | { |
46 | QTimer *SwapFile::s_timer = nullptr; |
47 | |
48 | SwapFile::SwapFile(KTextEditor::DocumentPrivate *document) |
49 | : QObject(document) |
50 | , m_document(document) |
51 | , m_trackingEnabled(false) |
52 | , m_recovered(false) |
53 | , m_needSync(false) |
54 | { |
55 | // fixed version of serialisation |
56 | m_stream.setVersion(QDataStream::Qt_4_6); |
57 | |
58 | // connect the timer |
59 | connect(sender: syncTimer(), signal: &QTimer::timeout, context: this, slot: &Kate::SwapFile::writeFileToDisk, type: Qt::DirectConnection); |
60 | |
61 | // connecting the signals |
62 | connect(sender: &m_document->buffer(), signal: &KateBuffer::saved, context: this, slot: &Kate::SwapFile::fileSaved); |
63 | connect(sender: &m_document->buffer(), signal: &KateBuffer::loaded, context: this, slot: &Kate::SwapFile::fileLoaded); |
64 | connect(sender: m_document, signal: &KTextEditor::Document::configChanged, context: this, slot: &SwapFile::configChanged); |
65 | |
66 | // tracking on! |
67 | setTrackingEnabled(true); |
68 | } |
69 | |
70 | SwapFile::~SwapFile() |
71 | { |
72 | // only remove swap file after data recovery (bug #304576) |
73 | if (!shouldRecover()) { |
74 | removeSwapFile(); |
75 | } |
76 | } |
77 | |
78 | void SwapFile::configChanged() |
79 | { |
80 | } |
81 | |
82 | void SwapFile::setTrackingEnabled(bool enable) |
83 | { |
84 | if (m_trackingEnabled == enable) { |
85 | return; |
86 | } |
87 | |
88 | m_trackingEnabled = enable; |
89 | |
90 | if (m_trackingEnabled) { |
91 | connect(sender: m_document, signal: &KTextEditor::Document::editingStarted, context: this, slot: &Kate::SwapFile::startEditing); |
92 | connect(sender: m_document, signal: &KTextEditor::Document::editingFinished, context: this, slot: &Kate::SwapFile::finishEditing); |
93 | connect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, context: this, slot: &SwapFile::modifiedChanged); |
94 | |
95 | connect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, context: this, slot: &Kate::SwapFile::wrapLine); |
96 | connect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, context: this, slot: &Kate::SwapFile::unwrapLine); |
97 | connect(sender: m_document, signal: &KTextEditor::Document::textInserted, context: this, slot: &Kate::SwapFile::insertText); |
98 | connect(sender: m_document, signal: &KTextEditor::Document::textRemoved, context: this, slot: &Kate::SwapFile::removeText); |
99 | } else { |
100 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingStarted, receiver: this, slot: &Kate::SwapFile::startEditing); |
101 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingFinished, receiver: this, slot: &Kate::SwapFile::finishEditing); |
102 | disconnect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, receiver: this, slot: &SwapFile::modifiedChanged); |
103 | |
104 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, receiver: this, slot: &Kate::SwapFile::wrapLine); |
105 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, receiver: this, slot: &Kate::SwapFile::unwrapLine); |
106 | disconnect(sender: m_document, signal: &KTextEditor::Document::textInserted, receiver: this, slot: &Kate::SwapFile::insertText); |
107 | disconnect(sender: m_document, signal: &KTextEditor::Document::textRemoved, receiver: this, slot: &Kate::SwapFile::removeText); |
108 | } |
109 | } |
110 | |
111 | void SwapFile::fileClosed() |
112 | { |
113 | // remove old swap file, file is now closed |
114 | if (!shouldRecover()) { |
115 | removeSwapFile(); |
116 | } else { |
117 | m_document->setReadWrite(true); |
118 | } |
119 | |
120 | // purge filename |
121 | updateFileName(); |
122 | } |
123 | |
124 | KTextEditor::DocumentPrivate *SwapFile::document() |
125 | { |
126 | return m_document; |
127 | } |
128 | |
129 | bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const |
130 | { |
131 | // read and check header |
132 | QByteArray ; |
133 | stream >> header; |
134 | |
135 | if (header != swapFileVersionString) { |
136 | qCWarning(LOG_KTE) << "Can't open swap file, wrong version" ; |
137 | return false; |
138 | } |
139 | |
140 | // read checksum |
141 | QByteArray checksum; |
142 | stream >> checksum; |
143 | // qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum(); |
144 | if (checkDigest && checksum != m_document->checksum()) { |
145 | qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed" ; |
146 | return false; |
147 | } |
148 | |
149 | return true; |
150 | } |
151 | |
152 | void SwapFile::fileLoaded(const QString &) |
153 | { |
154 | // look for swap file |
155 | if (!updateFileName()) { |
156 | return; |
157 | } |
158 | |
159 | if (!m_swapfile.exists()) { |
160 | // qCDebug(LOG_KTE) << "No swap file"; |
161 | return; |
162 | } |
163 | |
164 | if (!QFileInfo(m_swapfile).isReadable()) { |
165 | qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)" ; |
166 | return; |
167 | } |
168 | |
169 | // sanity check |
170 | QFile peekFile(fileName()); |
171 | if (peekFile.open(flags: QIODevice::ReadOnly)) { |
172 | QDataStream stream(&peekFile); |
173 | if (!isValidSwapFile(stream, checkDigest: true)) { |
174 | removeSwapFile(); |
175 | return; |
176 | } |
177 | peekFile.close(); |
178 | } else { |
179 | qCWarning(LOG_KTE) << "Can't open swap file:" << fileName(); |
180 | return; |
181 | } |
182 | |
183 | // show swap file message |
184 | m_document->setReadWrite(false); |
185 | showSwapFileMessage(); |
186 | } |
187 | |
188 | void SwapFile::modifiedChanged() |
189 | { |
190 | if (!m_document->isModified() && !shouldRecover()) { |
191 | m_needSync = false; |
192 | // the file is not modified and we are not in recover mode |
193 | removeSwapFile(); |
194 | } |
195 | } |
196 | |
197 | void SwapFile::recover() |
198 | { |
199 | m_document->setReadWrite(true); |
200 | |
201 | // if isOpen() returns true, the swap file likely changed already (appended data) |
202 | // Example: The document was falsely marked as writable and the user changed |
203 | // text even though the recover bar was visible. In this case, a replay of |
204 | // the swap file across wrong document content would happen -> certainly wrong |
205 | if (m_swapfile.isOpen()) { |
206 | qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting" ; |
207 | removeSwapFile(); |
208 | return; |
209 | } |
210 | |
211 | // if the file doesn't exist, abort (user might have deleted it, or use two editor instances) |
212 | if (!m_swapfile.open(flags: QIODevice::ReadOnly)) { |
213 | qCWarning(LOG_KTE) << "Can't open swap file" ; |
214 | return; |
215 | } |
216 | |
217 | // remember that the file has recovered |
218 | m_recovered = true; |
219 | |
220 | // open data stream |
221 | m_stream.setDevice(&m_swapfile); |
222 | |
223 | // replay the swap file |
224 | bool success = recover(m_stream); |
225 | |
226 | // close swap file |
227 | m_stream.setDevice(nullptr); |
228 | m_swapfile.close(); |
229 | |
230 | if (!success) { |
231 | removeSwapFile(); |
232 | } |
233 | |
234 | // recover can also be called through the KTE::RecoveryInterface. |
235 | // Make sure, the message is hidden in this case as well. |
236 | if (m_swapMessage) { |
237 | m_swapMessage->deleteLater(); |
238 | } |
239 | } |
240 | |
241 | bool SwapFile::recover(QDataStream &stream, bool checkDigest) |
242 | { |
243 | if (!isValidSwapFile(stream, checkDigest)) { |
244 | return false; |
245 | } |
246 | |
247 | // disconnect current signals |
248 | setTrackingEnabled(false); |
249 | |
250 | // needed to set undo/redo cursors in a sane way |
251 | bool firstEditInGroup = false; |
252 | KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid(); |
253 | KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid(); |
254 | |
255 | // replay swapfile |
256 | bool editRunning = false; |
257 | bool brokenSwapFile = false; |
258 | while (!stream.atEnd()) { |
259 | if (brokenSwapFile) { |
260 | break; |
261 | } |
262 | |
263 | qint8 type; |
264 | stream >> type; |
265 | switch (type) { |
266 | case EA_StartEditing: { |
267 | m_document->editStart(); |
268 | editRunning = true; |
269 | firstEditInGroup = true; |
270 | undoCursor = KTextEditor::Cursor::invalid(); |
271 | redoCursor = KTextEditor::Cursor::invalid(); |
272 | break; |
273 | } |
274 | case EA_FinishEditing: { |
275 | m_document->editEnd(); |
276 | |
277 | // empty editStart() / editEnd() groups exist: only set cursor if required |
278 | if (!firstEditInGroup) { |
279 | // set undo/redo cursor of last KateUndoGroup of the undo manager |
280 | m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor); |
281 | m_document->undoManager()->undoSafePoint(); |
282 | } |
283 | firstEditInGroup = false; |
284 | editRunning = false; |
285 | break; |
286 | } |
287 | case EA_WrapLine: { |
288 | if (!editRunning) { |
289 | brokenSwapFile = true; |
290 | break; |
291 | } |
292 | |
293 | int line = 0; |
294 | int column = 0; |
295 | stream >> line >> column; |
296 | |
297 | // emulate buffer unwrapLine with document |
298 | m_document->editWrapLine(line, col: column, newLine: true); |
299 | |
300 | // track undo/redo cursor |
301 | if (firstEditInGroup) { |
302 | firstEditInGroup = false; |
303 | undoCursor = KTextEditor::Cursor(line, column); |
304 | } |
305 | redoCursor = KTextEditor::Cursor(line + 1, 0); |
306 | |
307 | break; |
308 | } |
309 | case EA_UnwrapLine: { |
310 | if (!editRunning) { |
311 | brokenSwapFile = true; |
312 | break; |
313 | } |
314 | |
315 | int line = 0; |
316 | stream >> line; |
317 | |
318 | // assert valid line |
319 | Q_ASSERT(line > 0); |
320 | |
321 | const int undoColumn = m_document->lineLength(line: line - 1); |
322 | |
323 | // emulate buffer unwrapLine with document |
324 | m_document->editUnWrapLine(line: line - 1, removeLine: true, length: 0); |
325 | |
326 | // track undo/redo cursor |
327 | if (firstEditInGroup) { |
328 | firstEditInGroup = false; |
329 | undoCursor = KTextEditor::Cursor(line, 0); |
330 | } |
331 | redoCursor = KTextEditor::Cursor(line - 1, undoColumn); |
332 | |
333 | break; |
334 | } |
335 | case EA_InsertText: { |
336 | if (!editRunning) { |
337 | brokenSwapFile = true; |
338 | break; |
339 | } |
340 | |
341 | int line; |
342 | int column; |
343 | QByteArray text; |
344 | stream >> line >> column >> text; |
345 | m_document->insertText(position: KTextEditor::Cursor(line, column), s: QString::fromUtf8(utf8: text.data(), size: text.size())); |
346 | |
347 | // track undo/redo cursor |
348 | if (firstEditInGroup) { |
349 | firstEditInGroup = false; |
350 | undoCursor = KTextEditor::Cursor(line, column); |
351 | } |
352 | redoCursor = KTextEditor::Cursor(line, column + text.size()); |
353 | |
354 | break; |
355 | } |
356 | case EA_RemoveText: { |
357 | if (!editRunning) { |
358 | brokenSwapFile = true; |
359 | break; |
360 | } |
361 | |
362 | int line; |
363 | int startColumn; |
364 | int endColumn; |
365 | stream >> line >> startColumn >> endColumn; |
366 | m_document->removeText(range: KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); |
367 | |
368 | // track undo/redo cursor |
369 | if (firstEditInGroup) { |
370 | firstEditInGroup = false; |
371 | undoCursor = KTextEditor::Cursor(line, endColumn); |
372 | } |
373 | redoCursor = KTextEditor::Cursor(line, startColumn); |
374 | |
375 | break; |
376 | } |
377 | default: { |
378 | qCWarning(LOG_KTE) << "Unknown type:" << type; |
379 | } |
380 | } |
381 | } |
382 | |
383 | // balanced editStart and editEnd? |
384 | if (editRunning) { |
385 | brokenSwapFile = true; |
386 | m_document->editEnd(); |
387 | } |
388 | |
389 | // warn the user if the swap file is not complete |
390 | if (brokenSwapFile) { |
391 | qCWarning(LOG_KTE) << "Some data might be lost" ; |
392 | } else { |
393 | // set sane final cursor, if possible |
394 | KTextEditor::View *view = m_document->activeView(); |
395 | redoCursor = m_document->undoManager()->lastRedoCursor(); |
396 | if (view && redoCursor.isValid()) { |
397 | view->setCursorPosition(redoCursor); |
398 | } |
399 | } |
400 | |
401 | // reconnect the signals |
402 | setTrackingEnabled(true); |
403 | |
404 | return true; |
405 | } |
406 | |
407 | void SwapFile::fileSaved(const QString &) |
408 | { |
409 | m_needSync = false; |
410 | |
411 | // remove old swap file (e.g. if a file A was "saved as" B) |
412 | removeSwapFile(); |
413 | |
414 | // set the name for the new swap file |
415 | updateFileName(); |
416 | } |
417 | |
418 | void SwapFile::startEditing() |
419 | { |
420 | // no swap file, no work |
421 | if (m_swapfile.fileName().isEmpty()) { |
422 | return; |
423 | } |
424 | |
425 | // if swap file doesn't exists, open it in WriteOnly mode |
426 | // if it does, append the data to the existing swap file, |
427 | // in case you recover and start editing again |
428 | if (!m_swapfile.exists()) { |
429 | // create path if not there |
430 | if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory |
431 | && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) { |
432 | QDir().mkpath(dirPath: KateDocumentConfig::global()->swapDirectory()); |
433 | } |
434 | |
435 | m_swapfile.open(flags: QIODevice::WriteOnly); |
436 | m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); |
437 | m_stream.setDevice(&m_swapfile); |
438 | |
439 | // write file header |
440 | m_stream << QByteArray(swapFileVersionString); |
441 | |
442 | // write checksum |
443 | m_stream << m_document->checksum(); |
444 | } else if (m_stream.device() == nullptr) { |
445 | m_swapfile.open(flags: QIODevice::Append); |
446 | m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); |
447 | m_stream.setDevice(&m_swapfile); |
448 | } |
449 | |
450 | // format: qint8 |
451 | m_stream << EA_StartEditing; |
452 | } |
453 | |
454 | void SwapFile::finishEditing() |
455 | { |
456 | // skip if not open |
457 | if (!m_swapfile.isOpen()) { |
458 | return; |
459 | } |
460 | |
461 | // write the file to the disk every 15 seconds (default) |
462 | // skip this if we disabled that |
463 | if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) { |
464 | // important: we store the interval as seconds, start wants milliseconds! |
465 | syncTimer()->start(msec: m_document->config()->swapSyncInterval() * 1000); |
466 | } |
467 | |
468 | // format: qint8 |
469 | m_stream << EA_FinishEditing; |
470 | m_swapfile.flush(); |
471 | } |
472 | |
473 | void SwapFile::wrapLine(KTextEditor::Document *, const KTextEditor::Cursor position) |
474 | { |
475 | // skip if not open |
476 | if (!m_swapfile.isOpen()) { |
477 | return; |
478 | } |
479 | |
480 | // format: qint8, int, int |
481 | m_stream << EA_WrapLine << position.line() << position.column(); |
482 | |
483 | m_needSync = true; |
484 | } |
485 | |
486 | void SwapFile::unwrapLine(KTextEditor::Document *, int line) |
487 | { |
488 | // skip if not open |
489 | if (!m_swapfile.isOpen()) { |
490 | return; |
491 | } |
492 | |
493 | // format: qint8, int |
494 | m_stream << EA_UnwrapLine << line; |
495 | |
496 | m_needSync = true; |
497 | } |
498 | |
499 | void SwapFile::insertText(KTextEditor::Document *, const KTextEditor::Cursor position, const QString &text) |
500 | { |
501 | // skip if not open |
502 | if (!m_swapfile.isOpen()) { |
503 | return; |
504 | } |
505 | |
506 | // format: qint8, int, int, bytearray |
507 | m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8(); |
508 | |
509 | m_needSync = true; |
510 | } |
511 | |
512 | void SwapFile::removeText(KTextEditor::Document *, KTextEditor::Range range, const QString &) |
513 | { |
514 | // skip if not open |
515 | if (!m_swapfile.isOpen()) { |
516 | return; |
517 | } |
518 | |
519 | // format: qint8, int, int, int |
520 | Q_ASSERT(range.start().line() == range.end().line()); |
521 | m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column(); |
522 | |
523 | m_needSync = true; |
524 | } |
525 | |
526 | bool SwapFile::shouldRecover() const |
527 | { |
528 | // should not recover if the file has already recovered in another view |
529 | if (m_recovered) { |
530 | return false; |
531 | } |
532 | |
533 | return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr; |
534 | } |
535 | |
536 | void SwapFile::discard() |
537 | { |
538 | m_document->setReadWrite(true); |
539 | removeSwapFile(); |
540 | |
541 | // discard can also be called through the KTE::RecoveryInterface. |
542 | // Make sure, the message is hidden in this case as well. |
543 | if (m_swapMessage) { |
544 | m_swapMessage->deleteLater(); |
545 | } |
546 | } |
547 | |
548 | void SwapFile::removeSwapFile() |
549 | { |
550 | if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) { |
551 | m_stream.setDevice(nullptr); |
552 | m_swapfile.close(); |
553 | m_swapfile.remove(); |
554 | } |
555 | } |
556 | |
557 | bool SwapFile::updateFileName() |
558 | { |
559 | // first clear filename |
560 | m_swapfile.setFileName(QString()); |
561 | |
562 | // get the new path |
563 | QString path = fileName(); |
564 | if (path.isNull()) { |
565 | return false; |
566 | } |
567 | |
568 | m_swapfile.setFileName(path); |
569 | return true; |
570 | } |
571 | |
572 | QString SwapFile::fileName() |
573 | { |
574 | const QUrl &url = m_document->url(); |
575 | if (url.isEmpty() || !url.isLocalFile()) { |
576 | return QString(); |
577 | } |
578 | |
579 | const QString fullLocalPath(url.toLocalFile()); |
580 | QString path; |
581 | if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) { |
582 | path = KateDocumentConfig::global()->swapDirectory(); |
583 | path.append(c: QLatin1Char('/')); |
584 | |
585 | // append the sha1 sum of the full path + filename, to avoid "too long" paths created |
586 | path.append(s: QString::fromLatin1(ba: QCryptographicHash::hash(data: fullLocalPath.toUtf8(), method: QCryptographicHash::Sha1).toHex())); |
587 | path.append(c: QLatin1Char('-')); |
588 | path.append(s: QFileInfo(fullLocalPath).fileName()); |
589 | |
590 | path.append(s: QLatin1String(".kate-swp" )); |
591 | } else { |
592 | path = fullLocalPath; |
593 | int poz = path.lastIndexOf(c: QLatin1Char('/')); |
594 | path.insert(i: poz + 1, c: QLatin1Char('.')); |
595 | path.append(s: QLatin1String(".kate-swp" )); |
596 | } |
597 | |
598 | return path; |
599 | } |
600 | |
601 | QTimer *SwapFile::syncTimer() |
602 | { |
603 | if (s_timer == nullptr) { |
604 | s_timer = new QTimer(QApplication::instance()); |
605 | s_timer->setSingleShot(true); |
606 | } |
607 | |
608 | return s_timer; |
609 | } |
610 | |
611 | void SwapFile::writeFileToDisk() |
612 | { |
613 | if (m_needSync) { |
614 | m_needSync = false; |
615 | |
616 | #ifndef Q_OS_WIN |
617 | // ensure that the file is written to disk |
618 | #if HAVE_FDATASYNC |
619 | fdatasync(fildes: m_swapfile.handle()); |
620 | #else |
621 | fsync(m_swapfile.handle()); |
622 | #endif |
623 | #endif |
624 | } |
625 | } |
626 | |
627 | void SwapFile::showSwapFileMessage() |
628 | { |
629 | m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly." ), KTextEditor::Message::Warning); |
630 | m_swapMessage->setWordWrap(true); |
631 | |
632 | QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split" )), i18n("View Changes" ), nullptr); |
633 | QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo" )), i18n("Recover Data" ), nullptr); |
634 | QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard" ), nullptr); |
635 | |
636 | m_swapMessage->addAction(action: diffAction, closeOnTrigger: false); |
637 | m_swapMessage->addAction(action: recoverAction); |
638 | m_swapMessage->addAction(action: discardAction); |
639 | |
640 | connect(sender: diffAction, signal: &QAction::triggered, context: this, slot: &SwapFile::showDiff); |
641 | connect(sender: recoverAction, signal: &QAction::triggered, context: this, slot: qOverload<>(&Kate::SwapFile::recover), type: Qt::QueuedConnection); |
642 | connect(sender: discardAction, signal: &QAction::triggered, context: this, slot: &SwapFile::discard, type: Qt::QueuedConnection); |
643 | |
644 | m_document->postMessage(message: m_swapMessage); |
645 | } |
646 | |
647 | void SwapFile::showDiff() |
648 | { |
649 | // the diff creator deletes itself through deleteLater() when it's done |
650 | SwapDiffCreator *diffCreator = new SwapDiffCreator(this); |
651 | diffCreator->viewDiff(); |
652 | } |
653 | |
654 | } |
655 | |