File indexing completed on 2024-04-28 05:49:06
0001 /* This file is part of the Kate project. 0002 * 0003 * SPDX-FileCopyrightText: 2012 Christoph Cullmann <cullmann@kde.org> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kateproject.h" 0009 #include "kateprojectitem.h" 0010 #include "kateprojectplugin.h" 0011 #include "kateprojectworker.h" 0012 0013 #include <KIO/CopyJob> 0014 #include <KJobWidgets> 0015 #include <KLocalizedString> 0016 #include <QTextDocument> 0017 #include <ktexteditor/document.h> 0018 0019 #include <json_utils.h> 0020 0021 #include <QApplication> 0022 #include <QDir> 0023 #include <QFile> 0024 #include <QFileInfo> 0025 #include <QJsonArray> 0026 #include <QJsonDocument> 0027 #include <QJsonObject> 0028 #include <QJsonParseError> 0029 #include <QMimeData> 0030 #include <QPlainTextDocumentLayout> 0031 #include <utility> 0032 0033 bool KateProjectModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 0034 { 0035 if (!canDropMimeData(data, action, row, column, parent)) { 0036 return false; 0037 } 0038 0039 const auto index = this->index(row, column, parent); 0040 const auto type = (KateProjectItem::Type)index.data(KateProjectItem::TypeRole).toInt(); 0041 const auto parentType = (KateProjectItem::Type)parent.data(KateProjectItem::TypeRole).toInt(); 0042 QString pathToCopyTo; 0043 if (!index.isValid() && parent.isValid() && parentType == KateProjectItem::Directory) { 0044 pathToCopyTo = parent.data(Qt::UserRole).toString(); 0045 } else if (index.isValid() && type == KateProjectItem::File) { 0046 if (index.parent().isValid()) { 0047 pathToCopyTo = index.parent().data(Qt::UserRole).toString(); 0048 } else { 0049 pathToCopyTo = m_project->baseDir(); 0050 } 0051 } else if (!index.isValid() && !parent.isValid()) { 0052 pathToCopyTo = m_project->baseDir(); 0053 } 0054 0055 const QDir d = pathToCopyTo; 0056 if (!d.exists()) { 0057 return false; 0058 } 0059 0060 const auto urls = data->urls(); 0061 const QString destDir = d.absolutePath(); 0062 const QUrl dest = QUrl::fromLocalFile(destDir); 0063 QPointer<KIO::CopyJob> job = KIO::copy(urls, dest); 0064 KJobWidgets::setWindow(job, QApplication::activeWindow()); 0065 connect(job, &KIO::Job::finished, this, [this, job, destDir] { 0066 if (!job || job->error() != 0 || !m_project) 0067 return; 0068 0069 bool needsReload = false; 0070 QStandardItem *item = invisibleRootItem(); 0071 if (destDir != m_project->baseDir()) { 0072 const auto indexes = match(this->index(0, 0), Qt::UserRole, destDir, 1, Qt::MatchStartsWith); 0073 if (indexes.empty()) { 0074 needsReload = true; 0075 } else { 0076 item = itemFromIndex(indexes.constFirst()); 0077 } 0078 } 0079 0080 const auto urls = job->srcUrls(); 0081 if (!needsReload) { 0082 for (const auto &url : urls) { 0083 const QString newFile = destDir + QStringLiteral("/") + url.fileName(); 0084 const QFileInfo fi(newFile); 0085 if (fi.exists() && fi.isFile()) { 0086 KateProjectItem *i = new KateProjectItem(KateProjectItem::File, url.fileName()); 0087 item->appendRow(i); 0088 m_project->addFile(newFile, i); 0089 } else { 0090 // not a file? Just do a reload of the project on finish 0091 needsReload = true; 0092 break; 0093 } 0094 } 0095 } 0096 if (needsReload && m_project) { 0097 QMetaObject::invokeMethod( 0098 this, 0099 [this] { 0100 m_project->reload(true); 0101 }, 0102 Qt::QueuedConnection); 0103 } 0104 }); 0105 job->start(); 0106 0107 return true; 0108 } 0109 0110 bool KateProjectModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int, int, const QModelIndex &) const 0111 { 0112 return data && data->hasUrls() && action == Qt::CopyAction; 0113 } 0114 0115 KateProject::KateProject(QThreadPool &threadPool, KateProjectPlugin *plugin, const QString &fileName) 0116 : m_threadPool(threadPool) 0117 , m_plugin(plugin) 0118 , m_fileBacked(true) 0119 , m_fileName(QFileInfo(fileName).absoluteFilePath()) 0120 , m_baseDir(QFileInfo(fileName).absolutePath()) 0121 { 0122 // ensure we get notified for project file changes 0123 connect(&m_plugin->fileWatcher(), &QFileSystemWatcher::fileChanged, this, &KateProject::slotFileChanged); 0124 m_plugin->fileWatcher().addPath(m_fileName); 0125 0126 m_model.m_project = this; 0127 0128 // try to load the project map from our file, will start worker thread, too 0129 reload(); 0130 } 0131 0132 KateProject::KateProject(QThreadPool &threadPool, KateProjectPlugin *plugin, const QVariantMap &globalProject, const QString &directory) 0133 : m_threadPool(threadPool) 0134 , m_plugin(plugin) 0135 , m_fileBacked(false) 0136 , m_fileName(QDir(QDir(directory).absolutePath()).filePath(QStringLiteral(".kateproject"))) 0137 , m_baseDir(QDir(directory).absolutePath()) 0138 , m_globalProject(globalProject) 0139 { 0140 m_model.m_project = this; 0141 // try to load the project map, will start worker thread, too 0142 load(globalProject); 0143 } 0144 0145 KateProject::~KateProject() 0146 { 0147 saveNotesDocument(); 0148 0149 // stop watching if we have some real project file 0150 if (m_fileBacked && !m_fileName.isEmpty()) { 0151 m_plugin->fileWatcher().removePath(m_fileName); 0152 } 0153 } 0154 0155 bool KateProject::reload(bool force) 0156 { 0157 const QVariantMap map = readProjectFile(); 0158 if (!map.isEmpty()) { 0159 m_globalProject = map; 0160 } 0161 0162 return load(m_globalProject, force); 0163 } 0164 0165 void KateProject::renameFile(const QString &newName, const QString &oldName) 0166 { 0167 auto it = m_file2Item->find(oldName); 0168 if (it == m_file2Item->end()) { 0169 qWarning() << "renameFile() File not found, new: " << newName << "old: " << oldName; 0170 return; 0171 } 0172 (*m_file2Item)[newName] = it.value(); 0173 m_file2Item->erase(it); 0174 } 0175 0176 void KateProject::removeFile(const QString &file) 0177 { 0178 auto it = m_file2Item->find(file); 0179 if (it == m_file2Item->end()) { 0180 qWarning() << "removeFile() File not found: " << file; 0181 return; 0182 } 0183 m_file2Item->erase(it); 0184 } 0185 0186 /** 0187 * Read a JSON document from file. 0188 * 0189 * In case of an error, the returned object verifies isNull() is true. 0190 */ 0191 QJsonDocument KateProject::readJSONFile(const QString &fileName) const 0192 { 0193 /** 0194 * keep each project file last modification time to warn the user only once per malformed file. 0195 */ 0196 static QHash<QString, QDateTime> lastModifiedTimes; 0197 0198 if (fileName.isEmpty()) { 0199 return QJsonDocument(); 0200 } 0201 0202 QFile file(fileName); 0203 if (!file.exists() || !file.open(QFile::ReadOnly)) { 0204 return QJsonDocument(); 0205 } 0206 0207 /** 0208 * parse the whole file, bail out again on error! 0209 */ 0210 const QByteArray jsonData = file.readAll(); 0211 QJsonParseError parseError{}; 0212 QJsonDocument document(QJsonDocument::fromJson(jsonData, &parseError)); 0213 0214 if (parseError.error != QJsonParseError::NoError) { 0215 QDateTime lastModified = QFileInfo(fileName).lastModified(); 0216 if (lastModified > lastModifiedTimes.value(fileName, QDateTime())) { 0217 lastModifiedTimes[fileName] = lastModified; 0218 m_plugin->sendMessage(i18n("Malformed JSON file '%1': %2", fileName, parseError.errorString()), true); 0219 } 0220 return QJsonDocument(); 0221 } 0222 0223 return document; 0224 } 0225 0226 QVariantMap KateProject::readProjectFile() const 0227 { 0228 // not file back => will not work 0229 if (!m_fileBacked) { 0230 return QVariantMap(); 0231 } 0232 0233 // bail out on error 0234 QJsonDocument project(readJSONFile(m_fileName)); 0235 if (project.isNull()) { 0236 return QVariantMap(); 0237 } 0238 0239 /** 0240 * convenience; align with auto-generated projects 0241 * generate 'name' and 'files' if not specified explicitly, 0242 * so those parts need not be given if only wishes to specify additional 0243 * project configuration (e.g. build, ctags) 0244 */ 0245 if (project.isObject()) { 0246 auto dir = QFileInfo(m_fileName).dir(); 0247 auto object = project.object(); 0248 0249 // if there are local settings (.kateproject.local), override values 0250 { 0251 const auto localSettings = readJSONFile(projectLocalFileName(QStringLiteral("local"))); 0252 if (!localSettings.isNull() && localSettings.isObject()) { 0253 object = json::merge(object, localSettings.object()); 0254 } 0255 } 0256 0257 auto name = object[QStringLiteral("name")]; 0258 if (name.isUndefined() || name.isNull()) { 0259 name = dir.dirName(); 0260 } 0261 auto files = object[QStringLiteral("files")]; 0262 if (files.isUndefined() || files.isNull()) { 0263 // support all we can, could try to detect, 0264 // but it will be sorted out upon loading anyway 0265 QJsonArray afiles; 0266 for (const auto &t : {QStringLiteral("git"), QStringLiteral("hg"), QStringLiteral("svn"), QStringLiteral("darcs")}) { 0267 afiles.push_back(QJsonObject{{t, true}}); 0268 } 0269 files = afiles; 0270 } 0271 project.setObject(object); 0272 } 0273 0274 return project.toVariant().toMap(); 0275 } 0276 0277 bool KateProject::load(const QVariantMap &globalProject, bool force) 0278 { 0279 /** 0280 * no name, bad => bail out 0281 */ 0282 if (globalProject[QStringLiteral("name")].toString().isEmpty()) { 0283 return false; 0284 } 0285 0286 /** 0287 * support out-of-source project files 0288 * ensure we handle relative paths properly => relative to the potential invented .kateproject file name 0289 */ 0290 const auto baseDir = globalProject[QStringLiteral("directory")].toString(); 0291 if (!baseDir.isEmpty()) { 0292 m_baseDir = QFileInfo(QFileInfo(m_fileName).dir(), baseDir).absoluteFilePath(); 0293 } 0294 0295 /** 0296 * anything changed? 0297 * else be done without forced reload! 0298 */ 0299 if (!force && (m_projectMap == globalProject)) { 0300 return true; 0301 } 0302 0303 /** 0304 * setup global attributes in this object 0305 */ 0306 m_projectMap = globalProject; 0307 0308 // emit that we changed stuff 0309 Q_EMIT projectMapChanged(); 0310 0311 // trigger loading of project in background thread 0312 QString indexDir; 0313 if (m_plugin->getIndexEnabled()) { 0314 indexDir = m_plugin->getIndexDirectory().toLocalFile(); 0315 // if empty, use regular tempdir 0316 if (indexDir.isEmpty()) { 0317 indexDir = QDir::tempPath(); 0318 } 0319 } 0320 0321 auto column = m_model.invisibleRootItem()->takeColumn(0); 0322 m_untrackedDocumentsRoot = nullptr; 0323 m_file2Item.reset(); 0324 auto deleter = QRunnable::create([column = std::move(column)] { 0325 qDeleteAll(column); 0326 }); 0327 m_threadPool.start(deleter); 0328 0329 // let's run the stuff in our own thread pool 0330 // do manual queued connect, as only run() is done in extra thread, object stays in this one 0331 auto w = new KateProjectWorker(m_baseDir, indexDir, m_projectMap, force); 0332 connect(w, &KateProjectWorker::loadDone, this, &KateProject::loadProjectDone, Qt::QueuedConnection); 0333 connect(w, &KateProjectWorker::loadIndexDone, this, &KateProject::loadIndexDone, Qt::QueuedConnection); 0334 m_threadPool.start(w); 0335 0336 // we are done here 0337 return true; 0338 } 0339 0340 void KateProject::loadProjectDone(const KateProjectSharedQStandardItem &topLevel, KateProjectSharedQHashStringItem file2Item) 0341 { 0342 m_model.clear(); 0343 m_model.invisibleRootItem()->appendColumn(topLevel->takeColumn(0)); 0344 m_untrackedDocumentsRoot = nullptr; 0345 m_file2Item = std::move(file2Item); 0346 0347 /** 0348 * readd the documents that are open atm 0349 */ 0350 for (auto i = m_documents.constBegin(); i != m_documents.constEnd(); i++) { 0351 registerDocument(i.key()); 0352 } 0353 0354 Q_EMIT modelChanged(); 0355 } 0356 0357 void KateProject::loadIndexDone(KateProjectSharedProjectIndex projectIndex) 0358 { 0359 /** 0360 * move to our project 0361 */ 0362 m_projectIndex = std::move(projectIndex); 0363 0364 /** 0365 * notify external world that data is available 0366 */ 0367 Q_EMIT indexChanged(); 0368 } 0369 0370 QString KateProject::projectLocalFileName(const QString &suffix) const 0371 { 0372 /** 0373 * nothing on empty file names for project 0374 * should not happen 0375 */ 0376 if (m_baseDir.isEmpty() || suffix.isEmpty()) { 0377 return QString(); 0378 } 0379 0380 /** 0381 * compute full file name 0382 */ 0383 return QDir(m_baseDir).filePath(QStringLiteral(".kateproject.") + suffix); 0384 } 0385 0386 QTextDocument *KateProject::notesDocument() 0387 { 0388 /** 0389 * already there? 0390 */ 0391 if (m_notesDocument) { 0392 return m_notesDocument; 0393 } 0394 0395 /** 0396 * else create it 0397 */ 0398 m_notesDocument = new QTextDocument(this); 0399 m_notesDocument->setDocumentLayout(new QPlainTextDocumentLayout(m_notesDocument)); 0400 0401 /** 0402 * get file name 0403 */ 0404 const QString notesFileName = projectLocalFileName(QStringLiteral("notes")); 0405 if (notesFileName.isEmpty()) { 0406 return m_notesDocument; 0407 } 0408 0409 /** 0410 * and load text if possible 0411 */ 0412 QFile inFile(notesFileName); 0413 if (inFile.open(QIODevice::ReadOnly)) { 0414 QTextStream inStream(&inFile); 0415 m_notesDocument->setPlainText(inStream.readAll()); 0416 } 0417 0418 /** 0419 * and be done 0420 */ 0421 return m_notesDocument; 0422 } 0423 0424 void KateProject::saveNotesDocument() 0425 { 0426 /** 0427 * no notes document, nothing to do 0428 */ 0429 if (!m_notesDocument) { 0430 return; 0431 } 0432 0433 /** 0434 * get content & filename 0435 */ 0436 const QString content = m_notesDocument->toPlainText(); 0437 const QString notesFileName = projectLocalFileName(QStringLiteral("notes")); 0438 if (notesFileName.isEmpty()) { 0439 return; 0440 } 0441 0442 /** 0443 * no content => unlink file, if there 0444 */ 0445 if (content.isEmpty()) { 0446 if (QFile::exists(notesFileName)) { 0447 QFile::remove(notesFileName); 0448 } 0449 return; 0450 } 0451 0452 /** 0453 * else: save content to file 0454 */ 0455 QFile outFile(projectLocalFileName(QStringLiteral("notes"))); 0456 if (outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { 0457 QTextStream outStream(&outFile); 0458 outStream << content; 0459 } 0460 } 0461 0462 void KateProject::slotModifiedChanged(KTextEditor::Document *document) 0463 { 0464 KateProjectItem *item = itemForFile(m_documents.value(document)); 0465 0466 if (!item) { 0467 return; 0468 } 0469 0470 item->slotModifiedChanged(document); 0471 } 0472 0473 void KateProject::slotModifiedOnDisk(KTextEditor::Document *document, bool isModified, KTextEditor::Document::ModifiedOnDiskReason reason) 0474 { 0475 KateProjectItem *item = itemForFile(m_documents.value(document)); 0476 0477 if (!item) { 0478 return; 0479 } 0480 0481 item->slotModifiedOnDisk(document, isModified, reason); 0482 } 0483 0484 void KateProject::registerDocument(KTextEditor::Document *document) 0485 { 0486 // remember the document, if not already there 0487 if (!m_documents.contains(document)) { 0488 m_documents[document] = document->url().toLocalFile(); 0489 } 0490 0491 // try to get item for the document 0492 KateProjectItem *item = itemForFile(document->url().toLocalFile()); 0493 0494 // if we got one, we are done, else create a dummy! 0495 // clang-format off 0496 if (item) { 0497 disconnect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); 0498 disconnect(document, &KTextEditor::Document::modifiedOnDisk, this, &KateProject::slotModifiedOnDisk); 0499 item->slotModifiedChanged(document); 0500 0501 /*FIXME item->slotModifiedOnDisk(document,document->isModified(),qobject_cast<KTextEditor::ModificationInterface*>(document)->modifiedOnDisk()); 0502 * FIXME*/ 0503 0504 connect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); 0505 connect(document, &KTextEditor::Document::modifiedOnDisk, this, &KateProject::slotModifiedOnDisk); 0506 0507 return; 0508 } 0509 // clang-format on 0510 0511 registerUntrackedDocument(document); 0512 } 0513 0514 void KateProject::registerUntrackedDocument(KTextEditor::Document *document) 0515 { 0516 // perhaps create the parent item 0517 if (!m_untrackedDocumentsRoot) { 0518 m_untrackedDocumentsRoot = new KateProjectItem(KateProjectItem::Directory, i18n("<untracked>")); 0519 m_model.insertRow(0, m_untrackedDocumentsRoot); 0520 } 0521 0522 // create document item 0523 QFileInfo fileInfo(document->url().toLocalFile()); 0524 KateProjectItem *fileItem = new KateProjectItem(KateProjectItem::File, fileInfo.fileName()); 0525 fileItem->slotModifiedChanged(document); 0526 connect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); 0527 connect(document, &KTextEditor::Document::modifiedOnDisk, this, &KateProject::slotModifiedOnDisk); 0528 0529 bool inserted = false; 0530 for (int i = 0; i < m_untrackedDocumentsRoot->rowCount(); ++i) { 0531 if (m_untrackedDocumentsRoot->child(i)->data(Qt::UserRole).toString() > document->url().toLocalFile()) { 0532 m_untrackedDocumentsRoot->insertRow(i, fileItem); 0533 inserted = true; 0534 break; 0535 } 0536 } 0537 if (!inserted) { 0538 m_untrackedDocumentsRoot->appendRow(fileItem); 0539 } 0540 0541 fileItem->setData(document->url().toLocalFile(), Qt::UserRole); 0542 fileItem->setData(QVariant(true), Qt::UserRole + 3); 0543 0544 if (!m_file2Item) { 0545 m_file2Item = KateProjectSharedQHashStringItem(new QHash<QString, KateProjectItem *>()); 0546 } 0547 (*m_file2Item)[document->url().toLocalFile()] = fileItem; 0548 } 0549 0550 void KateProject::unregisterDocument(KTextEditor::Document *document) 0551 { 0552 if (!m_documents.contains(document)) { 0553 return; 0554 } 0555 0556 // ignore further updates but clear state once 0557 disconnect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); 0558 const QString &file = m_documents.value(document); 0559 KateProjectItem *item = static_cast<KateProjectItem *>(itemForFile(file)); 0560 if (item) { 0561 item->slotModifiedChanged(nullptr); 0562 } 0563 0564 if (m_untrackedDocumentsRoot) { 0565 if (item && item->data(Qt::UserRole + 3).toBool()) { 0566 unregisterUntrackedItem(item); 0567 m_file2Item->remove(file); 0568 } 0569 } 0570 0571 m_documents.remove(document); 0572 } 0573 0574 void KateProject::unregisterUntrackedItem(const KateProjectItem *item) 0575 { 0576 for (int i = 0; i < m_untrackedDocumentsRoot->rowCount(); ++i) { 0577 if (m_untrackedDocumentsRoot->child(i) == item) { 0578 m_untrackedDocumentsRoot->removeRow(i); 0579 break; 0580 } 0581 } 0582 0583 if (m_untrackedDocumentsRoot->rowCount() < 1) { 0584 m_model.removeRow(0); 0585 m_untrackedDocumentsRoot = nullptr; 0586 } 0587 } 0588 0589 void KateProject::slotFileChanged(const QString &file) 0590 { 0591 if (file == m_fileName) { 0592 reload(); 0593 } 0594 } 0595 0596 #include "moc_kateproject.cpp"