File indexing completed on 2024-05-05 04:39:25

0001 /*
0002     SPDX-FileCopyrightText: 2006 Matt Rogers <mattr@kde.org>
0003     SPDX-FileCopyrightText: 2007-2013 Aleix Pol <aleixpol@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "cmakemanager.h"
0009 #include "cmakeutils.h"
0010 #include "cmakeprojectdata.h"
0011 #include "duchain/cmakeparsejob.h"
0012 #include "cmakeimportjsonjob.h"
0013 #include "debug.h"
0014 #include "cmakecodecompletionmodel.h"
0015 #include "cmakenavigationwidget.h"
0016 #include "icmakedocumentation.h"
0017 #include "cmakemodelitems.h"
0018 #include "testing/ctestutils.h"
0019 #include "testing/ctestsuite.h"
0020 #include "testing/ctestfindjob.h"
0021 #include "cmakeserverimportjob.h"
0022 #include "cmakeserver.h"
0023 #include "cmakefileapi.h"
0024 #include "cmakefileapiimportjob.h"
0025 
0026 #ifndef CMAKEMANAGER_NO_SETTINGS
0027 #include "settings/cmakepreferences.h"
0028 #endif
0029 
0030 #include <QApplication>
0031 #include <QDir>
0032 #include <QReadWriteLock>
0033 #include <QThread>
0034 #include <QFileSystemWatcher>
0035 #include <QTimer>
0036 
0037 #include <KPluginFactory>
0038 #include <QUrl>
0039 #include <QAction>
0040 #include <KTextEditor/Document>
0041 #include <KDirWatch>
0042 
0043 #include <interfaces/icore.h>
0044 #include <interfaces/idocumentcontroller.h>
0045 #include <interfaces/iprojectcontroller.h>
0046 #include <interfaces/iproject.h>
0047 #include <interfaces/iplugincontroller.h>
0048 #include <interfaces/iruntimecontroller.h>
0049 #include <interfaces/iruntime.h>
0050 #include <interfaces/iruncontroller.h>
0051 #include <interfaces/itestcontroller.h>
0052 #include <interfaces/iuicontroller.h>
0053 #include <interfaces/contextmenuextension.h>
0054 #include <interfaces/context.h>
0055 #include <interfaces/idocumentation.h>
0056 #include <util/executecompositejob.h>
0057 #include <language/highlighting/codehighlighting.h>
0058 #include <project/projectmodel.h>
0059 #include <project/helper.h>
0060 #include <project/interfaces/iprojectbuilder.h>
0061 #include <language/codecompletion/codecompletion.h>
0062 #include <language/duchain/duchainlock.h>
0063 #include <language/duchain/use.h>
0064 #include <language/duchain/duchain.h>
0065 #include <makefileresolver/makefileresolver.h>
0066 #include <sublime/message.h>
0067 
0068 using namespace KDevelop;
0069 
0070 K_PLUGIN_FACTORY_WITH_JSON(CMakeSupportFactory, "kdevcmakemanager.json", registerPlugin<CMakeManager>(); )
0071 
0072 CMakeManager::CMakeManager( QObject* parent, const QVariantList& )
0073     : KDevelop::AbstractFileManagerPlugin( QStringLiteral("kdevcmakemanager"), parent )
0074 {
0075     if (CMake::findExecutable().isEmpty()) {
0076         setErrorDescription(i18n("Unable to find a CMake executable. Is one installed on the system?"));
0077         m_highlight = nullptr;
0078         return;
0079     }
0080 
0081     m_highlight = new KDevelop::CodeHighlighting(this);
0082 
0083     new CodeCompletion(this, new CMakeCodeCompletionModel(this), name());
0084 
0085     connect(ICore::self()->projectController(), &IProjectController::projectClosing, this, &CMakeManager::projectClosing);
0086     connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &CMakeManager::reloadProjects);
0087     connect(this, &KDevelop::AbstractFileManagerPlugin::folderAdded, this, &CMakeManager::folderAdded);
0088 }
0089 
0090 CMakeManager::~CMakeManager()
0091 {
0092     parseLock()->lockForWrite();
0093     // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state
0094     parseLock()->unlock();
0095 }
0096 
0097 bool CMakeManager::hasBuildInfo(ProjectBaseItem* item) const
0098 {
0099     return m_projects[item->project()].data.compilationData.files.contains(item->path());
0100 }
0101 
0102 Path CMakeManager::buildDirectory(KDevelop::ProjectBaseItem *item) const
0103 {
0104     return Path(CMake::currentBuildDir(item->project()));
0105 }
0106 
0107 KDevelop::ProjectFolderItem* CMakeManager::import( KDevelop::IProject *project )
0108 {
0109     CMake::checkForNeedingConfigure(project);
0110 
0111     return AbstractFileManagerPlugin::import(project);
0112 }
0113 
0114 class ChooseCMakeInterfaceJob : public ExecuteCompositeJob
0115 {
0116     Q_OBJECT
0117 public:
0118     ChooseCMakeInterfaceJob(IProject* project, CMakeManager* manager, bool forceConfigure)
0119         : ExecuteCompositeJob(manager, {})
0120         , project(project)
0121         , manager(manager)
0122         , forceConfigure(forceConfigure)
0123     {
0124     }
0125 
0126     void start() override {
0127         if (CMake::FileApi::supported(CMake::currentCMakeExecutable(project).toLocalFile())) {
0128             qCDebug(CMAKE) << "Using cmake-file-api for import of" << project->path();
0129 
0130             // try to import the data directly, if possible and not outdated
0131             if (forceConfigure) {
0132                 reconfigureThenImport();
0133             } else {
0134                 tryDirectImport();
0135             }
0136             ExecuteCompositeJob::start();
0137         } else {
0138             tryCMakeServer();
0139         }
0140     }
0141 
0142 private:
0143     void tryCMakeServer()
0144     {
0145         qCDebug(CMAKE) << "try cmake server for import";
0146         server.reset(new CMakeServer(project));
0147         connect(server.data(), &CMakeServer::connected, this, &ChooseCMakeInterfaceJob::successfulConnection);
0148         connect(server.data(), &CMakeServer::finished, this, &ChooseCMakeInterfaceJob::failedConnection);
0149     }
0150 
0151     void successfulConnection() {
0152         auto job = new CMakeServerImportJob(project, server, this);
0153         connect(job, &CMakeServerImportJob::result, this, [this, job](){
0154             if (job->error() == 0) {
0155                 manager->integrateData(job->projectData(), job->project(), server);
0156             }
0157         });
0158         addSubjob(job);
0159         ExecuteCompositeJob::start();
0160     }
0161 
0162     void failedConnection(int code) {
0163         Q_ASSERT(code > 0);
0164         Q_ASSERT(!server->isServerAvailable());
0165 
0166         qCDebug(CMAKE) << "CMake does not provide server mode, using compile_commands.json to import" << project->name();
0167 
0168         // parse the JSON file
0169         auto* job = new CMakeImportJsonJob(project, this);
0170 
0171         // create the JSON file if it doesn't exist
0172         auto commandsFile = CMake::commandsFile(project);
0173         if (!QFileInfo::exists(commandsFile.toLocalFile())) {
0174             qCDebug(CMAKE) << "couldn't find commands file:" << commandsFile << "- now trying to reconfigure";
0175             addSubjob(manager->builder()->configure(project));
0176         }
0177 
0178         connect(job, &CMakeImportJsonJob::result, this, [this, job]() {
0179             if (job->error() == 0) {
0180                 manager->integrateData(job->projectData(), job->project());
0181             }
0182         });
0183         addSubjob(job);
0184         ExecuteCompositeJob::start();
0185     }
0186 
0187     void reconfigureThenImport()
0188     {
0189         addSubjob(manager->builder()->configure(project));
0190         auto* importJob = new CMake::FileApi::ImportJob(project, this);
0191         connect(importJob, &CMake::FileApi::ImportJob::dataAvailable, this, &ChooseCMakeInterfaceJob::fileImportDone);
0192         addSubjob(importJob);
0193     }
0194 
0195     void tryDirectImport()
0196     {
0197         auto* importJob = new CMake::FileApi::ImportJob(project, this);
0198         importJob->setInvalidateOutdatedData();
0199         importJob->setEmitInvalidData();
0200         connect(importJob, &CMake::FileApi::ImportJob::dataAvailable, this, [this](const CMakeProjectData& data) {
0201             if (!data.compilationData.isValid) {
0202                 qCDebug(CMAKE) << "reconfiguring project" << project->name() << "because project data is outdated";
0203                 reconfigureThenImport();
0204             } else {
0205                 Q_ASSERT_X(!data.isOutdated, Q_FUNC_INFO,
0206                            "ImportJob::setInvalidateOutdatedData() failed to mark outdated data invalid.");
0207                 qCDebug(CMAKE) << "skipping configure project" << project->name()
0208                                << "because project data is up to date";
0209                 fileImportDone(data);
0210             }
0211         });
0212         addSubjob(importJob);
0213     }
0214 
0215     void fileImportDone(const CMakeProjectData& data)
0216     {
0217         Q_ASSERT(data.compilationData.isValid);
0218         manager->integrateData(data, project);
0219     }
0220 
0221     QSharedPointer<CMakeServer> server;
0222     IProject* const project;
0223     CMakeManager* const manager;
0224     const bool forceConfigure;
0225 };
0226 
0227 KJob* CMakeManager::createImportJob(ProjectFolderItem* item, bool forceConfigure)
0228 {
0229     auto project = item->project();
0230 
0231     delete m_configureStatusMessages.value(project); // discard now-obsolete message from the previous configuration
0232 
0233     auto job = new ChooseCMakeInterfaceJob(project, this, forceConfigure);
0234     connect(job, &KJob::result, this, [this, job, project]() {
0235         if (job->error() != 0) {
0236             qCWarning(CMAKE) << "couldn't load project successfully" << project->name() << job->error()
0237                              << job->errorText();
0238             showConfigureErrorMessage(*project, job->errorString());
0239         }
0240     });
0241 
0242     const QList<KJob*> jobs = {
0243         job,
0244         KDevelop::AbstractFileManagerPlugin::createImportJob(item) // generate the file system listing
0245     };
0246 
0247     Q_ASSERT(!jobs.contains(nullptr));
0248     auto* composite = new ExecuteCompositeJob(this, jobs);
0249     // even if the cmake call failed, we want to load the project so that the project can be worked on
0250     composite->setAbortOnSubjobError(false);
0251     return composite;
0252 }
0253 
0254 KJob* CMakeManager::createImportJob(ProjectFolderItem* item)
0255 {
0256     return createImportJob(item, false);
0257 }
0258 
0259 QList<KDevelop::ProjectTargetItem*> CMakeManager::targets() const
0260 {
0261     QList<KDevelop::ProjectTargetItem*> ret;
0262     for (auto it = m_projects.begin(), end = m_projects.end(); it != end; ++it) {
0263         IProject* p = it.key();
0264         ret+=p->projectItem()->targetList();
0265     }
0266     return ret;
0267 }
0268 
0269 CMakeFile CMakeManager::fileInformation(KDevelop::ProjectBaseItem* item) const
0270 {
0271     const auto projectData = m_projects.constFind(item->project());
0272     if (projectData == m_projects.cend()) {
0273         return {};
0274     }
0275     const auto& data = projectData->data.compilationData;
0276 
0277     const auto itemPath = item->path();
0278     if (!data.isValid || data.missingFiles.contains(itemPath)) {
0279         return {};
0280     }
0281 
0282     auto toCanonicalPath = [](const Path &path) -> Path {
0283         // if the path contains a symlink, then we will not find it in the lookup table
0284         // as that only only stores canonicalized paths. Thus, we fallback to
0285         // to the canonicalized path and see if that brings up any matches
0286         const auto localPath = path.toLocalFile();
0287         const auto canonicalPath = QFileInfo(localPath).canonicalFilePath();
0288         return (localPath == canonicalPath) ? path : Path(canonicalPath);
0289     };
0290 
0291     auto path = itemPath;
0292     if (!item->folder()) {
0293         // try to look for file meta data directly
0294         auto it = data.files.find(path);
0295         if (it == data.files.end()) {
0296             // fallback to canonical path lookup
0297             auto canonical = toCanonicalPath(path);
0298             if (canonical != path) {
0299                 it = data.files.find(canonical);
0300             }
0301         }
0302         if (it != data.files.end()) {
0303             return *it;
0304         }
0305         // else look for a file in the parent folder
0306         path = path.parent();
0307     }
0308 
0309     while (true) {
0310         // try to look for a file in the current folder path
0311         auto it = data.fileForFolder.find(path);
0312         if (it == data.fileForFolder.end()) {
0313             // fallback to canonical path lookup
0314             auto canonical = toCanonicalPath(path);
0315             if (canonical != path) {
0316                 it = data.fileForFolder.find(canonical);
0317             }
0318         }
0319         if (it != data.fileForFolder.end()) {
0320             return data.files[it.value()];
0321         }
0322         if (!path.hasParent()) {
0323             break;
0324         }
0325         path = path.parent();
0326     }
0327 
0328     qCDebug(CMAKE) << "no information found for" << itemPath;
0329     data.missingFiles.insert(itemPath);
0330     return {};
0331 }
0332 
0333 Path::List CMakeManager::includeDirectories(KDevelop::ProjectBaseItem *item) const
0334 {
0335     return fileInformation(item).includes;
0336 }
0337 
0338 Path::List CMakeManager::frameworkDirectories(KDevelop::ProjectBaseItem *item) const
0339 {
0340     return fileInformation(item).frameworkDirectories;
0341 }
0342 
0343 QHash<QString, QString> CMakeManager::defines(KDevelop::ProjectBaseItem *item ) const
0344 {
0345     return fileInformation(item).defines;
0346 }
0347 
0348 QString CMakeManager::extraArguments(KDevelop::ProjectBaseItem *item) const
0349 {
0350     return fileInformation(item).compileFlags;
0351 }
0352 
0353 KDevelop::IProjectBuilder * CMakeManager::builder() const
0354 {
0355     IPlugin* i = core()->pluginController()->pluginForExtension( QStringLiteral("org.kdevelop.IProjectBuilder"), QStringLiteral("KDevCMakeBuilder"));
0356     Q_ASSERT(i);
0357     auto* _builder = i->extension<KDevelop::IProjectBuilder>();
0358     Q_ASSERT(_builder );
0359     return _builder ;
0360 }
0361 
0362 bool CMakeManager::reload(KDevelop::ProjectFolderItem* folder)
0363 {
0364     qCDebug(CMAKE) << "reloading" << folder->path();
0365 
0366     IProject* project = folder->project();
0367     if (!project->isReady()) {
0368         qCDebug(CMAKE) << "the project is being reloaded, aborting reload!";
0369         return false;
0370     }
0371 
0372     KJob* job = createImportJob(folder, true);
0373     project->setReloadJob(job);
0374     ICore::self()->runController()->registerJob( job );
0375     if (folder == project->projectItem()) {
0376         connect(job, &KJob::finished, this, [project](KJob* job) {
0377             if (job->error())
0378                 return;
0379 
0380             emit KDevelop::ICore::self()->projectController()->projectConfigurationChanged(project);
0381             KDevelop::ICore::self()->projectController()->reparseProject(project);
0382         });
0383     }
0384 
0385     return true;
0386 }
0387 
0388 static void populateTargets(ProjectFolderItem* folder, const QHash<KDevelop::Path, QVector<CMakeTarget>>& targets)
0389 {
0390     auto isValidTarget = [](const CMakeTarget& target) -> bool {
0391         if (target.type != CMakeTarget::Custom)
0392             return true;
0393 
0394         // utility targets with empty sources are strange (e.g. _QCH) -> skip them
0395         if (target.sources.isEmpty())
0396             return false;
0397 
0398         auto match
0399             = [](const auto& needles, auto&& op) { return std::any_of(std::begin(needles), std::end(needles), op); };
0400         auto startsWith = [&](const auto& needle) { return target.name.startsWith(needle); };
0401         auto endsWith = [&](const auto& needle) { return target.name.endsWith(needle); };
0402         auto equals = [&](const auto& needle) { return target.name == needle; };
0403 
0404         const auto invalidPrefixes = { QLatin1String("install/") };
0405         const auto invalidSuffixes
0406             = { QLatin1String("_automoc"), QLatin1String("_autogen"), QLatin1String("_autogen_timestamp_deps") };
0407         const auto standardTargets
0408             = { QLatin1String("edit_cache"), QLatin1String("rebuild_cache"), QLatin1String("list_install_components"),
0409                 QLatin1String("test"), // not really standard, but applicable for make and ninja
0410                 QLatin1String("install") };
0411         return !match(invalidPrefixes, startsWith) && !match(invalidSuffixes, endsWith)
0412             && !match(standardTargets, equals);
0413     };
0414 
0415     auto isValidTargetSource = [](const Path& source) {
0416         const auto& segments = source.segments();
0417         const auto lastSegment = source.lastPathSegment();
0418         // skip non-existent cmake internal rule files
0419         if (lastSegment.endsWith(QLatin1String(".rule"))) {
0420             return false;
0421         }
0422 
0423         const auto secondToLastSegment = segments.value(segments.size() - 2);
0424         // ignore generated cmake-internal files
0425         if (secondToLastSegment == QLatin1String("CMakeFiles")) {
0426             return false;
0427         }
0428 
0429         // also skip *_autogen/timestamp files
0430         if (lastSegment == QLatin1String("timestamp") && secondToLastSegment.endsWith(QLatin1String("_autogen"))) {
0431             return false;
0432         }
0433 
0434         return true;
0435     };
0436 
0437     // start by deleting all targets, the type may have changed anyways
0438     const auto tl = folder->targetList();
0439     for (ProjectTargetItem* item : tl) {
0440         delete item;
0441     }
0442 
0443     QHash<QString, ProjectBaseItem*> folderItems;
0444     folderItems[{}] = folder;
0445     auto findOrCreateFolderItem = [&folderItems, folder](const QString& targetFolder)
0446     {
0447         auto& item = folderItems[targetFolder];
0448         if (!item) {
0449             item = new ProjectTargetItem(folder->project(), targetFolder, folder);
0450             // these are "virtual" folders, they keep the original path
0451             item->setPath(folder->path());
0452         }
0453         return item;
0454     };
0455 
0456     // target folder name (or empty) to list of targets
0457     for (const auto &target : targets[folder->path()]) {
0458         if (!isValidTarget(target)) {
0459             continue;
0460         }
0461 
0462         auto* targetFolder = findOrCreateFolderItem(target.folder);
0463         auto* targetItem = [&]() -> ProjectBaseItem* {
0464             switch(target.type) {
0465                 case CMakeTarget::Executable:
0466                     return new CMakeTargetItem(targetFolder, target.name, target.artifacts.value(0));
0467                 case CMakeTarget::Library:
0468                     return new ProjectLibraryTargetItem(folder->project(), target.name, targetFolder);
0469                 case CMakeTarget::Custom:
0470                     return new ProjectTargetItem(folder->project(), target.name, targetFolder);
0471             }
0472             Q_UNREACHABLE();
0473         }();
0474 
0475         for (const auto& source : target.sources) {
0476             if (!isValidTargetSource(source)) {
0477                 continue;
0478             }
0479             new ProjectFileItem(folder->project(), source, targetItem);
0480         }
0481     }
0482 }
0483 
0484 static void cleanupTestSuites(const QVector<CTestSuite*>& testSuites, const QVector<CTestFindJob*>& testSuiteJobs)
0485 {
0486     for (auto* testSuiteJob : testSuiteJobs) {
0487         testSuiteJob->kill(KJob::Quietly);
0488     }
0489     for (auto* testSuite : testSuites) {
0490         ICore::self()->testController()->removeTestSuite(testSuite);
0491         delete testSuite;
0492     }
0493 }
0494 
0495 void CMakeManager::integrateData(const CMakeProjectData &data, KDevelop::IProject* project, const QSharedPointer<CMakeServer>& server)
0496 {
0497     // TODO: show the warning message only after the entire import job finishes. When the message
0498     // is shown here (earlier), the user can follow the advice and manage to reload the project
0499     // before the current import finishes. Then KDevelop would print a kdevelop.plugins.cmake.debug
0500     // message "the project is being reloaded, aborting reload!" and ignore the reload request.
0501     if (data.isOutdated) {
0502         showConfigureOutdatedMessage(*project);
0503     }
0504 
0505     if (server) {
0506         connect(server.data(), &CMakeServer::response, project, [this, project](const QJsonObject& response) {
0507             if (response[QStringLiteral("type")] == QLatin1String("signal")) {
0508                 if (response[QStringLiteral("name")] == QLatin1String("dirty")) {
0509                     m_projects[project].server->configure({});
0510                 } else
0511                     qCDebug(CMAKE) << "unhandled signal response..." << project << response;
0512             } else if (response[QStringLiteral("type")] == QLatin1String("error")) {
0513                 showConfigureErrorMessage(*project, response[QStringLiteral("errorMessage")].toString());
0514             } else if (response[QStringLiteral("type")] == QLatin1String("reply")) {
0515                 const auto inReplyTo = response[QStringLiteral("inReplyTo")];
0516                 if (inReplyTo == QLatin1String("configure")) {
0517                     m_projects[project].server->compute();
0518                 } else if (inReplyTo == QLatin1String("compute")) {
0519                     m_projects[project].server->codemodel();
0520                 } else if(inReplyTo == QLatin1String("codemodel")) {
0521                     auto &data = m_projects[project].data;
0522                     CMakeServerImportJob::processCodeModel(response, data);
0523                     populateTargets(project->projectItem(), data.targets);
0524                 } else {
0525                     qCDebug(CMAKE) << "unhandled reply response..." << project << response;
0526                 }
0527             } else {
0528                 qCDebug(CMAKE) << "unhandled response..." << project << response;
0529             }
0530         });
0531     } else if (!m_projects.contains(project)) {
0532         auto* reloadTimer = new QTimer(project);
0533         reloadTimer->setSingleShot(true);
0534         reloadTimer->setInterval(1000);
0535         connect(reloadTimer, &QTimer::timeout, this, [project, this]() {
0536             reload(project->projectItem());
0537         });
0538         connect(projectWatcher(project), &KDirWatch::dirty, reloadTimer, [this, project, reloadTimer](const QString &strPath) {
0539             const auto it = m_projects.constFind(project);
0540             if (it == m_projects.cend() || !it->data.cmakeFiles.contains(Path{strPath})) {
0541                 return;
0542             }
0543             qCDebug(CMAKE) << "eventually starting reload due to change of" << strPath;
0544             reloadTimer->start();
0545         });
0546     }
0547 
0548     auto& projectData = m_projects[project];
0549     cleanupTestSuites(projectData.testSuites, projectData.testSuiteJobs);
0550 
0551     QVector<CTestSuite*> testSuites;
0552     QVector<CTestFindJob*> testSuiteJobs;
0553     for (auto& suite : CTestUtils::createTestSuites(data.testSuites, data.targets, project)) {
0554         auto* testSuite = suite.release();
0555         testSuites.append(testSuite);
0556         auto* job = new CTestFindJob(testSuite);
0557         connect(job, &KJob::result, this, [this, job, project, testSuite]() {
0558             if (!job->error()) {
0559                 ICore::self()->testController()->addTestSuite(testSuite);
0560             }
0561             m_projects[project].testSuiteJobs.removeOne(job);
0562         });
0563         ICore::self()->runController()->registerJob(job);
0564         testSuiteJobs.append(job);
0565     }
0566 
0567     projectData = { data, server, std::move(testSuites), std::move(testSuiteJobs) };
0568     populateTargets(project->projectItem(), data.targets);
0569 }
0570 
0571 QList< KDevelop::ProjectTargetItem * > CMakeManager::targets(KDevelop::ProjectFolderItem * folder) const
0572 {
0573     return folder->targetList();
0574 }
0575 
0576 QString CMakeManager::name() const
0577 {
0578     return languageName().str();
0579 }
0580 
0581 IndexedString CMakeManager::languageName()
0582 {
0583     static IndexedString name("CMake");
0584     return name;
0585 }
0586 
0587 KDevelop::ParseJob * CMakeManager::createParseJob(const IndexedString &url)
0588 {
0589     return new CMakeParseJob(url, this);
0590 }
0591 
0592 KDevelop::ICodeHighlighting* CMakeManager::codeHighlighting() const
0593 {
0594     return m_highlight;
0595 }
0596 
0597 bool CMakeManager::removeFilesFromTargets(const QList<ProjectFileItem*> &/*files*/)
0598 {
0599     return false;
0600 }
0601 
0602 bool CMakeManager::addFilesToTarget(const QList< ProjectFileItem* > &/*_files*/, ProjectTargetItem* /*target*/)
0603 {
0604     return false;
0605 }
0606 
0607 KTextEditor::Range CMakeManager::termRangeAtPosition(const KTextEditor::Document* textDocument,
0608                                                      const KTextEditor::Cursor& position) const
0609 {
0610     const KTextEditor::Cursor step(0, 1);
0611 
0612     enum ParseState {
0613         NoChar,
0614         NonLeadingChar,
0615         AnyChar,
0616     };
0617 
0618     ParseState parseState = NoChar;
0619     KTextEditor::Cursor start = position;
0620     while (true) {
0621         const QChar c = textDocument->characterAt(start);
0622         if (c.isDigit()) {
0623             parseState = NonLeadingChar;
0624         } else if (c.isLetter() || c == QLatin1Char('_')) {
0625             parseState = AnyChar;
0626         } else {
0627             // also catches going out of document range, where c is invalid
0628             break;
0629         }
0630         start -= step;
0631     }
0632 
0633     if (parseState != AnyChar) {
0634         return KTextEditor::Range::invalid();
0635     }
0636     // undo step before last valid char
0637     start += step;
0638 
0639     KTextEditor::Cursor end = position + step;
0640     while (true) {
0641         const QChar c = textDocument->characterAt(end);
0642         if (!(c.isDigit() || c.isLetter() || c == QLatin1Char('_'))) {
0643             // also catches going out of document range, where c is invalid
0644             break;
0645         }
0646         end += step;
0647     }
0648 
0649     return KTextEditor::Range(start, end);
0650 }
0651 
0652 void CMakeManager::showConfigureOutdatedMessage(const KDevelop::IProject& project)
0653 {
0654     const QString messageText = i18n(
0655         "Configured project '%1' with outdated CMake data."
0656         " As a result, KDevelop's code understanding may be wrong.\n"
0657         "\n"
0658         "To fix this issue, please right-click the project item in the projects tool view and click 'Reload'.",
0659         project.name());
0660     showConfigureStatusMessage(project, messageText, Sublime::Message::Warning);
0661 }
0662 
0663 void CMakeManager::showConfigureErrorMessage(const IProject& project, const QString& errorMessage)
0664 {
0665     const QString messageText = i18n(
0666         "Failed to configure project '%1' (error message: %2)."
0667         " As a result, KDevelop's code understanding will likely be broken.\n"
0668         "\n"
0669         "To fix this issue, please ensure that the project's CMakeLists.txt files"
0670         " are correct, and KDevelop is configured to use the correct CMake version and settings."
0671         " Then right-click the project item in the projects tool view and click 'Reload'.",
0672         project.name(), errorMessage);
0673     showConfigureStatusMessage(project, messageText, Sublime::Message::Error);
0674 }
0675 
0676 void CMakeManager::showConfigureStatusMessage(const IProject& project, const QString& messageText,
0677                                               Sublime::Message::MessageType messageType)
0678 {
0679     auto& message = m_configureStatusMessages[&project];
0680     Q_ASSERT_X(!message, Q_FUNC_INFO, "The previous message must have been discarded earlier.");
0681     message = new Sublime::Message(messageText, messageType);
0682     ICore::self()->uiController()->postMessage(message);
0683 }
0684 
0685 QPair<QWidget*, KTextEditor::Range> CMakeManager::specialLanguageObjectNavigationWidget(const QUrl& url, const KTextEditor::Cursor& position)
0686 {
0687     KTextEditor::Range itemRange;
0688     CMakeNavigationWidget* doc = nullptr;
0689 
0690     KDevelop::TopDUContextPointer top= TopDUContextPointer(KDevelop::DUChain::self()->chainForDocument(url));
0691     if(top)
0692     {
0693         int useAt=top->findUseAt(top->transformToLocalRevision(position));
0694         if(useAt>=0)
0695         {
0696             Use u=top->uses()[useAt];
0697             doc = new CMakeNavigationWidget(top, u.usedDeclaration(top->topContext()));
0698             itemRange = u.m_range.castToSimpleRange();
0699         }
0700     }
0701 
0702     if (!doc) {
0703         ICMakeDocumentation* docu=CMake::cmakeDocumentation();
0704         if( docu )
0705         {
0706             const auto* document = ICore::self()->documentController()->documentForUrl(url);
0707             const auto* textDocument = document->textDocument();
0708             itemRange = termRangeAtPosition(textDocument, position);
0709             if (itemRange.isValid()) {
0710                 const auto id = textDocument->text(itemRange);
0711 
0712                 if (!id.isEmpty()) {
0713                     IDocumentation::Ptr desc=docu->description(id, url);
0714                     if (desc) {
0715                         doc=new CMakeNavigationWidget(top, desc);
0716                     }
0717                 }
0718             }
0719         }
0720     }
0721 
0722     return {doc, itemRange};
0723 }
0724 
0725 QPair<QString, QString> CMakeManager::cacheValue(KDevelop::IProject* /*project*/, const QString& /*id*/) const
0726 { return QPair<QString, QString>(); }
0727 
0728 void CMakeManager::projectClosing(IProject* p)
0729 {
0730     const auto it = m_projects.constFind(p);
0731     if (it != m_projects.cend()) {
0732         cleanupTestSuites(it->testSuites, it->testSuiteJobs);
0733         m_projects.erase(it);
0734     }
0735 
0736     delete m_configureStatusMessages.take(p); // discard the message, because closing its project obsoletes it
0737 }
0738 
0739 void CMakeManager::folderAdded(KDevelop::ProjectFolderItem* folder)
0740 {
0741     populateTargets(folder, m_projects.value(folder->project()).data.targets);
0742 }
0743 
0744 ProjectFolderItem* CMakeManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent)
0745 {
0746 //     TODO: when we have data about targets, use folders with targets or similar
0747     if (QFile::exists(path.toLocalFile()+QLatin1String("/CMakeLists.txt")))
0748         return new KDevelop::ProjectBuildFolderItem( project, path, parent );
0749     else
0750         return KDevelop::AbstractFileManagerPlugin::createFolderItem(project, path, parent);
0751 }
0752 
0753 int CMakeManager::perProjectConfigPages() const
0754 {
0755     return 1;
0756 }
0757 
0758 ConfigPage* CMakeManager::perProjectConfigPage(int number, const ProjectConfigOptions& options, QWidget* parent)
0759 {
0760 #ifdef CMAKEMANAGER_NO_SETTINGS
0761     Q_UNUSED(number);
0762     Q_UNUSED(options);
0763     Q_UNUSED(parent);
0764     return nullptr;
0765 #else
0766     if (number == 0) {
0767         return new CMakePreferences(this, options, parent);
0768     }
0769     return nullptr;
0770 #endif
0771 }
0772 
0773 void CMakeManager::reloadProjects()
0774 {
0775     const auto& projects = m_projects.keys();
0776     for (IProject* project : projects) {
0777         CMake::checkForNeedingConfigure(project);
0778         reload(project->projectItem());
0779     }
0780 }
0781 
0782 CMakeTarget CMakeManager::targetInformation(KDevelop::ProjectTargetItem* item) const
0783 {
0784     const auto targets = m_projects[item->project()].data.targets[item->parent()->path()];
0785     for (auto target: targets) {
0786         if (item->text() == target.name) {
0787             return target;
0788         }
0789     }
0790     return {};
0791 }
0792 
0793 KDevelop::Path CMakeManager::compiler(KDevelop::ProjectTargetItem* item) const
0794 {
0795     const auto targetInfo = targetInformation(item);
0796     if (targetInfo.sources.isEmpty()) {
0797         qCDebug(CMAKE) << "could not find target" << item->text();
0798         return {};
0799     }
0800 
0801     const auto info = m_projects[item->project()].data.compilationData.files[targetInfo.sources.constFirst()];
0802     const auto lang = info.language;
0803     if (lang.isEmpty()) {
0804         qCDebug(CMAKE) << "no language for" << item << item->text() << info.defines << targetInfo.sources.constFirst();
0805         return {};
0806     }
0807     const QString var = QLatin1String("CMAKE_") + lang + QLatin1String("_COMPILER");
0808     const auto ret = CMake::readCacheValues(KDevelop::Path(buildDirectory(item), QStringLiteral("CMakeCache.txt")), {var});
0809     qCDebug(CMAKE) << "compiler for" << lang << var << ret;
0810     return KDevelop::Path(ret.value(var));
0811 }
0812 
0813 #include "cmakemanager.moc"
0814 #include "moc_cmakemanager.cpp"