File indexing completed on 2024-04-28 04:37:22

0001 /*
0002     SPDX-FileCopyrightText: 2001 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
0003     SPDX-FileCopyrightText: 2002-2003 Roberto Raggi <roberto@kdevelop.org>
0004     SPDX-FileCopyrightText: 2002 Simon Hausmann <hausmann@kde.org>
0005     SPDX-FileCopyrightText: 2003 Jens Dagerbo <jens.dagerbo@swipnet.se>
0006     SPDX-FileCopyrightText: 2003 Mario Scalas <mario.scalas@libero.it>
0007     SPDX-FileCopyrightText: 2003-2004 Alexander Dymo <adymo@kdevelop.org>
0008     SPDX-FileCopyrightText: 2006 Matt Rogers <mattr@kde.org>
0009     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-or-later
0012 */
0013 
0014 #include "project.h"
0015 
0016 #include <QSet>
0017 #include <QTemporaryFile>
0018 #include <QTimer>
0019 #include <QPointer>
0020 
0021 #include <KConfigGroup>
0022 #include <KIO/FileCopyJob>
0023 #include <KIO/MkdirJob>
0024 #include <KIO/StatJob>
0025 #include <KJobWidgets>
0026 #include <KLocalizedString>
0027 #include <KMessageBox>
0028 
0029 #include <project/interfaces/iprojectfilemanager.h>
0030 #include <project/interfaces/ibuildsystemmanager.h>
0031 #include <interfaces/iplugin.h>
0032 #include <interfaces/iplugincontroller.h>
0033 #include <interfaces/iruncontroller.h>
0034 #include <interfaces/iuicontroller.h>
0035 #include <interfaces/isession.h>
0036 #include <project/projectmodel.h>
0037 #include <sublime/message.h>
0038 #include <util/path.h>
0039 #include <serialization/indexedstring.h>
0040 #include <vcs/interfaces/ibasicversioncontrol.h>
0041 
0042 #include "core.h"
0043 #include "mainwindow.h"
0044 #include "projectcontroller.h"
0045 #include "uicontroller.h"
0046 #include "debug.h"
0047 
0048 namespace KDevelop
0049 {
0050 
0051 class ProjectProgress : public QObject, public IStatus
0052 {
0053     Q_OBJECT
0054     Q_INTERFACES(KDevelop::IStatus)
0055 
0056     public:
0057         ProjectProgress();
0058         ~ProjectProgress() override;
0059         QString statusName() const override;
0060 
0061         /*! Show indeterminate mode progress bar */
0062         void setBuzzy();
0063 
0064         /*! Hide progress bar */
0065         void setDone();
0066 
0067         QString projectName;
0068 
0069     private Q_SLOTS:
0070         void slotClean();
0071 
0072     Q_SIGNALS:
0073         void clearMessage(KDevelop::IStatus*) override;
0074         void showMessage(KDevelop::IStatus*,const QString & message, int timeout = 0) override;
0075         void showErrorMessage(const QString& message, int timeout) override;
0076         void hideProgress(KDevelop::IStatus*) override;
0077         void showProgress(KDevelop::IStatus*,int minimum, int maximum, int value) override;
0078 
0079     private:
0080         QTimer* m_timer;
0081 };
0082 
0083 
0084 
0085 ProjectProgress::ProjectProgress()
0086 {
0087     m_timer = new QTimer(this);
0088     m_timer->setSingleShot( true );
0089     m_timer->setInterval( 1000 );
0090     connect(m_timer, &QTimer::timeout,this, &ProjectProgress::slotClean);
0091 }
0092 
0093 ProjectProgress::~ProjectProgress()
0094 {
0095 }
0096 
0097 QString ProjectProgress::statusName() const
0098 {
0099     return i18n("Loading Project %1", projectName);
0100 }
0101 
0102 void ProjectProgress::setBuzzy()
0103 {
0104     qCDebug(SHELL) << "showing busy progress" << statusName();
0105     // show an indeterminate progressbar
0106     emit showProgress(this, 0,0,0);
0107     emit showMessage(this, i18nc("%1: Project name", "Loading %1", projectName));
0108 }
0109 
0110 
0111 void ProjectProgress::setDone()
0112 {
0113     qCDebug(SHELL) << "showing done progress" << statusName();
0114     // first show 100% bar for a second, then hide.
0115     emit showProgress(this, 0,1,1);
0116     m_timer->start();
0117 }
0118 
0119 void ProjectProgress::slotClean()
0120 {
0121     emit hideProgress(this);
0122     emit clearMessage(this);
0123 }
0124 
0125 class ProjectPrivate
0126 {
0127 public:
0128     Path projectPath;
0129     Path projectFile;
0130     Path developerFile;
0131     QString developerTempFile;
0132     QTemporaryFile projectTempFile;
0133     IPlugin* manager = nullptr;
0134     QPointer<IPlugin> vcsPlugin;
0135     ProjectFolderItem* topItem = nullptr;
0136     QString name;
0137     KSharedConfigPtr m_cfg;
0138     Project * const project;
0139     QSet<KDevelop::IndexedString> fileSet;
0140     bool loading = false;
0141     bool fullReload;
0142     bool scheduleReload = false;
0143     ProjectProgress* progress;
0144 
0145 public:
0146     explicit ProjectPrivate(Project* project)
0147         : project(project)
0148     {}
0149 
0150     void reloadDone(KJob* job)
0151     {
0152         progress->setDone();
0153         loading = false;
0154 
0155         ProjectController* projCtrl = Core::self()->projectControllerInternal();
0156         if (job->error() == 0 && !Core::self()->shuttingDown()) {
0157 
0158             if(fullReload)
0159                 projCtrl->projectModel()->appendRow(topItem);
0160 
0161             if (scheduleReload) {
0162                 scheduleReload = false;
0163                 project->reloadModel();
0164             }
0165         } else {
0166             projCtrl->abortOpeningProject(project);
0167         }
0168     }
0169 
0170     QList<ProjectBaseItem*> itemsForPath( const IndexedString& path ) const
0171     {
0172         if ( path.isEmpty() ) {
0173             return QList<ProjectBaseItem*>();
0174         }
0175 
0176         if (!topItem->model()) {
0177             // this gets hit when the project has not yet been added to the model
0178             // i.e. during import phase
0179             // TODO: should we handle this somehow?
0180             // possible idea: make the item<->path hash per-project
0181             return QList<ProjectBaseItem*>();
0182         }
0183 
0184         Q_ASSERT(topItem->model());
0185         QList<ProjectBaseItem*> items = topItem->model()->itemsForPath(path);
0186 
0187         QList<ProjectBaseItem*>::iterator it = items.begin();
0188         while(it != items.end()) {
0189             if ((*it)->project() != project) {
0190                 it = items.erase(it);
0191             } else {
0192                 ++it;
0193             }
0194         }
0195 
0196         return items;
0197     }
0198 
0199 
0200     void importDone( KJob* job)
0201     {
0202         progress->setDone();
0203         ProjectController* projCtrl = Core::self()->projectControllerInternal();
0204 
0205         if(job->error() == 0 && !Core::self()->shuttingDown()) {
0206             loading=false;
0207             projCtrl->projectModel()->appendRow(topItem);
0208             projCtrl->projectImportingFinished( project );
0209         } else {
0210             projCtrl->abortOpeningProject(project);
0211         }
0212     }
0213 
0214     void initProject(const Path& projectFile_)
0215     {
0216         // helper method for open()
0217         projectFile = projectFile_;
0218     }
0219 
0220     bool initProjectFiles()
0221     {
0222         KIO::StatJob* statJob = KIO::stat( projectFile.toUrl(), KIO::HideProgressInfo );
0223         if ( !statJob->exec() ) //be sync for right now
0224         {
0225             const QString messageText =
0226                 i18n("Unable to load the project file %1.<br>"
0227                      "The project has been removed from the session.",
0228                      projectFile.pathOrUrl());
0229             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0230             ICore::self()->uiController()->postMessage(message);
0231             return false;
0232         }
0233 
0234         // developerfile == dirname(projectFileUrl) ."/.kdev4/". basename(projectfileUrl)
0235         developerFile = projectFile;
0236         developerFile.setLastPathSegment( QStringLiteral(".kdev4") );
0237         developerFile.addPath( projectFile.lastPathSegment() );
0238 
0239         statJob = KIO::stat( developerFile.toUrl(), KIO::HideProgressInfo );
0240         if( !statJob->exec() )
0241         {
0242             // the developerfile does not exist yet, check if its folder exists
0243             // the developerfile itself will get created below
0244             QUrl dir = developerFile.parent().toUrl();
0245             statJob = KIO::stat( dir, KIO::HideProgressInfo );
0246             if( !statJob->exec() )
0247             {
0248                 KIO::SimpleJob* mkdirJob = KIO::mkdir( dir );
0249                 if( !mkdirJob->exec() )
0250                 {
0251                     const QString messageText =
0252                         i18n("Unable to create hidden dir (%1) for developer file",
0253                              dir.toDisplayString(QUrl::PreferLocalFile));
0254                     auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0255                     ICore::self()->uiController()->postMessage(message);
0256                     return false;
0257                 }
0258             }
0259         }
0260 
0261         projectTempFile.open();
0262         auto copyJob = KIO::file_copy(projectFile.toUrl(), QUrl::fromLocalFile(projectTempFile.fileName()), -1, KIO::HideProgressInfo | KIO::Overwrite);
0263         KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow());
0264         if (!copyJob->exec())
0265         {
0266             qCDebug(SHELL) << "Job failed:" << copyJob->errorString();
0267 
0268             const QString messageText = i18n("Unable to get project file: %1", projectFile.pathOrUrl());
0269             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0270             ICore::self()->uiController()->postMessage(message);
0271             return false;
0272         }
0273 
0274         if(developerFile.isLocalFile())
0275         {
0276             developerTempFile = developerFile.toLocalFile();
0277         }
0278         else {
0279             QTemporaryFile tmp;
0280             tmp.open();
0281             developerTempFile = tmp.fileName();
0282 
0283             auto job = KIO::file_copy(developerFile.toUrl(), QUrl::fromLocalFile(developerTempFile), -1, KIO::HideProgressInfo | KIO::Overwrite);
0284             KJobWidgets::setWindow(job, Core::self()->uiController()->activeMainWindow());
0285             job->exec();
0286         }
0287         return true;
0288     }
0289 
0290     KConfigGroup initKConfigObject()
0291     {
0292         // helper method for open()
0293         qCDebug(SHELL) << "Creating KConfig object for project files" << developerTempFile << projectTempFile.fileName();
0294         m_cfg = KSharedConfig::openConfig( developerTempFile );
0295         m_cfg->addConfigSources( QStringList() << projectTempFile.fileName() );
0296         KConfigGroup projectGroup( m_cfg, "Project" );
0297         return projectGroup;
0298     }
0299 
0300     bool projectNameUsed(const KConfigGroup& projectGroup)
0301     {
0302         // helper method for open()
0303         name = projectGroup.readEntry( "Name", projectFile.lastPathSegment() );
0304         progress->projectName = name;
0305         if( Core::self()->projectController()->isProjectNameUsed( name ) )
0306         {
0307             const QString messageText =
0308                 i18n("Could not load %1, a project with the same name '%2' is already open.", projectFile.pathOrUrl(), name);
0309             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0310             ICore::self()->uiController()->postMessage(message);
0311 
0312             qCWarning(SHELL) << "Trying to open a project with a name that is already used by another open project";
0313             return true;
0314         }
0315         return false;
0316     }
0317 
0318     IProjectFileManager* fetchFileManager(const KConfigGroup& projectGroup)
0319     {
0320         if (manager)
0321         {
0322             auto* iface = manager->extension<KDevelop::IProjectFileManager>();
0323             Q_ASSERT(iface);
0324             return iface;
0325         }
0326 
0327         // helper method for open()
0328         QString managerSetting = projectGroup.readEntry( "Manager", "KDevGenericManager" );
0329 
0330         //Get our importer
0331         IPluginController* pluginManager = Core::self()->pluginController();
0332         manager = pluginManager->pluginForExtension( QStringLiteral("org.kdevelop.IProjectFileManager"), managerSetting );
0333         IProjectFileManager* iface = nullptr;
0334         if ( manager )
0335             iface = manager->extension<IProjectFileManager>();
0336         else
0337         {
0338             const QString messageText =
0339                 i18n("Could not load project management plugin <b>%1</b>.<br>Check that the required programs are installed,"
0340                      " or see console output for more information.", managerSetting);
0341             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0342             ICore::self()->uiController()->postMessage(message);
0343             manager = nullptr;
0344             return nullptr;
0345         }
0346         if (iface == nullptr)
0347         {
0348             const QString messageText =
0349                 i18n("The project importing plugin (%1) does not support the IProjectFileManager interface.", managerSetting);
0350             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0351             ICore::self()->uiController()->postMessage(message);
0352             delete manager;
0353             manager = nullptr;
0354         }
0355         return iface;
0356     }
0357 
0358     void loadVersionControlPlugin(KConfigGroup& projectGroup)
0359     {
0360         // helper method for open()
0361         IPluginController* pluginManager = Core::self()->pluginController();
0362         if( projectGroup.hasKey( "VersionControlSupport" ) )
0363         {
0364             QString vcsPluginName = projectGroup.readEntry("VersionControlSupport", "");
0365             if( !vcsPluginName.isEmpty() )
0366             {
0367                 vcsPlugin = pluginManager->pluginForExtension( QStringLiteral( "org.kdevelop.IBasicVersionControl" ), vcsPluginName );
0368             }
0369         } else
0370         {
0371             const QList<IPlugin*> plugins = pluginManager->allPluginsForExtension( QStringLiteral( "org.kdevelop.IBasicVersionControl" ) );
0372             for (IPlugin* p : plugins) {
0373                 auto* iface = p->extension<KDevelop::IBasicVersionControl>();
0374                 if (!iface) {
0375                     continue;
0376                 }
0377 
0378                 const auto url = topItem->path().toUrl();
0379                 qCDebug(SHELL) << "Checking whether" << url << "is version controlled by" << iface->name();
0380                 if(iface->isVersionControlled(url))
0381                 {
0382                     qCDebug(SHELL) << "Detected that" << url << "is a" << iface->name() << "project";
0383 
0384                     vcsPlugin = p;
0385                     projectGroup.writeEntry("VersionControlSupport", pluginManager->pluginInfo(p).pluginId());
0386                     projectGroup.sync();
0387                 }
0388             }
0389         }
0390 
0391     }
0392 
0393     bool importTopItem(IProjectFileManager* fileManager)
0394     {
0395         if (!fileManager)
0396         {
0397             return false;
0398         }
0399         topItem = fileManager->import( project );
0400         if( !topItem )
0401         {
0402             auto* message = new Sublime::Message(i18n("Could not open project."), Sublime::Message::Error);
0403             ICore::self()->uiController()->postMessage(message);
0404             return false;
0405         }
0406 
0407         return true;
0408     }
0409 
0410 };
0411 
0412 Project::Project( QObject *parent )
0413         : IProject( parent )
0414         , d_ptr(new ProjectPrivate(this))
0415 {
0416     Q_D(Project);
0417 
0418     d->progress = new ProjectProgress;
0419     Core::self()->uiController()->registerStatus( d->progress );
0420 }
0421 
0422 Project::~Project()
0423 {
0424     Q_D(Project);
0425 
0426     delete d->progress;
0427 }
0428 
0429 QString Project::name() const
0430 {
0431     Q_D(const Project);
0432 
0433     return d->name;
0434 }
0435 
0436 QString Project::developerTempFile() const
0437 {
0438     Q_D(const Project);
0439 
0440     return d->developerTempFile;
0441 }
0442 
0443 QString Project::projectTempFile() const
0444 {
0445     Q_D(const Project);
0446 
0447     return d->projectTempFile.fileName();
0448 }
0449 
0450 KSharedConfigPtr Project::projectConfiguration() const
0451 {
0452     Q_D(const Project);
0453 
0454     return d->m_cfg;
0455 }
0456 
0457 Path Project::path() const
0458 {
0459     Q_D(const Project);
0460 
0461     return d->projectPath;
0462 }
0463 
0464 void Project::reloadModel()
0465 {
0466     Q_D(Project);
0467 
0468     if (d->loading) {
0469         d->scheduleReload = true;
0470         return;
0471     }
0472     d->loading = true;
0473     d->fileSet.clear();
0474 
0475     // delete topItem and remove it from model
0476     ProjectModel* model = Core::self()->projectController()->projectModel();
0477     model->removeRow( d->topItem->row() );
0478     d->topItem = nullptr;
0479 
0480     auto* iface = d->manager->extension<IProjectFileManager>();
0481     if (!d->importTopItem(iface))
0482     {
0483             d->loading = false;
0484             d->scheduleReload = false;
0485             return;
0486     }
0487 
0488     KJob* importJob = iface->createImportJob(d->topItem );
0489     setReloadJob(importJob);
0490     d->fullReload = true;
0491     Core::self()->runController()->registerJob( importJob );
0492 }
0493 
0494 void Project::setReloadJob(KJob* job)
0495 {
0496     Q_D(Project);
0497 
0498     d->loading = true;
0499     d->fullReload = false;
0500     d->progress->setBuzzy();
0501     connect(job, &KJob::finished,
0502             this, [this] (KJob* job) { Q_D(Project); d->reloadDone(job); });
0503 }
0504 
0505 bool Project::open( const Path& projectFile )
0506 {
0507     Q_D(Project);
0508 
0509     d->initProject(projectFile);
0510     if (!d->initProjectFiles())
0511         return false;
0512 
0513     KConfigGroup projectGroup = d->initKConfigObject();
0514     if (d->projectNameUsed(projectGroup))
0515         return false;
0516 
0517     d->projectPath = d->projectFile.parent();
0518 
0519     IProjectFileManager* iface = d->fetchFileManager(projectGroup);
0520     if (!iface)
0521         return false;
0522 
0523     Q_ASSERT(d->manager);
0524 
0525     emit aboutToOpen(this);
0526     if (!d->importTopItem(iface) ) {
0527         return false;
0528     }
0529 
0530     d->loading=true;
0531     d->loadVersionControlPlugin(projectGroup);
0532     d->progress->setBuzzy();
0533     KJob* importJob = iface->createImportJob(d->topItem );
0534     connect(importJob, &KJob::result,
0535             this, [this] (KJob* job) { Q_D(Project); d->importDone(job); } );
0536     Core::self()->runController()->registerJob( importJob );
0537     return true;
0538 }
0539 
0540 void Project::close()
0541 {
0542     Q_D(Project);
0543 
0544     Q_ASSERT(d->topItem);
0545     if (d->topItem->row() == -1) {
0546         qCWarning(SHELL) << "Something went wrong. ProjectFolderItem detached. Project closed during reload?";
0547         return;
0548     }
0549 
0550     Core::self()->projectController()->projectModel()->removeRow( d->topItem->row() );
0551 
0552     if (!d->developerFile.isLocalFile())
0553     {
0554         auto copyJob = KIO::file_copy(QUrl::fromLocalFile(d->developerTempFile), d->developerFile.toUrl(), -1, KIO::HideProgressInfo);
0555         KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow());
0556         if (!copyJob->exec()) {
0557             qCDebug(SHELL) << "Job failed:" << copyJob->errorString();
0558 
0559             KMessageBox::error(Core::self()->uiController()->activeMainWindow(),
0560                                i18n("Could not store developer specific project configuration.\n"
0561                                     "Attention: The project settings you changed will be lost."));
0562         }
0563     }
0564 }
0565 
0566 bool Project::inProject( const IndexedString& path ) const
0567 {
0568     Q_D(const Project);
0569 
0570     if (d->fileSet.contains( path )) {
0571         return true;
0572     }
0573     return !d->itemsForPath( path ).isEmpty();
0574 }
0575 
0576 QList< ProjectBaseItem* > Project::itemsForPath(const IndexedString& path) const
0577 {
0578     Q_D(const Project);
0579 
0580     return d->itemsForPath(path);
0581 }
0582 
0583 QList< ProjectFileItem* > Project::filesForPath(const IndexedString& file) const
0584 {
0585     Q_D(const Project);
0586 
0587     QList<ProjectFileItem*> fileItems;
0588     const auto items = d->itemsForPath(file);
0589     for (ProjectBaseItem* item : items) {
0590         if( item->type() == ProjectBaseItem::File )
0591             fileItems << static_cast<ProjectFileItem*>(item);
0592     }
0593     return fileItems;
0594 }
0595 
0596 QList<ProjectFolderItem*> Project::foldersForPath(const IndexedString& folder) const
0597 {
0598     Q_D(const Project);
0599 
0600     QList<ProjectFolderItem*> folderItems;
0601     const auto items = d->itemsForPath(folder);
0602     for (ProjectBaseItem* item : items) {
0603         if( item->type() == ProjectBaseItem::Folder || item->type() == ProjectBaseItem::BuildFolder )
0604             folderItems << static_cast<ProjectFolderItem*>(item);
0605     }
0606     return folderItems;
0607 }
0608 
0609 IProjectFileManager* Project::projectFileManager() const
0610 {
0611     Q_D(const Project);
0612 
0613     return d->manager->extension<IProjectFileManager>();
0614 }
0615 
0616 IBuildSystemManager* Project::buildSystemManager() const
0617 {
0618     Q_D(const Project);
0619 
0620     return d->manager->extension<IBuildSystemManager>();
0621 }
0622 
0623 IPlugin* Project::managerPlugin() const
0624 {
0625     Q_D(const Project);
0626 
0627     return d->manager;
0628 }
0629 
0630 void Project::setManagerPlugin( IPlugin* manager )
0631 {
0632     Q_D(Project);
0633 
0634     d->manager = manager;
0635 }
0636 
0637 Path Project::projectFile() const
0638 {
0639     Q_D(const Project);
0640 
0641     return d->projectFile;
0642 }
0643 
0644 Path Project::developerFile() const
0645 {
0646     Q_D(const Project);
0647 
0648     return d->developerFile;
0649 }
0650 
0651 ProjectFolderItem* Project::projectItem() const
0652 {
0653     Q_D(const Project);
0654 
0655     return d->topItem;
0656 }
0657 
0658 IPlugin* Project::versionControlPlugin() const
0659 {
0660     Q_D(const Project);
0661 
0662     return d->vcsPlugin.data();
0663 }
0664 
0665 void Project::addToFileSet( ProjectFileItem* file )
0666 {
0667     Q_D(Project);
0668 
0669     if (d->fileSet.contains(file->indexedPath())) {
0670         return;
0671     }
0672 
0673     d->fileSet.insert( file->indexedPath() );
0674     emit fileAddedToSet( file );
0675 }
0676 
0677 void Project::removeFromFileSet( ProjectFileItem* file )
0678 {
0679     Q_D(Project);
0680 
0681     QSet<IndexedString>::iterator it = d->fileSet.find(file->indexedPath());
0682     if (it == d->fileSet.end()) {
0683         return;
0684     }
0685 
0686     d->fileSet.erase( it );
0687     emit fileRemovedFromSet( file );
0688 }
0689 
0690 QSet<IndexedString> Project::fileSet() const
0691 {
0692     Q_D(const Project);
0693 
0694     return d->fileSet;
0695 }
0696 
0697 bool Project::isReady() const
0698 {
0699     Q_D(const Project);
0700 
0701     return !d->loading;
0702 }
0703 
0704 } // namespace KDevelop
0705 
0706 #include "project.moc"
0707 #include "moc_project.cpp"