diff --git a/language/backgroundparser/backgroundparser.cpp b/language/backgroundparser/backgroundparser.cpp index be5c26771..8623fb48b 100644 --- a/language/backgroundparser/backgroundparser.cpp +++ b/language/backgroundparser/backgroundparser.cpp @@ -1,917 +1,917 @@ /* * This file is part of KDevelop * * Copyright 2006 Adam Treat * Copyright 2007 Kris Wong * Copyright 2007-2008 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "backgroundparser.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util/debug.h" #include "parsejob.h" using namespace KDevelop; namespace { const bool separateThreadForHighPriority = true; /** * Elides string in @p path, e.g. "VEEERY/LONG/PATH" -> ".../LONG/PATH" * - probably much faster than QFontMetrics::elidedText() * - we do not need a widget context * - takes path separators into account * * @p width Maximum number of characters * * TODO: Move to kdevutil? */ QString elidedPathLeft(const QString& path, int width) { static const QChar separator = QDir::separator(); static const QString placeholder = QStringLiteral("..."); if (path.size() <= width) { return path; } int start = (path.size() - width) + placeholder.size(); int pos = path.indexOf(separator, start); if (pos == -1) { pos = start; // no separator => just cut off the path at the beginning } Q_ASSERT(path.size() - pos >= 0 && path.size() - pos <= width); QStringRef elidedText = path.rightRef(path.size() - pos); QString result = placeholder; result.append(elidedText); return result; } /** * @return true if @p url is non-empty, valid and has a clean path, false otherwise. */ inline bool isValidURL(const IndexedString& url) { if (url.isEmpty()) { return false; } QUrl original = url.toUrl(); - if (!original.isValid() || original.isRelative() || (original.fileName().isEmpty() && original.isLocalFile())) { + if (!original.isValid() || original.isRelative() || (original.isLocalFile() && !QFileInfo(original.toLocalFile()).isFile())) { qCWarning(LANGUAGE) << "INVALID URL ENCOUNTERED:" << url << original; return false; } QUrl cleaned = original.adjusted(QUrl::NormalizePathSegments); return original == cleaned; } } struct DocumentParseTarget { QPointer notifyWhenReady; int priority; TopDUContext::Features features; ParseJob::SequentialProcessingFlags sequentialProcessingFlags; bool operator==(const DocumentParseTarget& rhs) const { return notifyWhenReady == rhs.notifyWhenReady && priority == rhs.priority && features == rhs.features; } }; inline uint qHash(const DocumentParseTarget& target) { return target.features * 7 + target.priority * 13 + target.sequentialProcessingFlags * 17 + reinterpret_cast(target.notifyWhenReady.data()); }; struct DocumentParsePlan { QSet targets; ParseJob::SequentialProcessingFlags sequentialProcessingFlags() const { //Pick the strictest possible flags ParseJob::SequentialProcessingFlags ret = ParseJob::IgnoresSequentialProcessing; foreach(const DocumentParseTarget &target, targets) { ret |= target.sequentialProcessingFlags; } return ret; } int priority() const { //Pick the best priority int ret = BackgroundParser::WorstPriority; foreach(const DocumentParseTarget &target, targets) { if(target.priority < ret) { ret = target.priority; } } return ret; } TopDUContext::Features features() const { //Pick the best features TopDUContext::Features ret = (TopDUContext::Features)0; foreach(const DocumentParseTarget &target, targets) { ret = (TopDUContext::Features) (ret | target.features); } return ret; } QList > notifyWhenReady() const { QList > ret; foreach(const DocumentParseTarget &target, targets) { if(target.notifyWhenReady) ret << target.notifyWhenReady; } return ret; } }; Q_DECLARE_TYPEINFO(DocumentParseTarget, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(DocumentParsePlan, Q_MOVABLE_TYPE); class KDevelop::BackgroundParserPrivate { public: BackgroundParserPrivate(BackgroundParser *parser, ILanguageController *languageController) :m_parser(parser), m_languageController(languageController), m_shuttingDown(false), m_mutex(QMutex::Recursive) { parser->d = this; //Set this so we can safely call back BackgroundParser from within loadSettings() m_timer.setSingleShot(true); m_progressTimer.setSingleShot(true); m_progressTimer.setInterval(500); ThreadWeaver::setDebugLevel(true, 1); QObject::connect(&m_timer, &QTimer::timeout, m_parser, &BackgroundParser::parseDocuments); QObject::connect(&m_progressTimer, &QTimer::timeout, m_parser, &BackgroundParser::updateProgressBar); } void startTimerThreadSafe(int delay) { QMetaObject::invokeMethod(m_parser, "startTimer", Qt::QueuedConnection, Q_ARG(int, delay)); } ~BackgroundParserPrivate() { m_weaver.resume(); m_weaver.finish(); } // Non-mutex guarded functions, only call with m_mutex acquired. int currentBestRunningPriority() const { int bestRunningPriority = BackgroundParser::WorstPriority; for (const auto* decorator : m_parseJobs) { const ParseJob* parseJob = dynamic_cast(decorator->job()); Q_ASSERT(parseJob); if (parseJob->respectsSequentialProcessing() && parseJob->parsePriority() < bestRunningPriority) { bestRunningPriority = parseJob->parsePriority(); } } return bestRunningPriority; } IndexedString nextDocumentToParse() const { // Before starting a new job, first wait for all higher-priority ones to finish. // That way, parse job priorities can be used for dependency handling. const int bestRunningPriority = currentBestRunningPriority(); for (auto it1 = m_documentsForPriority.begin(); it1 != m_documentsForPriority.end(); ++it1 ) { const auto priority = it1.key(); if(priority > m_neededPriority) break; //The priority is not good enough to be processed right now if (m_parseJobs.count() >= m_threads && priority > BackgroundParser::NormalPriority && !specialParseJob) { break; //The additional parsing thread is reserved for higher priority parsing } for (const auto& url : it1.value()) { // When a document is scheduled for parsing while it is being parsed, it will be parsed // again once the job finished, but not now. if (m_parseJobs.contains(url)) { continue; } Q_ASSERT(m_documents.contains(url)); const auto& parsePlan = m_documents[url]; // If the current job requires sequential processing, but not all jobs with a better priority have been // completed yet, it will not be created now. if ( parsePlan.sequentialProcessingFlags() & ParseJob::RequiresSequentialProcessing && parsePlan.priority() > bestRunningPriority ) { continue; } return url; } } return {}; } /** * Create a single delayed parse job * * E.g. jobs for documents which have been changed by the user, but also to * handle initial startup where we parse all project files. */ void parseDocumentsInternal() { if(m_shuttingDown) return; //Only create parse-jobs for up to thread-count * 2 documents, so we don't fill the memory unnecessarily if (m_parseJobs.count() >= m_threads+1 || (m_parseJobs.count() >= m_threads && !separateThreadForHighPriority)) { return; } const auto& url = nextDocumentToParse(); if (!url.isEmpty()) { qCDebug(LANGUAGE) << "creating parse-job" << url << "new count of active parse-jobs:" << m_parseJobs.count() + 1; const QString elidedPathString = elidedPathLeft(url.str(), 70); emit m_parser->showMessage(m_parser, i18n("Parsing: %1", elidedPathString)); ThreadWeaver::QObjectDecorator* decorator = nullptr; { // copy shared data before unlocking the mutex const auto parsePlanConstIt = m_documents.constFind(url); const DocumentParsePlan parsePlan = *parsePlanConstIt; // we must not lock the mutex while creating a parse job // this could in turn lock e.g. the DUChain and then // we have a classic lock order inversion (since, usually, // we lock first the duchain and then our background parser // mutex) // see also: https://bugs.kde.org/show_bug.cgi?id=355100 m_mutex.unlock(); decorator = createParseJob(url, parsePlan); m_mutex.lock(); } // iterator might get invalid during the time we didn't have the lock // search again const auto parsePlanIt = m_documents.find(url); if (parsePlanIt != m_documents.end()) { // Remove all mentions of this document. for (const auto& target : qAsConst(parsePlanIt->targets)) { m_documentsForPriority[target.priority].remove(url); } m_documents.erase(parsePlanIt); } else { qWarning(LANGUAGE) << "Document got removed during parse job creation:" << url; } if (decorator) { if(m_parseJobs.count() == m_threads+1 && !specialParseJob) specialParseJob = decorator; //This parse-job is allocated into the reserved thread m_parseJobs.insert(url, decorator); m_weaver.enqueue(ThreadWeaver::JobPointer(decorator)); } else { --m_maxParseJobs; } if (!m_documents.isEmpty()) { // Only try creating one parse-job at a time, else we might iterate through thousands of files // without finding a language-support, and block the UI for a long time. QMetaObject::invokeMethod(m_parser, "parseDocuments", Qt::QueuedConnection); } else { // make sure we cleaned up properly // TODO: also empty m_documentsForPriority when m_documents is empty? or do we want to keep capacity? Q_ASSERT(std::none_of(m_documentsForPriority.constBegin(), m_documentsForPriority.constEnd(), [] (const QSet& docs) { return !docs.isEmpty(); })); } } m_parser->updateProgressData(); } // NOTE: you must not access any of the data structures that are protected by any of the // background parser internal mutexes in this method // see also: https://bugs.kde.org/show_bug.cgi?id=355100 ThreadWeaver::QObjectDecorator* createParseJob(const IndexedString& url, const DocumentParsePlan& parsePlan) { ///FIXME: use IndexedString in the other APIs as well! Esp. for createParseJob! QUrl qUrl = url.toUrl(); const auto languages = m_languageController->languagesForUrl(qUrl); const auto& notifyWhenReady = parsePlan.notifyWhenReady(); for (const auto language : languages) { if (!language) { qCWarning(LANGUAGE) << "got zero language for" << qUrl; continue; } ParseJob* job = language->createParseJob(url); if (!job) { continue; // Language part did not produce a valid ParseJob. } job->setParsePriority(parsePlan.priority()); job->setMinimumFeatures(parsePlan.features()); job->setNotifyWhenReady(notifyWhenReady); job->setSequentialProcessingFlags(parsePlan.sequentialProcessingFlags()); ThreadWeaver::QObjectDecorator* decorator = new ThreadWeaver::QObjectDecorator(job); QObject::connect(decorator, &ThreadWeaver::QObjectDecorator::done, m_parser, &BackgroundParser::parseComplete); QObject::connect(decorator, &ThreadWeaver::QObjectDecorator::failed, m_parser, &BackgroundParser::parseComplete); QObject::connect(job, &ParseJob::progress, m_parser, &BackgroundParser::parseProgress, Qt::QueuedConnection); // TODO more thinking required here to support multiple parse jobs per url (where multiple language plugins want to parse) return decorator; } if (languages.isEmpty()) qCDebug(LANGUAGE) << "found no languages for url" << qUrl; else qCDebug(LANGUAGE) << "could not create parse-job for url" << qUrl; //Notify that we failed for (const auto& n : notifyWhenReady) { if (!n) { continue; } QMetaObject::invokeMethod(n.data(), "updateReady", Qt::QueuedConnection, Q_ARG(KDevelop::IndexedString, url), Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext())); } return nullptr; } void loadSettings() { ///@todo re-load settings when they have been changed! Q_ASSERT(ICore::self()->activeSession()); KConfigGroup config(ICore::self()->activeSession()->config(), "Background Parser"); // stay backwards compatible KConfigGroup oldConfig(KSharedConfig::openConfig(), "Background Parser"); #define BACKWARDS_COMPATIBLE_ENTRY(entry, default) \ config.readEntry(entry, oldConfig.readEntry(entry, default)) m_delay = BACKWARDS_COMPATIBLE_ENTRY("Delay", 500); m_timer.setInterval(m_delay); m_threads = 0; if (qEnvironmentVariableIsSet("KDEV_BACKGROUNDPARSER_MAXTHREADS")) { m_parser->setThreadCount(qgetenv("KDEV_BACKGROUNDPARSER_MAXTHREADS").toInt()); } else { m_parser->setThreadCount(BACKWARDS_COMPATIBLE_ENTRY("Number of Threads", QThread::idealThreadCount())); } resume(); if (BACKWARDS_COMPATIBLE_ENTRY("Enabled", true)) { m_parser->enableProcessing(); } else { m_parser->disableProcessing(); } } void suspend() { qCDebug(LANGUAGE) << "Suspending background parser"; bool s = m_weaver.state()->stateId() == ThreadWeaver::Suspended || m_weaver.state()->stateId() == ThreadWeaver::Suspending; if (s) { // Already suspending qCWarning(LANGUAGE) << "Already suspended or suspending"; return; } m_timer.stop(); m_weaver.suspend(); } void resume() { bool s = m_weaver.state()->stateId() == ThreadWeaver::Suspended || m_weaver.state()->stateId() == ThreadWeaver::Suspending; if (m_timer.isActive() && !s) { // Not suspending return; } m_timer.start(m_delay); m_weaver.resume(); } BackgroundParser *m_parser; ILanguageController* m_languageController; //Current parse-job that is executed in the additional thread QPointer specialParseJob; QTimer m_timer; int m_delay = 500; int m_threads = 1; bool m_shuttingDown; // A list of documents that are planned to be parsed, and their priority QHash m_documents; // The documents ordered by priority QMap > m_documentsForPriority; // Currently running parse jobs QHash m_parseJobs; // The url for each managed document. Those may temporarily differ from the real url. QHash m_managedTextDocumentUrls; // Projects currently in progress of loading QSet m_loadingProjects; ThreadWeaver::Queue m_weaver; // generic high-level mutex QMutex m_mutex; // local mutex only protecting m_managed QMutex m_managedMutex; // A change tracker for each managed document QHash m_managed; int m_maxParseJobs = 0; int m_doneParseJobs = 0; QHash m_jobProgress; /// The minimum priority needed for processed jobs int m_neededPriority = BackgroundParser::WorstPriority; int m_progressMax = 0; int m_progressDone = 0; QTimer m_progressTimer; }; BackgroundParser::BackgroundParser(ILanguageController *languageController) : QObject(languageController), d(new BackgroundParserPrivate(this, languageController)) { Q_ASSERT(ICore::self()->documentController()); connect(ICore::self()->documentController(), &IDocumentController::documentLoaded, this, &BackgroundParser::documentLoaded); connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this, &BackgroundParser::documentUrlChanged); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &BackgroundParser::documentClosed); connect(ICore::self(), &ICore::aboutToShutdown, this, &BackgroundParser::aboutToQuit); bool connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectAboutToBeOpened, this, &BackgroundParser::projectAboutToBeOpened); Q_ASSERT(connected); connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, &BackgroundParser::projectOpened); Q_ASSERT(connected); connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectOpeningAborted, this, &BackgroundParser::projectOpeningAborted); Q_ASSERT(connected); Q_UNUSED(connected); } void BackgroundParser::aboutToQuit() { d->m_shuttingDown = true; } BackgroundParser::~BackgroundParser() { delete d; } QString BackgroundParser::statusName() const { return i18n("Background Parser"); } void BackgroundParser::loadSettings() { d->loadSettings(); } void BackgroundParser::parseProgress(KDevelop::ParseJob* job, float value, QString text) { Q_UNUSED(text) d->m_jobProgress[job] = value; updateProgressData(); } void BackgroundParser::revertAllRequests(QObject* notifyWhenReady) { QMutexLocker lock(&d->m_mutex); for (auto it = d->m_documents.begin(); it != d->m_documents.end(); ) { d->m_documentsForPriority[it.value().priority()].remove(it.key()); foreach ( const DocumentParseTarget& target, (*it).targets ) { if ( notifyWhenReady && target.notifyWhenReady.data() == notifyWhenReady ) { (*it).targets.remove(target); } } if((*it).targets.isEmpty()) { it = d->m_documents.erase(it); --d->m_maxParseJobs; continue; } d->m_documentsForPriority[it.value().priority()].insert(it.key()); ++it; } } void BackgroundParser::addDocument(const IndexedString& url, TopDUContext::Features features, int priority, QObject* notifyWhenReady, ParseJob::SequentialProcessingFlags flags, int delay) { // qCDebug(LANGUAGE) << "BackgroundParser::addDocument" << url.toUrl(); Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); { DocumentParseTarget target; target.priority = priority; target.features = features; target.sequentialProcessingFlags = flags; target.notifyWhenReady = QPointer(notifyWhenReady); auto it = d->m_documents.find(url); if (it != d->m_documents.end()) { //Update the stored plan d->m_documentsForPriority[it.value().priority()].remove(url); it.value().targets << target; d->m_documentsForPriority[it.value().priority()].insert(url); }else{ // qCDebug(LANGUAGE) << "BackgroundParser::addDocument: queuing" << cleanedUrl; d->m_documents[url].targets << target; d->m_documentsForPriority[d->m_documents[url].priority()].insert(url); ++d->m_maxParseJobs; //So the progress-bar waits for this document } if ( delay == ILanguageSupport::DefaultDelay ) { delay = d->m_delay; } d->startTimerThreadSafe(delay); } } void BackgroundParser::removeDocument(const IndexedString& url, QObject* notifyWhenReady) { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); if(d->m_documents.contains(url)) { d->m_documentsForPriority[d->m_documents[url].priority()].remove(url); foreach(const DocumentParseTarget& target, d->m_documents[url].targets) { if(target.notifyWhenReady.data() == notifyWhenReady) { d->m_documents[url].targets.remove(target); } } if(d->m_documents[url].targets.isEmpty()) { d->m_documents.remove(url); --d->m_maxParseJobs; }else{ //Insert with an eventually different priority d->m_documentsForPriority[d->m_documents[url].priority()].insert(url); } } } void BackgroundParser::parseDocuments() { if (!d->m_loadingProjects.empty()) { startTimer(d->m_delay); return; } QMutexLocker lock(&d->m_mutex); d->parseDocumentsInternal(); } void BackgroundParser::parseComplete(const ThreadWeaver::JobPointer& job) { auto decorator = dynamic_cast(job.data()); Q_ASSERT(decorator); ParseJob* parseJob = dynamic_cast(decorator->job()); Q_ASSERT(parseJob); emit parseJobFinished(parseJob); { QMutexLocker lock(&d->m_mutex); d->m_parseJobs.remove(parseJob->document()); d->m_jobProgress.remove(parseJob); ++d->m_doneParseJobs; updateProgressData(); } //Continue creating more parse-jobs QMetaObject::invokeMethod(this, "parseDocuments", Qt::QueuedConnection); } void BackgroundParser::disableProcessing() { setNeededPriority(BestPriority); } void BackgroundParser::enableProcessing() { setNeededPriority(WorstPriority); } int BackgroundParser::priorityForDocument(const IndexedString& url) const { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); return d->m_documents[url].priority(); } bool BackgroundParser::isQueued(const IndexedString& url) const { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); return d->m_documents.contains(url); } int BackgroundParser::queuedCount() const { QMutexLocker lock(&d->m_mutex); return d->m_documents.count(); } bool BackgroundParser::isIdle() const { QMutexLocker lock(&d->m_mutex); return d->m_documents.isEmpty() && d->m_weaver.isIdle(); } void BackgroundParser::setNeededPriority(int priority) { QMutexLocker lock(&d->m_mutex); d->m_neededPriority = priority; d->startTimerThreadSafe(d->m_delay); } void BackgroundParser::abortAllJobs() { qCDebug(LANGUAGE) << "Aborting all parse jobs"; d->m_weaver.requestAbort(); } void BackgroundParser::suspend() { d->suspend(); emit hideProgress(this); } void BackgroundParser::resume() { d->resume(); updateProgressData(); } void BackgroundParser::updateProgressData() { if (d->m_doneParseJobs >= d->m_maxParseJobs) { if(d->m_doneParseJobs > d->m_maxParseJobs) { qCDebug(LANGUAGE) << "m_doneParseJobs larger than m_maxParseJobs:" << d->m_doneParseJobs << d->m_maxParseJobs; } d->m_doneParseJobs = 0; d->m_maxParseJobs = 0; } else { float additionalProgress = 0; for (auto it = d->m_jobProgress.constBegin(); it != d->m_jobProgress.constEnd(); ++it) { additionalProgress += *it; } d->m_progressMax = d->m_maxParseJobs*1000; d->m_progressDone = (additionalProgress + d->m_doneParseJobs)*1000; if (!d->m_progressTimer.isActive()) { d->m_progressTimer.start(); } } // Cancel progress updating and hide progress-bar when parsing is done. if(d->m_doneParseJobs == d->m_maxParseJobs || (d->m_neededPriority == BackgroundParser::BestPriority && d->m_weaver.queueLength() == 0)) { if (d->m_progressTimer.isActive()) { d->m_progressTimer.stop(); } emit d->m_parser->hideProgress(d->m_parser); } } ParseJob* BackgroundParser::parseJobForDocument(const IndexedString& document) const { Q_ASSERT(isValidURL(document)); QMutexLocker lock(&d->m_mutex); auto decorator = d->m_parseJobs.value(document); return decorator ? dynamic_cast(decorator->job()) : nullptr; } void BackgroundParser::setThreadCount(int threadCount) { if (d->m_threads != threadCount) { d->m_threads = threadCount; d->m_weaver.setMaximumNumberOfThreads(d->m_threads+1); //1 Additional thread for high-priority parsing } } int BackgroundParser::threadCount() const { return d->m_threads; } void BackgroundParser::setDelay(int milliseconds) { if (d->m_delay != milliseconds) { d->m_delay = milliseconds; d->m_timer.setInterval(d->m_delay); } } QList< IndexedString > BackgroundParser::managedDocuments() { QMutexLocker l(&d->m_managedMutex); return d->m_managed.keys(); } DocumentChangeTracker* BackgroundParser::trackerForUrl(const KDevelop::IndexedString& url) const { if (url.isEmpty()) { // this happens e.g. when setting the final location of a problem that is not // yet associated with a top ctx. return nullptr; } if ( !isValidURL(url) ) { qCWarning(LANGUAGE) << "Tracker requested for invalild URL:" << url.toUrl(); } Q_ASSERT(isValidURL(url)); QMutexLocker l(&d->m_managedMutex); return d->m_managed.value(url, nullptr); } void BackgroundParser::documentClosed(IDocument* document) { QMutexLocker l(&d->m_mutex); if(document->textDocument()) { KTextEditor::Document* textDocument = document->textDocument(); if(!d->m_managedTextDocumentUrls.contains(textDocument)) return; // Probably the document had an invalid url, and thus it wasn't added to the background parser Q_ASSERT(d->m_managedTextDocumentUrls.contains(textDocument)); IndexedString url(d->m_managedTextDocumentUrls[textDocument]); QMutexLocker l2(&d->m_managedMutex); Q_ASSERT(d->m_managed.contains(url)); qCDebug(LANGUAGE) << "removing" << url.str() << "from background parser"; delete d->m_managed[url]; d->m_managedTextDocumentUrls.remove(textDocument); d->m_managed.remove(url); } } void BackgroundParser::documentLoaded( IDocument* document ) { QMutexLocker l(&d->m_mutex); if(document->textDocument() && document->textDocument()->url().isValid()) { KTextEditor::Document* textDocument = document->textDocument(); IndexedString url(document->url()); // Some debugging because we had issues with this QMutexLocker l2(&d->m_managedMutex); if(d->m_managed.contains(url) && d->m_managed[url]->document() == textDocument) { qCDebug(LANGUAGE) << "Got redundant documentLoaded from" << document->url() << textDocument; return; } qCDebug(LANGUAGE) << "Creating change tracker for " << document->url(); Q_ASSERT(!d->m_managed.contains(url)); Q_ASSERT(!d->m_managedTextDocumentUrls.contains(textDocument)); d->m_managedTextDocumentUrls[textDocument] = url; d->m_managed.insert(url, new DocumentChangeTracker(textDocument)); }else{ qCDebug(LANGUAGE) << "NOT creating change tracker for" << document->url(); } } void BackgroundParser::documentUrlChanged(IDocument* document) { documentClosed(document); // Only call documentLoaded if the file wasn't renamed to a filename that is already tracked. if(document->textDocument() && !trackerForUrl(IndexedString(document->textDocument()->url()))) documentLoaded(document); } void BackgroundParser::startTimer(int delay) { d->m_timer.start(delay); } void BackgroundParser::projectAboutToBeOpened(IProject* project) { d->m_loadingProjects.insert(project); } void BackgroundParser::projectOpened(IProject* project) { d->m_loadingProjects.remove(project); } void BackgroundParser::projectOpeningAborted(IProject* project) { d->m_loadingProjects.remove(project); } void BackgroundParser::updateProgressBar() { emit showProgress(this, 0, d->m_progressMax, d->m_progressDone); } diff --git a/plugins/appwizard/appwizardplugin.cpp b/plugins/appwizard/appwizardplugin.cpp index 8b10c4c80..e254eb9f0 100644 --- a/plugins/appwizard/appwizardplugin.cpp +++ b/plugins/appwizard/appwizardplugin.cpp @@ -1,552 +1,563 @@ /*************************************************************************** * Copyright 2001 Bernd Gehrmann * * Copyright 2004-2005 Sascha Cunz * * Copyright 2005 Ian Reinhart Geiser * * Copyright 2007 Alexander Dymo * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "appwizardplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "appwizarddialog.h" #include "projectselectionpage.h" #include "projectvcspage.h" #include "projecttemplatesmodel.h" #include "debug.h" using namespace KDevelop; Q_LOGGING_CATEGORY(PLUGIN_APPWIZARD, "kdevplatform.plugins.appwizard") K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin();) AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent) , m_templatesModel(nullptr) { setXMLFile(QStringLiteral("kdevappwizard.rc")); m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new")); m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); m_newFromTemplate->setText(i18n("New From Template...")); connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject); m_newFromTemplate->setToolTip( i18n("Generate a new project from a template") ); m_newFromTemplate->setWhatsThis( i18n("This starts KDevelop's application wizard. " "It helps you to generate a skeleton for your " "application from a set of templates.") ); } AppWizardPlugin::~AppWizardPlugin() { } void AppWizardPlugin::slotNewProject() { model()->refresh(); AppWizardDialog dlg(core()->pluginController(), m_templatesModel); if (dlg.exec() == QDialog::Accepted) { QString project = createProject( dlg.appInfo() ); if (!project.isEmpty()) { core()->projectController()->openProject(QUrl::fromLocalFile(project)); KConfig templateConfig(dlg.appInfo().appTemplate); KConfigGroup general(&templateConfig, "General"); const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), QString::SkipEmptyParts); for (const auto& fileArg : fileArgs) { QString file = KMacroExpander::expandMacros(fileArg.trimmed(), m_variables); if (QDir::isRelativePath(file)) { file = m_variables[QStringLiteral("PROJECTDIR")] + QLatin1Char('/') + file; } core()->documentController()->openDocument(QUrl::fromUserInput(file)); } } else { KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n("Could not create project from template\n"), i18n("Failed to create project") ); } } } namespace { IDistributedVersionControl* toDVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } ICentralizedVersionControl* toCVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } /*! Trouble while initializing version control. Show failure message to user. */ void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString()) { QString displayDetails = details; if (displayDetails.isEmpty()) { displayDetails = i18n("Please see the Version Control toolview"); } KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18n("Version Control System Error")); KIO::del(dest, KIO::HideProgressInfo)->exec(); tmpdir.remove(); } /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(dvcs); qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS"; const QUrl& dest = info.location; //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir VcsJob* job = dvcs->init(dest); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest); return false; } qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest; job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest); return false; } job = dvcs->commit(info.importCommitMessage, {dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString()); return false; } return true; // We're good } /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(cvcs); qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to" << info.repository.repositoryServer(); VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } qCDebug(PLUGIN_APPWIZARD) << "Checking out"; job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } return true; // initialization phase complete } QString generateIdentifier( const QString& appname ) { QString tmp = appname; QRegExp re("[^a-zA-Z0-9_]"); return tmp.replace(re, QStringLiteral("_")); } } // end anonymous namespace QString AppWizardPlugin::createProject(const ApplicationInfo& info) { QFileInfo templateInfo(info.appTemplate); if (!templateInfo.exists()) { qCWarning(PLUGIN_APPWIZARD) << "Project app template does not exist:" << info.appTemplate; return QString(); } QString templateName = templateInfo.baseName(); QString templateArchive; const QStringList filters = {templateName + QStringLiteral(".*")}; const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory); foreach(const QString& matchesPath, matchesPaths) { const QStringList files = QDir(matchesPath).entryList(filters); if(!files.isEmpty()) { templateArchive = matchesPath + files.first(); } } if(templateArchive.isEmpty()) { qCWarning(PLUGIN_APPWIZARD) << "Template name does not exist in the template list"; return QString(); } QUrl dest = info.location; //prepare variable substitution hash m_variables.clear(); m_variables[QStringLiteral("APPNAME")] = info.name; m_variables[QStringLiteral("APPNAMEUC")] = info.name.toUpper(); m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower(); m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name); m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile(); // backwards compatibility m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")]; m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName(); m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName; KArchive* arch = nullptr; if( templateArchive.endsWith(QLatin1String(".zip")) ) { arch = new KZip(templateArchive); } else { arch = new KTar(templateArchive, QStringLiteral("application/x-bzip")); } if (arch->open(QIODevice::ReadOnly)) { QTemporaryDir tmpdir; QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName ); if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension() ) ) { if( !QFileInfo::exists( dest.toLocalFile() ) ) { QDir::root().mkpath( dest.toLocalFile() ); } unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory } else { QUrl url = KIO::upUrl(dest); if(!QFileInfo::exists(url.toLocalFile())) { QDir::root().mkpath(url.toLocalFile()); } } // estimate metadata files which should not be copied QStringList metaDataFileNames; // try by same name const KArchiveEntry *templateEntry = arch->directory()->entry(templateName + QLatin1String(".kdevtemplate")); // but could be different name, if e.g. downloaded, so make a guess if (!templateEntry || !templateEntry->isFile()) { for (const auto& entryName : arch->directory()->entries()) { if (entryName.endsWith(QLatin1String(".kdevtemplate"))) { templateEntry = arch->directory()->entry(entryName); break; } } } if (templateEntry && templateEntry->isFile()) { metaDataFileNames << templateEntry->name(); // check if a preview file is to be ignored const KArchiveFile *templateFile = static_cast(templateEntry); QTemporaryDir temporaryDir; templateFile->copyTo(temporaryDir.path()); KConfig config(temporaryDir.path() + QLatin1Char('/') + templateEntry->name()); KConfigGroup group(&config, "General"); if (group.hasKey("Icon")) { const KArchiveEntry* iconEntry = arch->directory()->entry(group.readEntry("Icon")); if (iconEntry && iconEntry->isFile()) { metaDataFileNames << iconEntry->name(); } } } if (!unpackArchive(arch->directory(), unpackDir, metaDataFileNames)) { QString errorMsg = i18n("Could not create new project"); vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir)); return QString(); } if( !info.vcsPluginName.isEmpty() ) { if (!plugin) { // Red Alert, serious program corruption. // This should never happen, the vcs dialog presented a list of vcs // systems and now the chosen system doesn't exist anymore?? tmpdir.remove(); return QString(); } IDistributedVersionControl* dvcs = toDVCS(plugin); ICentralizedVersionControl* cvcs = toCVCS(plugin); bool success = false; if (dvcs) { success = initializeDVCS(dvcs, info, tmpdir); } else if (cvcs) { success = initializeCVCS(cvcs, info, tmpdir); } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel(nullptr, QStringLiteral("Failed to initialize version control system, " "plugin is neither VCS nor DVCS."))) success = true; } if (!success) return QString(); } tmpdir.remove(); }else { qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive"; return QString(); } QString projectFileName = QDir::cleanPath( dest.toLocalFile() + '/' + info.name + ".kdev4" ); // Loop through the new project directory and try to detect the first .kdev4 file. // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the // project templates can be more complex. QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories); if(it.hasNext() == true) { projectFileName = it.next(); } qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ; - if( ! QFileInfo::exists( projectFileName ) ) - { + const QFileInfo projectFileInfo(projectFileName); + if (!projectFileInfo.exists()) { qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file"; KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig ); KConfigGroup project = cfg->group( "Project" ); project.writeEntry( "Name", info.name ); QString manager = QStringLiteral("KDevGenericManager"); QDir d( dest.toLocalFile() ); auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); foreach(const KPluginMetaData& info, data) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); if (!filter.isEmpty()) { if (!d.entryList(filter).isEmpty()) { manager = info.pluginId(); break; } } } project.writeEntry( "Manager", manager ); project.sync(); cfg->sync(); KConfigGroup project2 = cfg->group( "Project" ); qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" ); } + // create developer .kde4 file + const QString developerProjectFileName = projectFileInfo.canonicalPath() + QLatin1String("/.kdev4/") + projectFileInfo.fileName(); + + qCDebug(PLUGIN_APPWIZARD) << "creating developer .kdev4 file:" << developerProjectFileName; + KSharedConfigPtr developerCfg = KSharedConfig::openConfig(developerProjectFileName, KConfig::SimpleConfig); + KConfigGroup developerProjectGroup = developerCfg->group("Project"); + developerProjectGroup.writeEntry("VersionControlSupport", info.vcsPluginName); + developerProjectGroup.sync(); + + developerCfg->sync(); + return projectFileName; } bool AppWizardPlugin::unpackArchive(const KArchiveDirectory* dir, const QString& dest, const QStringList& skipList) { qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest; const QStringList entries = dir->entries(); qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QStringLiteral(",")); //This extra tempdir is needed just for the files files have special names, //which may contain macros also files contain content with macros. So the //easiest way to extract the files from the archive and then rename them //and replace the macros is to use a tempdir and copy the file (and //replacing while copying). This also allows one to easily remove all files, //by just unlinking the tempdir QTemporaryDir tdir; bool ret = true; foreach (const QString& entry, entries) { if (skipList.contains(entry)) { continue; } if (dir->entry(entry)->isDirectory()) { const KArchiveDirectory *file = (KArchiveDirectory *)dir->entry(entry); QString newdest = dest + '/' + KMacroExpander::expandMacros(file->name(), m_variables); if( !QFileInfo::exists( newdest ) ) { QDir::root().mkdir( newdest ); } ret |= unpackArchive(file, newdest); } else if (dir->entry(entry)->isFile()) { const KArchiveFile *file = (KArchiveFile *)dir->entry(entry); file->copyTo(tdir.path()); QString destName = dest + '/' + file->name(); if (!copyFileAndExpandMacros(QDir::cleanPath(tdir.path()+'/'+file->name()), KMacroExpander::expandMacros(destName, m_variables))) { KMessageBox::sorry(nullptr, i18n("The file %1 cannot be created.", dest)); return false; } } } tdir.remove(); return ret; } bool AppWizardPlugin::copyFileAndExpandMacros(const QString &source, const QString &dest) { qCDebug(PLUGIN_APPWIZARD) << "copy:" << source << "to" << dest; QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(source); if( !mime.inherits(QStringLiteral("text/plain")) ) { KIO::CopyJob* job = KIO::copy( QUrl::fromUserInput(source), QUrl::fromUserInput(dest), KIO::HideProgressInfo ); if( !job->exec() ) { return false; } return true; } else { QFile inputFile(source); QFile outputFile(dest); if (inputFile.open(QFile::ReadOnly) && outputFile.open(QFile::WriteOnly)) { QTextStream input(&inputFile); input.setCodec(QTextCodec::codecForName("UTF-8")); QTextStream output(&outputFile); output.setCodec(QTextCodec::codecForName("UTF-8")); while(!input.atEnd()) { QString line = input.readLine(); output << KMacroExpander::expandMacros(line, m_variables) << "\n"; } #ifndef Q_OS_WIN // Preserve file mode... QT_STATBUF statBuf; QT_FSTAT(inputFile.handle(), &statBuf); // Unix only, won't work in Windows, maybe KIO::chmod could be used ::fchmod(outputFile.handle(), statBuf.st_mode); #endif return true; } else { inputFile.close(); outputFile.close(); return false; } } } KDevelop::ContextMenuExtension AppWizardPlugin::contextMenuExtension(KDevelop::Context* context) { KDevelop::ContextMenuExtension ext; if ( context->type() != KDevelop::Context::ProjectItemContext || !static_cast(context)->items().isEmpty() ) { return ext; } ext.addAction(KDevelop::ContextMenuExtension::ProjectGroup, m_newFromTemplate); return ext; } ProjectTemplatesModel* AppWizardPlugin::model() { if(!m_templatesModel) m_templatesModel = new ProjectTemplatesModel(this); return m_templatesModel; } QAbstractItemModel* AppWizardPlugin::templatesModel() { return model(); } QString AppWizardPlugin::knsConfigurationFile() const { return QStringLiteral("kdevappwizard.knsrc"); } QStringList AppWizardPlugin::supportedMimeTypes() const { QStringList types; types << QStringLiteral("application/x-desktop"); types << QStringLiteral("application/x-bzip-compressed-tar"); types << QStringLiteral("application/zip"); return types; } QIcon AppWizardPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("project-development-new-template")); } QString AppWizardPlugin::name() const { return i18n("Project Templates"); } void AppWizardPlugin::loadTemplate(const QString& fileName) { model()->loadTemplateFile(fileName); } void AppWizardPlugin::reload() { model()->refresh(); } #include "appwizardplugin.moc" diff --git a/project/abstractfilemanagerplugin.cpp b/project/abstractfilemanagerplugin.cpp index e603aa676..0f0372f6c 100644 --- a/project/abstractfilemanagerplugin.cpp +++ b/project/abstractfilemanagerplugin.cpp @@ -1,662 +1,662 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2010-2012 Milian Wolff * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "abstractfilemanagerplugin.h" #include "filemanagerlistjob.h" #include "projectmodel.h" #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include "projectfiltermanager.h" #include "debug.h" #define ifDebug(x) using namespace KDevelop; //BEGIN Helper namespace { /** * Returns the parent folder item for a given item or the project root item if there is no parent. */ ProjectFolderItem* getParentFolder(ProjectBaseItem* item) { if ( item->parent() ) { return static_cast(item->parent()); } else { return item->project()->projectItem(); } } } //END Helper //BEGIN Private struct AbstractFileManagerPlugin::Private { explicit Private(AbstractFileManagerPlugin* qq) : q(qq) { } AbstractFileManagerPlugin* q; /** * The just returned must be started in one way or another for this method * to have any affect. The job will then auto-delete itself upon completion. */ KIO::Job* eventuallyReadFolder( ProjectFolderItem* item ) Q_REQUIRED_RESULT; void addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries); void deleted(const QString &path); void created(const QString &path); void projectClosing(IProject* project); void jobFinished(KJob* job); /// Stops watching the given folder for changes, only useful for local files. void stopWatcher(ProjectFolderItem* folder); /// Continues watching the given folder for changes. void continueWatcher(ProjectFolderItem* folder); /// Common renaming function. bool rename(ProjectBaseItem* item, const Path& newPath); void removeFolder(ProjectFolderItem* folder); QHash m_watchers; QHash > m_projectJobs; QVector m_stoppedFolders; ProjectFilterManager m_filters; }; void AbstractFileManagerPlugin::Private::projectClosing(IProject* project) { if ( m_projectJobs.contains(project) ) { // make sure the import job does not live longer than the project // see also addLotsOfFiles test foreach( FileManagerListJob* job, m_projectJobs[project] ) { qCDebug(FILEMANAGER) << "killing project job:" << job; job->abort(); } m_projectJobs.remove(project); } delete m_watchers.take(project); m_filters.remove(project); } KIO::Job* AbstractFileManagerPlugin::Private::eventuallyReadFolder( ProjectFolderItem* item ) { FileManagerListJob* listJob = new FileManagerListJob( item ); m_projectJobs[ item->project() ] << listJob; qCDebug(FILEMANAGER) << "adding job" << listJob << item << item->path() << "for project" << item->project(); q->connect( listJob, &FileManagerListJob::finished, q, [&] (KJob* job) { jobFinished(job); } ); q->connect( listJob, &FileManagerListJob::entries, q, [&] (FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { addJobItems(job, baseItem, entries); } ); return listJob; } void AbstractFileManagerPlugin::Private::jobFinished(KJob* job) { FileManagerListJob* gmlJob = qobject_cast(job); if (gmlJob) { ifDebug(qCDebug(FILEMANAGER) << job << gmlJob << gmlJob->item();) m_projectJobs[ gmlJob->item()->project() ].removeOne( gmlJob ); } else { // job emitted its finished signal from its destructor // ensure we don't keep a dangling point in our list foreach (auto jobs, m_projectJobs) { if (jobs.removeOne(reinterpret_cast(job))) { break; } } } } void AbstractFileManagerPlugin::Private::addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { if ( entries.empty() ) { return; } qCDebug(FILEMANAGER) << "reading entries of" << baseItem->path(); // build lists of valid files and folders with paths relative to the project folder Path::List files; Path::List folders; foreach ( const KIO::UDSEntry& entry, entries ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if (name == QLatin1String(".") || name == QLatin1String("..")) { continue; } Path path(baseItem->path(), name); if ( !q->isValid( path, entry.isDir(), baseItem->project() ) ) { continue; } else { if ( entry.isDir() ) { if( entry.isLink() ) { const Path linkedPath = baseItem->path().cd(entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST )); // make sure we don't end in an infinite loop if( linkedPath.isParentOf( baseItem->project()->path() ) || baseItem->project()->path().isParentOf( linkedPath ) || linkedPath == baseItem->project()->path() ) { continue; } } folders << path; } else { files << path; } } } ifDebug(qCDebug(FILEMANAGER) << "valid folders:" << folders;) ifDebug(qCDebug(FILEMANAGER) << "valid files:" << files;) // remove obsolete rows for ( int j = 0; j < baseItem->rowCount(); ++j ) { if ( ProjectFolderItem* f = baseItem->child(j)->folder() ) { // check if this is still a valid folder int index = folders.indexOf( f->path() ); if ( index == -1 ) { // folder got removed or is now invalid removeFolder(f); --j; } else { // this folder already exists in the view folders.remove( index ); // no need to add this item, but we still want to recurse into it job->addSubDir( f ); emit q->reloadedFolderItem( f ); } } else if ( ProjectFileItem* f = baseItem->child(j)->file() ) { // check if this is still a valid file int index = files.indexOf( f->path() ); if ( index == -1 ) { // file got removed or is now invalid ifDebug(qCDebug(FILEMANAGER) << "removing file:" << f << f->path();) baseItem->removeRow( j ); --j; } else { // this file already exists in the view files.remove( index ); emit q->reloadedFileItem( f ); } } } // add new rows foreach ( const Path& path, files ) { ProjectFileItem* file = q->createFileItem( baseItem->project(), path, baseItem ); if (file) { emit q->fileAdded( file ); } } foreach ( const Path& path, folders ) { ProjectFolderItem* folder = q->createFolderItem( baseItem->project(), path, baseItem ); if (folder) { emit q->folderAdded( folder ); job->addSubDir( folder ); } } } void AbstractFileManagerPlugin::Private::created(const QString &path_) { qCDebug(FILEMANAGER) << "created:" << path_; QFileInfo info(path_); ///FIXME: share memory with parent const Path path(path_); const IndexedString indexedPath(path.pathOrUrl()); const IndexedString indexedParent(path.parent().pathOrUrl()); QHashIterator it(m_watchers); while (it.hasNext()) { const auto p = it.next().key(); if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } if ( !q->isValid(path, info.isDir(), p) ) { continue; } if ( info.isDir() ) { bool found = false; foreach ( ProjectFolderItem* folder, p->foldersForPath(indexedPath) ) { // exists already in this project, happens e.g. when we restart the dirwatcher // or if we delete and remove folders consecutively https://bugs.kde.org/show_bug.cgi?id=260741 qCDebug(FILEMANAGER) << "force reload of" << path << folder; auto job = eventuallyReadFolder( folder ); job->start(); found = true; } if ( found ) { continue; } } else if (!p->filesForPath(indexedPath).isEmpty()) { // also gets triggered for kate's backup files continue; } foreach ( ProjectFolderItem* parentItem, p->foldersForPath(indexedParent) ) { if ( info.isDir() ) { ProjectFolderItem* folder = q->createFolderItem( p, path, parentItem ); if (folder) { emit q->folderAdded( folder ); auto job = eventuallyReadFolder( folder ); job->start(); } } else { ProjectFileItem* file = q->createFileItem( p, path, parentItem ); if (file) { emit q->fileAdded( file ); } } } } } void AbstractFileManagerPlugin::Private::deleted(const QString &path_) { if ( QFile::exists(path_) ) { // stopDirScan... return; } // ensure that the path is not inside a stopped folder foreach(const QString& folder, m_stoppedFolders) { if (path_.startsWith(folder)) { return; } } qCDebug(FILEMANAGER) << "deleted:" << path_; - const Path path(path_); + const Path path(QUrl::fromLocalFile(path_)); const IndexedString indexed(path.pathOrUrl()); QHashIterator it(m_watchers); while (it.hasNext()) { const auto p = it.next().key(); if (path == p->path()) { KMessageBox::error(qApp->activeWindow(), i18n("The base folder of project %1" " got deleted or moved outside of KDevelop.\n" "The project has to be closed.", p->name()), i18n("Project Folder Deleted") ); ICore::self()->projectController()->closeProject(p); continue; } if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } foreach ( ProjectFolderItem* item, p->foldersForPath(indexed) ) { removeFolder(item); } foreach ( ProjectFileItem* item, p->filesForPath(indexed) ) { emit q->fileRemoved(item); ifDebug(qCDebug(FILEMANAGER) << "removing file" << item;) item->parent()->removeRow(item->row()); } } } bool AbstractFileManagerPlugin::Private::rename(ProjectBaseItem* item, const Path& newPath) { if ( !q->isValid(newPath, true, item->project()) ) { int cancel = KMessageBox::warningContinueCancel( qApp->activeWindow(), i18n("You tried to rename '%1' to '%2', but the latter is filtered and will be hidden.\n" "Do you want to continue?", item->text(), newPath.lastPathSegment()), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("GenericManagerRenameToFiltered") ); if ( cancel == KMessageBox::Cancel ) { return false; } } foreach ( ProjectFolderItem* parent, item->project()->foldersForPath(IndexedString(newPath.parent().pathOrUrl())) ) { if ( parent->folder() ) { stopWatcher(parent); const Path source = item->path(); bool success = renameUrl( item->project(), source.toUrl(), newPath.toUrl() ); if ( success ) { item->setPath( newPath ); item->parent()->takeRow( item->row() ); parent->appendRow( item ); if (item->file()) { emit q->fileRenamed(source, item->file()); } else { Q_ASSERT(item->folder()); emit q->folderRenamed(source, item->folder()); } } continueWatcher(parent); return success; } } return false; } void AbstractFileManagerPlugin::Private::stopWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->stopDirScan(path); m_stoppedFolders.append(path); } void AbstractFileManagerPlugin::Private::continueWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->restartDirScan(path); const int idx = m_stoppedFolders.indexOf(path); if (idx != -1) { m_stoppedFolders.remove(idx); } } bool isChildItem(ProjectBaseItem* parent, ProjectBaseItem* child) { do { if (child == parent) { return true; } child = child->parent(); } while(child); return false; } void AbstractFileManagerPlugin::Private::removeFolder(ProjectFolderItem* folder) { ifDebug(qCDebug(FILEMANAGER) << "removing folder:" << folder << folder->path();) foreach(FileManagerListJob* job, m_projectJobs[folder->project()]) { if (isChildItem(folder, job->item())) { qCDebug(FILEMANAGER) << "killing list job for removed folder" << job << folder->path(); job->abort(); Q_ASSERT(!m_projectJobs.value(folder->project()).contains(job)); } else { job->removeSubDir(folder); } } folder->parent()->removeRow( folder->row() ); } //END Private //BEGIN Plugin AbstractFileManagerPlugin::AbstractFileManagerPlugin( const QString& componentName, QObject *parent, const QVariantList & /*args*/ ) : IProjectFileManager(), IPlugin( componentName, parent ), d(new Private(this)) { connect(core()->projectController(), &IProjectController::projectClosing, this, [&] (IProject* project) { d->projectClosing(project); }); } AbstractFileManagerPlugin::~AbstractFileManagerPlugin() { delete d; } IProjectFileManager::Features AbstractFileManagerPlugin::features() const { return Features( Folders | Files ); } QList AbstractFileManagerPlugin::parse( ProjectFolderItem *item ) { // we are async, can't return anything here qCDebug(FILEMANAGER) << "note: parse will always return an empty list"; Q_UNUSED(item); return QList(); } ProjectFolderItem *AbstractFileManagerPlugin::import( IProject *project ) { ProjectFolderItem *projectRoot = createFolderItem( project, project->path(), nullptr ); emit folderAdded( projectRoot ); qCDebug(FILEMANAGER) << "imported new project" << project->name() << "at" << projectRoot->path(); ///TODO: check if this works for remote files when something gets changed through another KDE app if ( project->path().isLocalFile() ) { d->m_watchers[project] = new KDirWatch( project ); connect(d->m_watchers[project], &KDirWatch::created, this, [&] (const QString& path_) { d->created(path_); }); connect(d->m_watchers[project], &KDirWatch::deleted, this, [&] (const QString& path_) { d->deleted(path_); }); d->m_watchers[project]->addDir(project->path().toLocalFile(), KDirWatch::WatchSubDirs | KDirWatch:: WatchFiles ); } d->m_filters.add(project); return projectRoot; } KJob* AbstractFileManagerPlugin::createImportJob(ProjectFolderItem* item) { return d->eventuallyReadFolder(item); } bool AbstractFileManagerPlugin::reload( ProjectFolderItem* item ) { qCDebug(FILEMANAGER) << "reloading item" << item->path(); auto job = d->eventuallyReadFolder( item->folder() ); job->start(); return true; } ProjectFolderItem* AbstractFileManagerPlugin::addFolder( const Path& folder, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding folder" << folder << "to" << parent->path(); ProjectFolderItem* created = nullptr; d->stopWatcher(parent); if ( createFolder(folder.toUrl()) ) { created = createFolderItem( parent->project(), folder, parent ); if (created) { emit folderAdded(created); } } d->continueWatcher(parent); return created; } ProjectFileItem* AbstractFileManagerPlugin::addFile( const Path& file, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding file" << file << "to" << parent->path(); ProjectFileItem* created = nullptr; d->stopWatcher(parent); if ( createFile(file.toUrl()) ) { created = createFileItem( parent->project(), file, parent ); if (created) { emit fileAdded(created); } } d->continueWatcher(parent); return created; } bool AbstractFileManagerPlugin::renameFolder(ProjectFolderItem* folder, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a folder:" << folder->path() << newPath; return d->rename(folder, newPath); } bool AbstractFileManagerPlugin::renameFile(ProjectFileItem* file, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a file:" << file->path() << newPath; return d->rename(file, newPath); } bool AbstractFileManagerPlugin::removeFilesAndFolders(const QList &items) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* parent = getParentFolder(item); d->stopWatcher(parent); success &= removeUrl(parent->project(), item->path().toUrl(), true); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { Q_ASSERT(item->folder()); emit folderRemoved(item->folder()); } item->parent()->removeRow( item->row() ); } d->continueWatcher(parent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::moveFilesAndFolders(const QList< ProjectBaseItem* >& items, ProjectFolderItem* newParent) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* oldParent = getParentFolder(item); d->stopWatcher(oldParent); d->stopWatcher(newParent); const Path oldPath = item->path(); const Path newPath(newParent->path(), item->baseName()); success &= renameUrl(oldParent->project(), oldPath.toUrl(), newPath. toUrl()); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { emit folderRemoved(item->folder()); } oldParent->removeRow( item->row() ); KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(oldParent); d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::copyFilesAndFolders(const Path::List& items, ProjectFolderItem* newParent) { bool success = true; foreach(const Path& item, items) { d->stopWatcher(newParent); success &= copyUrl(newParent->project(), item.toUrl(), newParent->path().toUrl()); if ( success ) { KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::isValid( const Path& path, const bool isFolder, IProject* project ) const { return d->m_filters.isValid( path, isFolder, project ); } ProjectFileItem* AbstractFileManagerPlugin::createFileItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFileItem( project, path, parent ); } ProjectFolderItem* AbstractFileManagerPlugin::createFolderItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFolderItem( project, path, parent ); } KDirWatch* AbstractFileManagerPlugin::projectWatcher( IProject* project ) const { return d->m_watchers.value( project, nullptr ); } //END Plugin #include "moc_abstractfilemanagerplugin.cpp" diff --git a/project/helper.cpp b/project/helper.cpp index b63b6f8dd..4bc71b53d 100644 --- a/project/helper.cpp +++ b/project/helper.cpp @@ -1,232 +1,234 @@ /* This file is part of KDevelop Copyright 2010 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "debug.h" #include "helper.h" #include "path.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; bool KDevelop::removeUrl(const KDevelop::IProject* project, const QUrl& url, const bool isFolder) { + qCDebug(PROJECT) << "Removing url:" << url << "from project" << project; + QWidget* window = QApplication::activeWindow(); auto job = KIO::stat(url, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(job, window); if (!job->exec()) { qCWarning(PROJECT) << "tried to remove non-existing url:" << url << project << isFolder; return true; } IPlugin* vcsplugin=project->versionControlPlugin(); if(vcsplugin) { IBasicVersionControl* vcs=vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if(vcs->isVersionControlled(url)) { VcsJob* job=vcs->remove(QList() << url); if(job) { return job->exec(); } } } //if we didn't find a VCS, we remove using KIO (if the file still exists, the vcs plugin might have simply deleted the url without returning a job auto deleteJob = KIO::del(url); KJobWidgets::setWindow(deleteJob, window); if (!deleteJob->exec() && url.isLocalFile() && (QFileInfo::exists(url.toLocalFile()))) { KMessageBox::error( window, isFolder ? i18n( "Cannot remove folder %1.", url.toDisplayString(QUrl::PreferLocalFile) ) : i18n( "Cannot remove file %1.", url.toDisplayString(QUrl::PreferLocalFile) ) ); return false; } return true; } bool KDevelop::removePath(const KDevelop::IProject* project, const KDevelop::Path& path, const bool isFolder) { return removeUrl(project, path.toUrl(), isFolder); } bool KDevelop::createFile(const QUrl& file) { auto statJob = KIO::stat(file, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { KMessageBox::error( QApplication::activeWindow(), i18n( "The file %1 already exists.", file.toDisplayString(QUrl::PreferLocalFile) ) ); return false; } { auto uploadJob = KIO::storedPut(QByteArray("\n"), file, -1); KJobWidgets::setWindow(uploadJob, QApplication::activeWindow()); if (!uploadJob->exec()) { KMessageBox::error( QApplication::activeWindow(), i18n( "Cannot create file %1.", file.toDisplayString(QUrl::PreferLocalFile) ) ); return false; } } return true; } bool KDevelop::createFile(const KDevelop::Path& file) { return createFile(file.toUrl()); } bool KDevelop::createFolder(const QUrl& folder) { auto mkdirJob = KIO::mkdir(folder); KJobWidgets::setWindow(mkdirJob, QApplication::activeWindow()); if (!mkdirJob->exec()) { KMessageBox::error( QApplication::activeWindow(), i18n( "Cannot create folder %1.", folder.toDisplayString(QUrl::PreferLocalFile) ) ); return false; } return true; } bool KDevelop::createFolder(const KDevelop::Path& folder) { return createFolder(folder.toUrl()); } bool KDevelop::renameUrl(const KDevelop::IProject* project, const QUrl& oldname, const QUrl& newname) { bool wasVcsMoved = false; IPlugin* vcsplugin = project->versionControlPlugin(); if (vcsplugin) { IBasicVersionControl* vcs = vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if (vcs->isVersionControlled(oldname)) { VcsJob* job = vcs->move(oldname, newname); if (job && !job->exec()) { return false; } wasVcsMoved = true; } } // Fallback for the case of no vcs, or not-vcs-managed file/folder // try to save-as the text document, so users can directly continue to work // on the renamed url as well as keeping the undo-stack intact IDocument* document = ICore::self()->documentController()->documentForUrl(oldname); if (document && document->textDocument()) { if (!document->textDocument()->saveAs(newname)) { return false; } if (!wasVcsMoved) { // unlink the old file removeUrl(project, oldname, false); } return true; } else if (!wasVcsMoved) { // fallback for non-textdocuments (also folders e.g.) KIO::CopyJob* job = KIO::move(oldname, newname); KJobWidgets::setWindow(job, QApplication::activeWindow()); bool success = job->exec(); if (success) { // save files that where opened in this folder under the new name Path oldBasePath(oldname); Path newBasePath(newname); foreach (auto doc, ICore::self()->documentController()->openDocuments()) { auto textDoc = doc->textDocument(); if (textDoc && oldname.isParentOf(doc->url())) { const auto path = Path(textDoc->url()); const auto relativePath = oldBasePath.relativePath(path); const auto newPath = Path(newBasePath, relativePath); textDoc->saveAs(newPath.toUrl()); } } } return success; } else { return true; } } bool KDevelop::renamePath(const KDevelop::IProject* project, const KDevelop::Path& oldName, const KDevelop::Path& newName) { return renameUrl(project, oldName.toUrl(), newName.toUrl()); } bool KDevelop::copyUrl(const KDevelop::IProject* project, const QUrl& source, const QUrl& target) { IPlugin* vcsplugin=project->versionControlPlugin(); if(vcsplugin) { IBasicVersionControl* vcs=vcsplugin->extension(); // We have a vcs and the file/folder is controller, need to make the rename through vcs if(vcs->isVersionControlled(source)) { VcsJob* job=vcs->copy(source, target); if(job) { return job->exec(); } } } // Fallback for the case of no vcs, or not-vcs-managed file/folder auto job = KIO::copy(source, target); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } bool KDevelop::copyPath(const KDevelop::IProject* project, const KDevelop::Path& source, const KDevelop::Path& target) { return copyUrl(project, source.toUrl(), target.toUrl()); } Path KDevelop::proposedBuildFolder(const Path& sourceFolder) { Path proposedBuildFolder; if (sourceFolder.path().contains(QStringLiteral("/src/"))) { const QString srcBuildPath = sourceFolder.path().replace(QStringLiteral("/src/"), QStringLiteral("/build/")); Q_ASSERT(!srcBuildPath.isEmpty()); if (QDir(srcBuildPath).exists()) { proposedBuildFolder = Path(srcBuildPath); } } if (!proposedBuildFolder.isValid()) { proposedBuildFolder = Path(sourceFolder, QStringLiteral("build")); } return proposedBuildFolder; }