File indexing completed on 2024-04-28 05:49:29

0001 /* This file is part of the KDE project
0002    SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
0003    SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
0004    SPDX-FileCopyrightText: 2007 Flavio Castelli <flavio.castelli@gmail.com>
0005 
0006    SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #include "katedocmanager.h"
0010 
0011 #include "kateapp.h"
0012 #include "katemainwindow.h"
0013 #include "katesavemodifieddialog.h"
0014 #include "kateviewmanager.h"
0015 
0016 #include <kcoreaddons_version.h>
0017 #include <ktexteditor/editor.h>
0018 #include <ktexteditor/view.h>
0019 
0020 #include <KConfigGroup>
0021 #include <KLocalizedString>
0022 #include <KMessageBox>
0023 #include <KNetworkMounts>
0024 #include <KSharedConfig>
0025 #include <kwidgetsaddons_version.h>
0026 
0027 #include <QFileDialog>
0028 #include <QProgressDialog>
0029 
0030 KateDocManager::KateDocManager(QObject *parent)
0031     : QObject(parent)
0032     , m_metaInfos(KateApp::isKate() ? QStringLiteral("katemetainfos") : QStringLiteral("kwritemetainfos"), KConfig::NoGlobals)
0033     , m_saveMetaInfos(true)
0034     , m_daysMetaInfos(0)
0035 {
0036     // set our application wrapper
0037     KTextEditor::Editor::instance()->setApplication(KateApp::self()->wrapper());
0038 }
0039 
0040 KateDocManager::~KateDocManager()
0041 {
0042     // write metainfos?
0043     if (m_saveMetaInfos) {
0044         // saving meta-infos when file is saved is not enough, we need to do it once more at the end
0045         saveMetaInfos(m_docList);
0046 
0047         // purge saved filesessions
0048         if (m_daysMetaInfos > 0) {
0049             const QStringList groups = m_metaInfos.groupList();
0050             QDateTime def(QDate(1970, 1, 1).startOfDay());
0051 
0052             for (const auto &group : groups) {
0053                 QDateTime last = m_metaInfos.group(group).readEntry("Time", def);
0054                 if (last.daysTo(QDateTime::currentDateTimeUtc()) > m_daysMetaInfos) {
0055                     m_metaInfos.deleteGroup(group);
0056                 }
0057             }
0058         }
0059     }
0060 }
0061 
0062 static QUrl normalizeUrl(const QUrl &url)
0063 {
0064     // Resolve symbolic links for local files
0065     if (url.isLocalFile() && !KNetworkMounts::self()->isOptionEnabledForPath(url.toLocalFile(), KNetworkMounts::StrongSideEffectsOptimizations)) {
0066         QString normalizedUrl = QFileInfo(url.toLocalFile()).canonicalFilePath();
0067         if (!normalizedUrl.isEmpty()) {
0068             return QUrl::fromLocalFile(normalizedUrl);
0069         }
0070     }
0071 
0072     // else: cleanup only the .. stuff
0073     return url.adjusted(QUrl::NormalizePathSegments);
0074 }
0075 
0076 static QUrl absoluteUrl(const QUrl &url)
0077 {
0078     // Get absolute path if local file
0079     if (url.isLocalFile()) {
0080         return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).absoluteFilePath());
0081     }
0082 
0083     // else: cleanup only the .. stuff
0084     return url.adjusted(QUrl::NormalizePathSegments);
0085 }
0086 
0087 void KateDocManager::slotUrlChanged(const QUrl &newUrl)
0088 {
0089     KTextEditor::Document *doc = qobject_cast<KTextEditor::Document *>(sender());
0090     if (doc) {
0091         m_docInfos.at(doc).normalizedUrl = normalizeUrl(newUrl);
0092     }
0093 }
0094 
0095 KTextEditor::Document *KateDocManager::createDoc(const KateDocumentInfo &docInfo)
0096 {
0097     KTextEditor::Document *doc = KTextEditor::Editor::instance()->createDocument(this);
0098 
0099     // turn off the editorpart's own modification dialog, we have our own one, too!
0100     const KConfigGroup generalGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
0101     bool ownModNotification = generalGroup.readEntry("Modified Notification", false);
0102     doc->setModifiedOnDiskWarning(!ownModNotification);
0103 
0104     m_docList.push_back(doc);
0105     m_docInfos.emplace(doc, docInfo);
0106 
0107     // connect internal signals...
0108     connect(doc, &KTextEditor::Document::modifiedChanged, this, &KateDocManager::slotModChanged1);
0109     connect(doc, &KTextEditor::Document::modifiedOnDisk, this, &KateDocManager::slotModifiedOnDisc);
0110     connect(doc, &KTextEditor::Document::documentUrlChanged, this, [this](KTextEditor::Document *doc) {
0111         slotUrlChanged(doc->url());
0112     });
0113     connect(doc, &KParts::ReadOnlyPart::urlChanged, this, &KateDocManager::slotUrlChanged);
0114 
0115     // we have a new document, show it the world
0116     Q_EMIT documentCreated(doc);
0117 
0118     // return our new document
0119     return doc;
0120 }
0121 
0122 KateDocumentInfo *KateDocManager::documentInfo(KTextEditor::Document *doc)
0123 {
0124     auto it = m_docInfos.find(doc);
0125     if (it != m_docInfos.end()) {
0126         return &it->second;
0127     }
0128     return nullptr;
0129 }
0130 
0131 KTextEditor::Document *KateDocManager::findDocument(const QUrl &url) const
0132 {
0133     auto it = std::find_if(m_docInfos.begin(), m_docInfos.end(), [u = normalizeUrl(url)](const auto &p) {
0134         return p.second.normalizedUrl == u;
0135     });
0136 
0137     return it == m_docInfos.end() ? nullptr : it->first;
0138 }
0139 
0140 std::vector<KTextEditor::Document *> KateDocManager::openUrls(const QList<QUrl> &urls, const QString &encoding, const KateDocumentInfo &docInfo)
0141 {
0142     std::vector<KTextEditor::Document *> docs;
0143     docs.reserve(urls.size());
0144     for (const QUrl &url : urls) {
0145         docs.push_back(openUrl(url, encoding, docInfo));
0146     }
0147     return docs;
0148 }
0149 
0150 KTextEditor::Document *KateDocManager::openUrl(const QUrl &url, const QString &encoding, const KateDocumentInfo &docInfo)
0151 {
0152     // We want to work on absolute urls
0153     const QUrl u(absoluteUrl(url));
0154 
0155     // try to find already open document
0156     if (!u.isEmpty()) {
0157         if (auto doc = findDocument(u)) {
0158             return doc;
0159         }
0160     }
0161 
0162     // else: create new document
0163     auto doc = createDoc(docInfo);
0164     if (!encoding.isEmpty()) {
0165         doc->setEncoding(encoding);
0166     }
0167     if (!u.isEmpty()) {
0168         doc->openUrl(u);
0169         loadMetaInfos(doc, u);
0170     }
0171     return doc;
0172 }
0173 
0174 bool KateDocManager::closeDocuments(const QList<KTextEditor::Document *> documents, bool closeUrl)
0175 {
0176     if (documents.empty()) {
0177         return false;
0178     }
0179 
0180     saveMetaInfos(documents);
0181 
0182     Q_EMIT aboutToDeleteDocuments(QList<KTextEditor::Document *>{documents.begin(), documents.end()});
0183 
0184     int last = 0;
0185     bool success = true;
0186     for (KTextEditor::Document *doc : documents) {
0187         if (closeUrl && !doc->closeUrl()) {
0188             success = false; // get out on first error
0189             break;
0190         }
0191 
0192         // document will be deleted, soon
0193         Q_EMIT documentWillBeDeleted(doc);
0194 
0195         // really delete the document and its infos
0196         disconnect(doc, &KParts::ReadOnlyPart::urlChanged, this, &KateDocManager::slotUrlChanged);
0197         m_docInfos.erase(doc);
0198         delete m_docList.takeAt(m_docList.indexOf(doc));
0199 
0200         // document is gone, emit our signals
0201         Q_EMIT documentDeleted(doc);
0202 
0203         last++;
0204     }
0205 
0206     Q_EMIT documentsDeleted(QList<KTextEditor::Document *>{documents.begin() + last, documents.end()});
0207 
0208     return success;
0209 }
0210 
0211 bool KateDocManager::closeDocument(KTextEditor::Document *doc, bool closeUrl)
0212 {
0213     if (!doc) {
0214         return false;
0215     }
0216 
0217     return closeDocuments({doc}, closeUrl);
0218 }
0219 
0220 bool KateDocManager::closeDocumentList(const QList<KTextEditor::Document *> &documents, KateMainWindow *window)
0221 {
0222     std::vector<KTextEditor::Document *> modifiedDocuments;
0223     for (KTextEditor::Document *document : documents) {
0224         if (document->isModified()) {
0225             modifiedDocuments.push_back(document);
0226         }
0227     }
0228 
0229     if (!modifiedDocuments.empty() && !KateSaveModifiedDialog::queryClose(window, modifiedDocuments)) {
0230         return false;
0231     }
0232 
0233     return closeDocuments(documents, false); // Do not show save/discard dialog
0234 }
0235 
0236 bool KateDocManager::closeAllDocuments(bool closeUrl)
0237 {
0238     /**
0239      * just close all documents
0240      */
0241     return closeDocuments(m_docList, closeUrl);
0242 }
0243 
0244 bool KateDocManager::closeOtherDocuments(KTextEditor::Document *doc)
0245 {
0246     /**
0247      * close all documents beside the passed one
0248      */
0249     QList<KTextEditor::Document *> documents;
0250     documents.reserve(m_docList.size() - 1);
0251     for (auto document : qAsConst(m_docList)) {
0252         if (document != doc) {
0253             documents.push_back(document);
0254         }
0255     }
0256 
0257     return closeDocuments(documents);
0258 }
0259 
0260 /**
0261  * Find all modified documents.
0262  * @return Return the list of all modified documents.
0263  */
0264 std::vector<KTextEditor::Document *> KateDocManager::modifiedDocumentList()
0265 {
0266     std::vector<KTextEditor::Document *> modified;
0267     std::copy_if(m_docList.begin(), m_docList.end(), std::back_inserter(modified), [](KTextEditor::Document *doc) {
0268         return doc->isModified();
0269     });
0270     return modified;
0271 }
0272 
0273 bool KateDocManager::queryCloseDocuments(KateMainWindow *w)
0274 {
0275     const auto docCount = m_docList.size();
0276     for (KTextEditor::Document *doc : qAsConst(m_docList)) {
0277         if (doc->url().isEmpty() && doc->isModified()) {
0278             int msgres = KMessageBox::warningTwoActionsCancel(w,
0279                                                               i18n("<p>The document '%1' has been modified, but not saved.</p>"
0280                                                                    "<p>Do you want to save your changes or discard them?</p>",
0281                                                                    doc->documentName()),
0282                                                               i18n("Close Document"),
0283                                                               KStandardGuiItem::save(),
0284                                                               KStandardGuiItem::discard());
0285 
0286             if (msgres == KMessageBox::Cancel) {
0287                 return false;
0288             }
0289 
0290             if (msgres == KMessageBox::PrimaryAction) {
0291                 const QUrl url = QFileDialog::getSaveFileUrl(w, i18n("Save As"));
0292                 if (!url.isEmpty()) {
0293                     if (!doc->saveAs(url)) {
0294                         return false;
0295                     }
0296                 } else {
0297                     return false;
0298                 }
0299             }
0300         } else {
0301             if (!doc->queryClose()) {
0302                 return false;
0303             }
0304         }
0305     }
0306 
0307     // document count changed while queryClose, abort and notify user
0308     if (m_docList.size() > docCount) {
0309         KMessageBox::information(w, i18n("New file opened while trying to close Kate, closing aborted."), i18n("Closing Aborted"));
0310         return false;
0311     }
0312 
0313     return true;
0314 }
0315 
0316 void KateDocManager::saveAll()
0317 {
0318     for (KTextEditor::Document *doc : qAsConst(m_docList)) {
0319         if (doc->isModified()) {
0320             doc->documentSave();
0321         }
0322     }
0323 }
0324 
0325 void KateDocManager::saveSelected(const QList<KTextEditor::Document *> &docList)
0326 {
0327     for (KTextEditor::Document *doc : docList) {
0328         if (doc->isModified()) {
0329             doc->documentSave();
0330         }
0331     }
0332 }
0333 
0334 void KateDocManager::reloadAll()
0335 {
0336     // reload all docs that are NOT modified on disk
0337     for (KTextEditor::Document *doc : qAsConst(m_docList)) {
0338         if (!documentInfo(doc)->modifiedOnDisc) {
0339             doc->documentReload();
0340         }
0341     }
0342 
0343     // take care of all documents that ARE modified on disk
0344     KateApp::self()->activeKateMainWindow()->showModOnDiskPrompt(KateMainWindow::PromptAll);
0345 }
0346 
0347 void KateDocManager::closeOrphaned()
0348 {
0349     QList<KTextEditor::Document *> documents;
0350 
0351     for (KTextEditor::Document *doc : qAsConst(m_docList)) {
0352         KateDocumentInfo *info = documentInfo(doc);
0353         if (info && !info->openSuccess) {
0354             documents.push_back(doc);
0355         }
0356     }
0357 
0358     closeDocuments(documents);
0359 }
0360 
0361 void KateDocManager::saveDocumentList(KConfig *config)
0362 {
0363     KConfigGroup openDocGroup(config, QStringLiteral("Open Documents"));
0364 
0365     openDocGroup.writeEntry("Count", (int)m_docList.size());
0366 
0367     int i = 0;
0368     for (KTextEditor::Document *doc : qAsConst(m_docList)) {
0369         const QString entryName = QStringLiteral("Document %1").arg(i);
0370         KConfigGroup cg(config, entryName);
0371         doc->writeSessionConfig(cg);
0372 
0373         i++;
0374     }
0375 }
0376 
0377 void KateDocManager::restoreDocumentList(KConfig *config)
0378 {
0379     KConfigGroup openDocGroup(config, QStringLiteral("Open Documents"));
0380     unsigned int count = openDocGroup.readEntry("Count", 0);
0381 
0382     if (count == 0) {
0383         return;
0384     }
0385 
0386     QProgressDialog progress;
0387     progress.setWindowTitle(i18n("Starting Up"));
0388     progress.setLabelText(i18n("Reopening files from the last session..."));
0389     progress.setModal(true);
0390     progress.setCancelButton(nullptr);
0391     progress.setRange(0, count);
0392 
0393     for (unsigned int i = 0; i < count; i++) {
0394         KConfigGroup cg(config, QStringLiteral("Document %1").arg(i));
0395         KTextEditor::Document *doc = createDoc();
0396 
0397         connect(doc, SIGNAL(completed()), this, SLOT(documentOpened()));
0398         connect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened);
0399 
0400         doc->readSessionConfig(cg);
0401 
0402         KateApp::self()->stashManager()->popDocument(doc, cg);
0403 
0404         progress.setValue(i);
0405     }
0406 }
0407 
0408 void KateDocManager::slotModifiedOnDisc(KTextEditor::Document *doc, bool b, KTextEditor::Document::ModifiedOnDiskReason reason)
0409 {
0410     auto it = m_docInfos.find(doc);
0411     if (it != m_docInfos.end()) {
0412         it->second.modifiedOnDisc = b;
0413         it->second.modifiedOnDiscReason = reason;
0414         slotModChanged1(doc);
0415     }
0416 }
0417 
0418 /**
0419  * Load file's meta-information if the checksum didn't change since last time.
0420  */
0421 bool KateDocManager::loadMetaInfos(KTextEditor::Document *doc, const QUrl &url)
0422 {
0423     if (!m_saveMetaInfos) {
0424         return false;
0425     }
0426 
0427     if (!m_metaInfos.hasGroup(url.toDisplayString())) {
0428         return false;
0429     }
0430 
0431     const QByteArray checksum = doc->checksum().toHex();
0432     bool ok = true;
0433     if (!checksum.isEmpty()) {
0434         KConfigGroup urlGroup(&m_metaInfos, url.toDisplayString());
0435         const QString old_checksum = urlGroup.readEntry("Checksum");
0436 
0437         if (QString::fromLatin1(checksum) == old_checksum) {
0438             QSet<QString> flags;
0439             if (documentInfo(doc)->openedByUser) {
0440                 flags << QStringLiteral("SkipEncoding");
0441             }
0442             flags << QStringLiteral("SkipUrl");
0443             doc->readSessionConfig(urlGroup, flags);
0444         } else {
0445             urlGroup.deleteGroup();
0446             ok = false;
0447         }
0448     }
0449 
0450     return ok && doc->url() == url;
0451 }
0452 
0453 /**
0454  * Save file's meta-information if doc is in 'unmodified' state
0455  */
0456 
0457 void KateDocManager::saveMetaInfos(const QList<KTextEditor::Document *> &documents)
0458 {
0459     /**
0460      * skip work if no meta infos wanted
0461      */
0462     if (!m_saveMetaInfos) {
0463         return;
0464     }
0465 
0466     const QSet<QString> flags{QStringLiteral("SkipUrl")};
0467 
0468     /**
0469      * store meta info for all non-modified documents which have some checksum
0470      */
0471     const QDateTime now = QDateTime::currentDateTimeUtc();
0472     for (KTextEditor::Document *doc : documents) {
0473         /**
0474          * skip modified docs
0475          */
0476         if (doc->isModified()) {
0477             continue;
0478         }
0479 
0480         const QByteArray checksum = doc->checksum().toHex();
0481         if (!checksum.isEmpty()) {
0482             /**
0483              * write the group with checksum and time
0484              */
0485             const QString url = doc->url().toString();
0486             KConfigGroup urlGroup(&m_metaInfos, url);
0487 
0488             /**
0489              * write document session config
0490              */
0491             doc->writeSessionConfig(urlGroup, flags);
0492             if (!urlGroup.keyList().isEmpty()) {
0493                 urlGroup.writeEntry("URL", url);
0494                 urlGroup.writeEntry("Checksum", QString::fromLatin1(checksum));
0495                 urlGroup.writeEntry("Time", now);
0496             } else {
0497                 urlGroup.deleteGroup();
0498             }
0499         }
0500     }
0501 }
0502 
0503 void KateDocManager::slotModChanged(KTextEditor::Document *doc)
0504 {
0505     saveMetaInfos({doc});
0506 }
0507 
0508 void KateDocManager::slotModChanged1(KTextEditor::Document *doc)
0509 {
0510     // clang-format off
0511     QMetaObject::invokeMethod(KateApp::self()->activeKateMainWindow(), "queueModifiedOnDisc", Qt::QueuedConnection, Q_ARG(KTextEditor::Document*,doc));
0512     // clang-format on
0513 
0514     if (doc->isModified()) {
0515         KateDocumentInfo *info = documentInfo(doc);
0516         if (info) {
0517             info->wasDocumentEverModified = true;
0518         }
0519     }
0520 }
0521 
0522 void KateDocManager::documentOpened()
0523 {
0524     KTextEditor::Document *doc = qobject_cast<KTextEditor::Document *>(sender());
0525     if (!doc) {
0526         return; // should never happen, but who knows
0527     }
0528     disconnect(doc, SIGNAL(completed()), this, SLOT(documentOpened()));
0529     disconnect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened);
0530 
0531     // Only set "no success" when doc is empty to avoid close of files
0532     // with other trouble when do closeOrphaned()
0533     if (doc->openingError() && doc->isEmpty()) {
0534         KateDocumentInfo *info = documentInfo(doc);
0535         if (info) {
0536             info->openSuccess = false;
0537         }
0538     }
0539 }
0540 
0541 #include "moc_katedocmanager.cpp"