diff --git a/interfaces/configpage.cpp b/interfaces/configpage.cpp index 181fc29ed..a114769ef 100644 --- a/interfaces/configpage.cpp +++ b/interfaces/configpage.cpp @@ -1,129 +1,129 @@ /* * This file is part of KDevelop * Copyright 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "configpage.h" #include #include namespace KDevelop { struct ConfigPagePrivate { explicit ConfigPagePrivate(IPlugin* plugin) : plugin(plugin) {} QScopedPointer configManager; KCoreConfigSkeleton* configSkeleton = nullptr; IPlugin* plugin; }; ConfigPage::ConfigPage(IPlugin* plugin, KCoreConfigSkeleton* config, QWidget* parent) : KTextEditor::ConfigPage(parent) , d(new ConfigPagePrivate(plugin)) { setConfigSkeleton(config); } ConfigPage::~ConfigPage() { } void ConfigPage::apply() { - // if d->configManager is null, this method must be overriden + // if d->configManager is null, this method must be overridden Q_ASSERT_X(d->configManager, metaObject()->className(), "Config page does not use a KConfigSkeleton, but doesn't override apply()"); QSignalBlocker blockSigs(this); // we don't want to emit changed() while calling apply() d->configManager->updateSettings(); d->configSkeleton->load(); d->configManager->updateWidgets(); } void ConfigPage::defaults() { - // if d->configManager is null, this method must be overriden + // if d->configManager is null, this method must be overridden Q_ASSERT_X(d->configManager, metaObject()->className(), "Config page does not use a KConfigSkeleton, but doesn't override defaults()"); d->configManager->updateWidgetsDefault(); } void ConfigPage::reset() { - // if d->configManager is null, this method must be overriden + // if d->configManager is null, this method must be overridden Q_ASSERT_X(d->configManager, metaObject()->className(), "Config page does not use a KConfigSkeleton, but doesn't override reset()"); d->configManager->updateWidgets(); } void ConfigPage::initConfigManager() { if (d->configManager) { d->configManager->addWidget(this); } } KCoreConfigSkeleton* ConfigPage::configSkeleton() const { return d->configSkeleton; } void ConfigPage::setConfigSkeleton(KCoreConfigSkeleton* skel) { if (d->configSkeleton == skel) { return; } d->configSkeleton = skel; if (!skel) { d->configManager.reset(); return; } // create the config dialog manager if it didn't exist or recreate it. // This is needed because the used config skeleton has changed // and no setter for that exists in KConfigDialogManager d->configManager.reset(new KConfigDialogManager(this, d->configSkeleton)); connect(d->configManager.data(), &KConfigDialogManager::widgetModified, this, &ConfigPage::changed); // d->configManager->addWidget(this) must be called from the config dialog, // since the widget tree is probably not complete when calling this function } int ConfigPage::childPages() const { return 0; } ConfigPage* ConfigPage::childPage(int number) { Q_UNUSED(number) return nullptr; } IPlugin* ConfigPage::plugin() const { return d->plugin; } ConfigPage::ConfigPageType ConfigPage::configPageType() const { return DefaultConfigPage; } } // namespace KDevelop diff --git a/interfaces/iplugin.h b/interfaces/iplugin.h index f2d870da2..6f56b2a1f 100644 --- a/interfaces/iplugin.h +++ b/interfaces/iplugin.h @@ -1,280 +1,280 @@ /* This file is part of the KDE project Copyright 1999-2001 Bernd Gehrmann Copyright 2004,2007 Alexander Dymo Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat 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. */ #ifndef KDEVPLATFORM_IPLUGIN_H #define KDEVPLATFORM_IPLUGIN_H #include #include #include "interfacesexport.h" namespace Sublime { class MainWindow; } /** * This macro adds an extension interface to register with the extension manager * Call this macro for all interfaces your plugin implements in its constructor */ #define KDEV_USE_EXTENSION_INTERFACE(Extension) \ _Pragma("message(\"Using deprecated function: KDEV_USE_EXTENSION_INTERFACE. Just remove the use of it.\")") namespace KDevelop { class ICore; class ConfigPage; class Context; class ContextMenuExtension; struct ProjectConfigOptions; /** * The base class for all KDevelop plugins. * * Plugin is a component which is loaded into KDevelop shell at startup or by * request. Each plugin should have corresponding .desktop file with a * description. The .desktop file template looks like: * @code * [Desktop Entry] * Type=Service * Exec=blubb * Name= * GenericName= * Comment= * Icon= * ServiceTypes=KDevelop/Plugin * X-KDE-Library= * X-KDE-PluginInfo-Name= * X-KDE-PluginInfo-Author= * X-KDE-PluginInfo-Version= * X-KDE-PluginInfo-License= * X-KDE-PluginInfo-Category= * X-KDevelop-Version= * X-KDevelop-Category= * X-KDevelop-Mode=GUI * X-KDevelop-LoadMode= * X-KDevelop-Languages= * X-KDevelop-SupportedMimeTypes= * X-KDevelop-Interfaces= * X-KDevelop-IOptional= * X-KDevelop-IRequired= * @endcode * Description of parameters in .desktop file: * - Name is a translatable name of a plugin, it is used in the plugin * selection list (required); * - GenericName is a translatable generic name of a plugin, it should * describe in general what the plugin can do (required); * - Comment is a short description about the plugin (optional); * - Icon is a plugin icon (preferred); * X-KDE-librarythis is the name of the .so file to load for this plugin (required); * - X-KDE-PluginInfo-Name is a non-translatable user-readable plugin * identifier used in KTrader queries (required); * - X-KDE-PluginInfo-Author is a non-translateable name of the plugin * author (optional); * - X-KDE-PluginInfo-Version is version number of the plugin (optional); * - X-KDE-PluginInfo-License is a license (optional). can be: GPL, * LGPL, BSD, Artistic, QPL or Custom. If this property is not set, license is * considered as unknown; * - X-KDE-PluginInfo-Category is used to categorize plugins (optional). can be: * Core, Project Management, Version Control, Utilities, Documentation, * Language Support, Debugging, Other * If this property is not set, "Other" is assumed * - X-KDevelop-Version is the KDevPlatform API version this plugin * works with (required); * - X-KDevelop-Interfaces is a list of extension interfaces that this * plugin implements (optional); * - X-KDevelop-IRequired is a list of extension interfaces that this * plugin depends on (optional); A list entry can also be of the form @c interface@pluginname, * in which case a plugin of the given name is required which implements the interface. * - X-KDevelop-IOptional is a list of extension interfaces that this * plugin will use if they are available (optional); * - X-KDevelop-Languages is a list of the names of the languages the plugin provides * support for (optional); * - X-KDevelop-SupportedMimeTypes is a list of mimetypes that the * language-parser in this plugin supports (optional); * - X-KDevelop-Mode is either GUI or NoGUI to indicate whether a plugin can run * with the GUI components loaded or not (required); * - X-KDevelop-Category is a scope of a plugin (see below for * explanation) (required); * - X-KDevelop-LoadMode can be set to AlwaysOn in which case the plugin will * never be unloaded even if requested via the API. (optional); * * Plugin scope can be either: * - Global * - Project * . * Global plugins are plugins which require only the shell to be loaded and do not operate on * the Project interface and/or do not use project wide information.\n * Core plugins are global plugins which offer some important "core" functionality and thus * are not selectable by user in plugin configuration pages.\n * Project plugins require a project to be loaded and are usually loaded/unloaded along with * the project. * If your plugin uses the Project interface and/or operates on project-related * information then this is a project plugin. * * * @sa Core class documentation for information about features available to * plugins from shell applications. */ class KDEVPLATFORMINTERFACES_EXPORT IPlugin: public QObject, public KXMLGUIClient { Q_OBJECT public: /**Constructs a plugin. * @param componentName The component name for this plugin. * @param parent The parent object for the plugin. */ IPlugin(const QString &componentName, QObject *parent); /**Destructs a plugin.*/ ~IPlugin() override; /** * Signal the plugin that it should cleanup since it will be unloaded soon. */ Q_SCRIPTABLE virtual void unload(); /** * Provides access to the ICore implementation */ Q_SCRIPTABLE ICore *core() const; /** * Convenience API to access an interface inherited by this plugin * * @return Instance to the specified interface, or nullptr */ template inline Extension* extension() { return qobject_cast(this); } /** * Ask the plugin for a ContextActionContainer, which contains actions * that will be merged into the context menu. * @param context the context describing where the context menu was requested * @returns a container describing which actions to merge into which context menu part */ virtual ContextMenuExtension contextMenuExtension( KDevelop::Context* context ); /** * Can create a new KXMLGUIClient, and set it up correctly with all the plugins per-window GUI actions. * * The caller owns the created object, including all contained actions. The object is destroyed as soon as * the mainwindow is closed. * * The default implementation calls the convenience function @ref createActionsForMainWindow and uses it to fill a custom KXMLGUIClient. * * Only override this version if you need more specific features of KXMLGUIClient, or other special per-window handling. * * @param window The main window the actions are created for */ virtual KXMLGUIClient* createGUIForMainWindow( Sublime::MainWindow* window ); /** * This function allows allows setting up plugin actions conveniently. Unless createGUIForMainWindow was overridden, * this is called for each new mainwindow, and the plugin should add its actions to @p actions, and write its KXMLGui xml file * into @p xmlFile. * * @param window The main window the actions are created for * @param xmlFile Change this to the xml file that needed for merging the actions into the GUI * @param actions Add your actions here. A new set of actions has to be created for each mainwindow. */ virtual void createActionsForMainWindow( Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions ); /** * This function is necessary because there is no proper way to signal errors from plugin constructor. * @returns True if the plugin has encountered an error (and therefore has an error description), * false otherwise. */ bool hasError() const; /** * Description of the last encountered error, of an empty string if none. */ QString errorDescription() const; /** - * Set a @p description of the errror encountered. An empty error + * Set a @p description of the error encountered. An empty error * description implies no error in the plugin. */ void setErrorDescription(QString const& description); /** * Get the global config page with the \p number, config pages from 0 to * configPages()-1 are available if configPages() > 0. * * @param number index of config page * @param parent parent widget for config page * @return newly created config page or NULL, if the number is out of bounds, default implementation returns NULL. * This config page should inherit from ProjectConfigPage, but it is not a strict requirement. * The default implementation returns @c nullptr. * @see perProjectConfigPages(), ProjectConfigPage */ virtual ConfigPage* configPage(int number, QWidget *parent); /** * Get the number of available config pages for global settings. * @return number of global config pages. The default implementation returns zero. * @see configPage() */ virtual int configPages() const; /** * Get the number of available config pages for per project settings. * @return number of per project config pages. The default implementation returns zero. * @see perProjectConfigPage() */ virtual int perProjectConfigPages() const; /** * Get the per project config page with the \p number, config pages from 0 to * perProjectConfigPages()-1 are available if perProjectConfigPages() > 0. * * @param number index of config page * @param options The options used to initialize the ProjectConfigPage * @param parent parent widget for config page * @return newly created config page or NULL, if the number is out of bounds, default implementation returns NULL. * This config page should inherit from ProjectConfigPage, but it is not a strict requirement. * The default implementation returns @c nullptr. * @see perProjectConfigPages(), ProjectConfigPage */ virtual ConfigPage* perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent); protected: /** * Initialize the XML GUI State. */ virtual void initializeGuiState(); private: friend class IPluginPrivate; class IPluginPrivate* const d; }; } #endif diff --git a/language/backgroundparser/backgroundparser.cpp b/language/backgroundparser/backgroundparser.cpp index 4794e6c94..8cd7f4588 100644 --- a/language/backgroundparser/backgroundparser.cpp +++ b/language/backgroundparser/backgroundparser.cpp @@ -1,916 +1,916 @@ /* * 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 #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" #include 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 dont need a widget context + * - 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())) { 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 : 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) ) { qWarning() << "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/language/backgroundparser/documentchangetracker.cpp b/language/backgroundparser/documentchangetracker.cpp index e1014a570..e78fe2cd5 100644 --- a/language/backgroundparser/documentchangetracker.cpp +++ b/language/backgroundparser/documentchangetracker.cpp @@ -1,468 +1,468 @@ /* * This file is part of KDevelop * * Copyright 2010 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 "documentchangetracker.h" #include #include #include #include #include #include #include #include "backgroundparser.h" #include "util/debug.h" #include // Can be used to disable the 'clever' updating logic that ignores whitespace-only changes and such. // #define ALWAYS_UPDATE using namespace KTextEditor; /** * @todo Track the exact changes to the document, and then: - * Dont reparse if: + * Do not reparse if: * - Comment added/changed * - Newlines added/changed (ready) * Complete the document for validation: * - Incomplete for-loops * - ... * Only reparse after a statement was completed (either with statement-completion or manually), or after the cursor was switched away * Incremental parsing: * - All changes within a local function (or function parameter context): Update only the context (and all its importers) * * @todo: Prevent recursive updates after insignificant changes * (whitespace changes, or changes that don't affect publically visible stuff, eg. local incremental changes) * -> Maybe alter the file-modification caches directly * */ namespace KDevelop { DocumentChangeTracker::DocumentChangeTracker( KTextEditor::Document* document ) : m_needUpdate(false) , m_document(document) , m_moving(nullptr) , m_url(IndexedString(document->url())) { Q_ASSERT(document); Q_ASSERT(document->url().isValid()); connect(document, &Document::textInserted, this, &DocumentChangeTracker::textInserted); connect(document, &Document::lineWrapped, this, &DocumentChangeTracker::lineWrapped); connect(document, &Document::lineUnwrapped, this, &DocumentChangeTracker::lineUnwrapped); connect(document, &Document::textRemoved, this, &DocumentChangeTracker::textRemoved); connect(document, &Document::destroyed, this, &DocumentChangeTracker::documentDestroyed); connect(document, &Document::documentSavedOrUploaded, this, &DocumentChangeTracker::documentSavedOrUploaded); m_moving = dynamic_cast(document); Q_ASSERT(m_moving); // can't use new connect syntax here, MovingInterface is not a QObject connect(m_document, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*))); ModificationRevision::setEditorRevisionForFile(m_url, m_moving->revision()); reset(); } QList< QPair< KTextEditor::Range, QString > > DocumentChangeTracker::completions() const { VERIFY_FOREGROUND_LOCKED QList< QPair< KTextEditor::Range , QString > > ret; return ret; } void DocumentChangeTracker::reset() { VERIFY_FOREGROUND_LOCKED // We don't reset the insertion here, as it may continue m_needUpdate = false; m_revisionAtLastReset = acquireRevision(m_moving->revision()); Q_ASSERT(m_revisionAtLastReset); } RevisionReference DocumentChangeTracker::currentRevision() { VERIFY_FOREGROUND_LOCKED return acquireRevision(m_moving->revision()); } RevisionReference DocumentChangeTracker::revisionAtLastReset() const { VERIFY_FOREGROUND_LOCKED return m_revisionAtLastReset; } bool DocumentChangeTracker::needUpdate() const { VERIFY_FOREGROUND_LOCKED return m_needUpdate; } void DocumentChangeTracker::updateChangedRange(int delay) { // Q_ASSERT(m_moving->revision() != m_revisionAtLastReset->revision()); // May happen after reload // When reloading, textRemoved is called with an invalid m_document->url(). For that reason, we use m_url instead. ModificationRevision::setEditorRevisionForFile(m_url, m_moving->revision()); if(needUpdate()) { ICore::self()->languageController()->backgroundParser()->addDocument(m_url, TopDUContext::AllDeclarationsContextsAndUses, 0, nullptr, ParseJob::IgnoresSequentialProcessing, delay); } } static Cursor cursorAdd(Cursor c, const QString& text) { c.setLine(c.line() + text.count('\n')); c.setColumn(c.column() + (text.length() - qMin(0, text.lastIndexOf('\n')))); return c; } int DocumentChangeTracker::recommendedDelay(KTextEditor::Document* doc, const KTextEditor::Range& range, const QString& text, bool removal) { auto languages = ICore::self()->languageController()->languagesForUrl(doc->url()); int delay = ILanguageSupport::NoUpdateRequired; Q_FOREACH (const auto& lang, languages) { // take the largest value, because NoUpdateRequired is -2 and we want to make sure // that if one language requires an update it actually happens delay = qMax(lang->suggestedReparseDelayForChange(doc, range, text, removal), delay); } return delay; } void DocumentChangeTracker::lineWrapped(KTextEditor::Document* document, const KTextEditor::Cursor& position) { textInserted(document, position, QStringLiteral("\n")); } void DocumentChangeTracker::lineUnwrapped(KTextEditor::Document* document, int line) { textRemoved(document, {{document->lineLength(line), line}, {0, line+1}}, QStringLiteral("\n")); } void DocumentChangeTracker::textInserted(Document* document, const Cursor& cursor, const QString& text) { /// TODO: get this data from KTextEditor directly, make its signal public KTextEditor::Range range(cursor, cursorAdd(cursor, text)); if(!m_lastInsertionPosition.isValid() || m_lastInsertionPosition == cursor) { m_currentCleanedInsertion.append(text); m_lastInsertionPosition = range.end(); } auto delay = recommendedDelay(document, range, text, false); m_needUpdate = delay != ILanguageSupport::NoUpdateRequired; updateChangedRange(delay); } void DocumentChangeTracker::textRemoved( Document* document, const KTextEditor::Range& oldRange, const QString& oldText ) { m_currentCleanedInsertion.clear(); m_lastInsertionPosition = KTextEditor::Cursor::invalid(); auto delay = recommendedDelay(document, oldRange, oldText, true); m_needUpdate = delay != ILanguageSupport::NoUpdateRequired; updateChangedRange(delay); } void DocumentChangeTracker::documentSavedOrUploaded(KTextEditor::Document* doc,bool) { ModificationRevision::clearModificationCache(IndexedString(doc->url())); } void DocumentChangeTracker::documentDestroyed( QObject* ) { m_document = nullptr; m_moving = nullptr; } DocumentChangeTracker::~DocumentChangeTracker() { Q_ASSERT(m_document); ModificationRevision::clearEditorRevisionForFile(KDevelop::IndexedString(m_document->url())); } Document* DocumentChangeTracker::document() const { return m_document; } MovingInterface* DocumentChangeTracker::documentMovingInterface() const { return m_moving; } void DocumentChangeTracker::aboutToInvalidateMovingInterfaceContent ( Document* ) { // Release all revisions! They must not be used any more. qCDebug(LANGUAGE) << "clearing all revisions"; m_revisionLocks.clear(); m_revisionAtLastReset = RevisionReference(); ModificationRevision::setEditorRevisionForFile(m_url, 0); } KDevelop::RangeInRevision DocumentChangeTracker::transformBetweenRevisions(KDevelop::RangeInRevision range, qint64 fromRevision, qint64 toRevision) const { VERIFY_FOREGROUND_LOCKED if((fromRevision == -1 || holdingRevision(fromRevision)) && (toRevision == -1 || holdingRevision(toRevision))) { m_moving->transformCursor(range.start.line, range.start.column, KTextEditor::MovingCursor::MoveOnInsert, fromRevision, toRevision); m_moving->transformCursor(range.end.line, range.end.column, KTextEditor::MovingCursor::StayOnInsert, fromRevision, toRevision); } return range; } KDevelop::CursorInRevision DocumentChangeTracker::transformBetweenRevisions(KDevelop::CursorInRevision cursor, qint64 fromRevision, qint64 toRevision, KTextEditor::MovingCursor::InsertBehavior behavior) const { VERIFY_FOREGROUND_LOCKED if((fromRevision == -1 || holdingRevision(fromRevision)) && (toRevision == -1 || holdingRevision(toRevision))) { m_moving->transformCursor(cursor.line, cursor.column, behavior, fromRevision, toRevision); } return cursor; } RangeInRevision DocumentChangeTracker::transformToRevision(KTextEditor::Range range, qint64 toRevision) const { return transformBetweenRevisions(RangeInRevision::castFromSimpleRange(range), -1, toRevision); } CursorInRevision DocumentChangeTracker::transformToRevision(KTextEditor::Cursor cursor, qint64 toRevision, MovingCursor::InsertBehavior behavior) const { return transformBetweenRevisions(CursorInRevision::castFromSimpleCursor(cursor), -1, toRevision, behavior); } KTextEditor::Range DocumentChangeTracker::transformToCurrentRevision(RangeInRevision range, qint64 fromRevision) const { return transformBetweenRevisions(range, fromRevision, -1).castToSimpleRange(); } KTextEditor::Cursor DocumentChangeTracker::transformToCurrentRevision(CursorInRevision cursor, qint64 fromRevision, MovingCursor::InsertBehavior behavior) const { return transformBetweenRevisions(cursor, fromRevision, -1, behavior).castToSimpleCursor(); } RevisionLockerAndClearerPrivate::RevisionLockerAndClearerPrivate(DocumentChangeTracker* tracker, qint64 revision) : m_tracker(tracker), m_revision(revision) { VERIFY_FOREGROUND_LOCKED moveToThread(QApplication::instance()->thread()); // Lock the revision m_tracker->lockRevision(revision); } RevisionLockerAndClearerPrivate::~RevisionLockerAndClearerPrivate() { if (m_tracker) m_tracker->unlockRevision(m_revision); } RevisionLockerAndClearer::~RevisionLockerAndClearer() { m_p->deleteLater(); // Will be deleted in the foreground thread, as the object was re-owned to the foreground } RevisionReference DocumentChangeTracker::acquireRevision(qint64 revision) { VERIFY_FOREGROUND_LOCKED if(!holdingRevision(revision) && revision != m_moving->revision()) return RevisionReference(); RevisionReference ret(new RevisionLockerAndClearer); ret->m_p = new RevisionLockerAndClearerPrivate(this, revision); return ret; } bool DocumentChangeTracker::holdingRevision(qint64 revision) const { VERIFY_FOREGROUND_LOCKED return m_revisionLocks.contains(revision); } void DocumentChangeTracker::lockRevision(qint64 revision) { VERIFY_FOREGROUND_LOCKED QMap< qint64, int >::iterator it = m_revisionLocks.find(revision); if(it != m_revisionLocks.end()) ++(*it); else { m_revisionLocks.insert(revision, 1); m_moving->lockRevision(revision); } } void DocumentChangeTracker::unlockRevision(qint64 revision) { VERIFY_FOREGROUND_LOCKED QMap< qint64, int >::iterator it = m_revisionLocks.find(revision); if(it == m_revisionLocks.end()) { qCDebug(LANGUAGE) << "cannot unlock revision" << revision << ", probably the revisions have been cleared"; return; } --(*it); if(*it == 0) { m_moving->unlockRevision(revision); m_revisionLocks.erase(it); } } qint64 RevisionLockerAndClearer::revision() const { return m_p->revision(); } RangeInRevision RevisionLockerAndClearer::transformToRevision(const KDevelop::RangeInRevision& range, const KDevelop::RevisionLockerAndClearer::Ptr& to) const { VERIFY_FOREGROUND_LOCKED if(!m_p->m_tracker || !valid() || (to && !to->valid())) return range; qint64 fromRevision = revision(); qint64 toRevision = -1; if(to) toRevision = to->revision(); return m_p->m_tracker->transformBetweenRevisions(range, fromRevision, toRevision); } CursorInRevision RevisionLockerAndClearer::transformToRevision(const KDevelop::CursorInRevision& cursor, const KDevelop::RevisionLockerAndClearer::Ptr& to, MovingCursor::InsertBehavior behavior) const { VERIFY_FOREGROUND_LOCKED if(!m_p->m_tracker || !valid() || (to && !to->valid())) return cursor; qint64 fromRevision = revision(); qint64 toRevision = -1; if(to) toRevision = to->revision(); return m_p->m_tracker->transformBetweenRevisions(cursor, fromRevision, toRevision, behavior); } RangeInRevision RevisionLockerAndClearer::transformFromRevision(const KDevelop::RangeInRevision& range, const KDevelop::RevisionLockerAndClearer::Ptr& from) const { VERIFY_FOREGROUND_LOCKED if(!m_p->m_tracker || !valid()) return range; qint64 toRevision = revision(); qint64 fromRevision = -1; if(from) fromRevision = from->revision(); return m_p->m_tracker->transformBetweenRevisions(range, fromRevision, toRevision); } CursorInRevision RevisionLockerAndClearer::transformFromRevision(const KDevelop::CursorInRevision& cursor, const KDevelop::RevisionLockerAndClearer::Ptr& from, MovingCursor::InsertBehavior behavior) const { VERIFY_FOREGROUND_LOCKED if(!m_p->m_tracker) return cursor; qint64 toRevision = revision(); qint64 fromRevision = -1; if(from) fromRevision = from->revision(); return m_p->m_tracker->transformBetweenRevisions(cursor, fromRevision, toRevision, behavior); } KTextEditor::Range RevisionLockerAndClearer::transformToCurrentRevision(const KDevelop::RangeInRevision& range) const { return transformToRevision(range, KDevelop::RevisionLockerAndClearer::Ptr()).castToSimpleRange(); } KTextEditor::Cursor RevisionLockerAndClearer::transformToCurrentRevision(const KDevelop::CursorInRevision& cursor, MovingCursor::InsertBehavior behavior) const { return transformToRevision(cursor, KDevelop::RevisionLockerAndClearer::Ptr(), behavior).castToSimpleCursor(); } RangeInRevision RevisionLockerAndClearer::transformFromCurrentRevision(const KTextEditor::Range& range) const { return transformFromRevision(RangeInRevision::castFromSimpleRange(range), RevisionReference()); } CursorInRevision RevisionLockerAndClearer::transformFromCurrentRevision(const KTextEditor::Cursor& cursor, MovingCursor::InsertBehavior behavior) const { return transformFromRevision(CursorInRevision::castFromSimpleCursor(cursor), RevisionReference(), behavior); } bool RevisionLockerAndClearer::valid() const { VERIFY_FOREGROUND_LOCKED if(!m_p->m_tracker) return false; if(revision() == -1) return true; // The 'current' revision is always valid return m_p->m_tracker->holdingRevision(revision()); } RevisionReference DocumentChangeTracker::diskRevision() const { ///@todo Track which revision was last saved to disk return RevisionReference(); } } diff --git a/language/backgroundparser/urlparselock.cpp b/language/backgroundparser/urlparselock.cpp index 80ebf1d1c..dca89f998 100644 --- a/language/backgroundparser/urlparselock.cpp +++ b/language/backgroundparser/urlparselock.cpp @@ -1,101 +1,101 @@ /* * This file is part of KDevelop * * Copyright 2007-2009 David Nolden * Copyright 2016 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 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 "urlparselock.h" #include #include using namespace KDevelop; namespace { struct PerUrlData { PerUrlData() // TODO: make this non-recursive : mutex(QMutex::Recursive) , ref(0) {} QMutex mutex; // how many people are (trying to) parse this url - // we use this to delete the entry once noone needs it anymore + // we use this to delete the entry once no-one needs it anymore uint ref; }; // this mutex protects the parsingUrls // NOTE: QBasicMutex is safe to initialize statically QBasicMutex parsingUrlsMutex; // Hash of urls that are currently being parsed and their protection data using ParsingUrls = QHash; ParsingUrls& parsingUrls() { // delay initialization of the hash until it's needed static ParsingUrls parsingUrls; return parsingUrls; } } UrlParseLock::UrlParseLock(const IndexedString& url) : m_url(url) { QMutexLocker lock(&parsingUrlsMutex); // NOTE: operator[] default-initializes the ptr to zero for us when not available auto& perUrlData = parsingUrls()[url]; if (!perUrlData) { // if that was the case, we are the first to parse this url, create an entry perUrlData = new PerUrlData; } // always increment the refcount ++perUrlData->ref; // now lock the url, but don't do so while blocking the global mutex auto& mutex = perUrlData->mutex; lock.unlock(); mutex.lock(); } UrlParseLock::~UrlParseLock() { QMutexLocker lock(&parsingUrlsMutex); // find the entry for this url auto& urls = parsingUrls(); auto it = urls.find(m_url); Q_ASSERT(it != urls.end()); // it must exist auto& perUrlData = it.value(); // unlock the per-url mutex perUrlData->mutex.unlock(); // decrement the refcount --perUrlData->ref; if (perUrlData->ref == 0) { // and cleanup, if possible delete perUrlData; urls.erase(it); } } diff --git a/language/duchain/duchain.cpp b/language/duchain/duchain.cpp index 5c457c3ab..4a4c21506 100644 --- a/language/duchain/duchain.cpp +++ b/language/duchain/duchain.cpp @@ -1,1749 +1,1749 @@ /* This is part of KDevelop Copyright 2006-2008 Hamish Rodda Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 "duchain.h" #include "duchainlock.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "../interfaces/ilanguagesupport.h" #include "../interfaces/icodehighlighting.h" #include "../backgroundparser/backgroundparser.h" #include "util/debug.h" #include "language-features.h" #include "topducontext.h" #include "topducontextdata.h" #include "topducontextdynamicdata.h" #include "parsingenvironment.h" #include "declaration.h" #include "definitions.h" #include "duchainutils.h" #include "use.h" #include "uses.h" #include "abstractfunctiondeclaration.h" #include "duchainregister.h" #include "persistentsymboltable.h" #include "serialization/itemrepository.h" #include "waitforupdate.h" #include "importers.h" #if HAVE_MALLOC_TRIM #include "malloc.h" #endif namespace { //Additional "soft" cleanup steps that are done before the actual cleanup. //During "soft" cleanup, the consistency is not guaranteed. The repository is //marked to be updating during soft cleanup, so if kdevelop crashes, it will be cleared. //The big advantage of the soft cleanup steps is, that the duchain is always only locked for //short times, which leads to no lockup in the UI. const int SOFT_CLEANUP_STEPS = 1; const uint cleanupEverySeconds = 200; ///Approximate maximum count of top-contexts that are checked during final cleanup const uint maxFinalCleanupCheckContexts = 2000; const uint minimumFinalCleanupCheckContextsPercentage = 10; //Check at least n% of all top-contexts during cleanup //Set to true as soon as the duchain is deleted } namespace KDevelop { bool DUChain::m_deleted = false; ///Must be locked through KDevelop::SpinLock before using chainsByIndex ///This lock should be locked only for very short times QMutex DUChain::chainsByIndexLock; std::vector DUChain::chainsByIndex; //This thing is not actually used, but it's needed for compiling DEFINE_LIST_MEMBER_HASH(EnvironmentInformationListItem, items, uint) //An entry for the item-repository that holds some meta-data. Behind this entry, the actual ParsingEnvironmentFileData is stored. class EnvironmentInformationItem { public: EnvironmentInformationItem(uint topContext, uint size) : m_topContext(topContext), m_size(size) { } ~EnvironmentInformationItem() { } unsigned int hash() const { return m_topContext; } unsigned int itemSize() const { return sizeof(*this) + m_size; } uint m_topContext; uint m_size;//Size of the data behind, that holds the actual item }; struct ItemRepositoryIndexHash { uint operator()(unsigned int __x) const { return 173*(__x>>2) + 11 * (__x >> 16); } }; class EnvironmentInformationRequest { public: ///This constructor should only be used for lookup EnvironmentInformationRequest(uint topContextIndex) : m_file(nullptr), m_index(topContextIndex) { } EnvironmentInformationRequest(const ParsingEnvironmentFile* file) : m_file(file), m_index(file->indexedTopContext().index()) { } enum { AverageSize = 32 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_index; } uint itemSize() const { return sizeof(EnvironmentInformationItem) + DUChainItemSystem::self().dynamicSize(*m_file->d_func()); } void createItem(EnvironmentInformationItem* item) const { new (item) EnvironmentInformationItem(m_index, DUChainItemSystem::self().dynamicSize(*m_file->d_func())); Q_ASSERT(m_file->d_func()->m_dynamic); DUChainBaseData* data = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); DUChainItemSystem::self().copy(*m_file->d_func(), *data, true); Q_ASSERT(data->m_range == m_file->d_func()->m_range); Q_ASSERT(data->classId == m_file->d_func()->classId); Q_ASSERT(data->m_dynamic == false); } static void destroy(EnvironmentInformationItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationItem(); //We don't need to call the destructor, because that's done in DUChainBase::makeDynamic() //We just need to make sure that every environment-file is dynamic when it's deleted // DUChainItemSystem::self().callDestructor((DUChainBaseData*)(((char*)item) + sizeof(EnvironmentInformationItem))); } static bool persistent(const EnvironmentInformationItem* ) { //Cleanup done separately return true; } bool equals(const EnvironmentInformationItem* item) const { return m_index == item->m_topContext; } const ParsingEnvironmentFile* m_file; uint m_index; }; ///A list of environment-information/top-contexts mapped to a file-name class EnvironmentInformationListItem { public: EnvironmentInformationListItem() { initializeAppendedLists(true); } EnvironmentInformationListItem(const EnvironmentInformationListItem& rhs, bool dynamic = true) { initializeAppendedLists(dynamic); m_file = rhs.m_file; copyListsFrom(rhs); } ~EnvironmentInformationListItem() { freeAppendedLists(); } unsigned int hash() const { //We only compare the declaration. This allows us implementing a map, although the item-repository //originally represents a set. return m_file.hash(); } unsigned short int itemSize() const { return dynamicSize(); } IndexedString m_file; uint classSize() const { return sizeof(*this); } START_APPENDED_LISTS(EnvironmentInformationListItem); ///Contains the index of each contained environment-item APPENDED_LIST_FIRST(EnvironmentInformationListItem, uint, items); END_APPENDED_LISTS(EnvironmentInformationListItem, items); }; class EnvironmentInformationListRequest { public: ///This constructor should only be used for lookup EnvironmentInformationListRequest(const IndexedString& file) : m_file(file), m_item(nullptr) { } ///This is used to actually construct the information in the repository EnvironmentInformationListRequest(const IndexedString& file, const EnvironmentInformationListItem& item) : m_file(file), m_item(&item) { } enum { AverageSize = 160 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_file.hash(); } uint itemSize() const { return m_item->itemSize(); } void createItem(EnvironmentInformationListItem* item) const { Q_ASSERT(m_item->m_file == m_file); new (item) EnvironmentInformationListItem(*m_item, false); } static void destroy(EnvironmentInformationListItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationListItem(); } static bool persistent(const EnvironmentInformationListItem*) { //Cleanup is done separately return true; } bool equals(const EnvironmentInformationListItem* item) const { return m_file == item->m_file; } IndexedString m_file; const EnvironmentInformationListItem* m_item; }; class DUChainPrivate; static DUChainPrivate* duChainPrivateSelf = nullptr; class DUChainPrivate { class CleanupThread : public QThread { public: explicit CleanupThread(DUChainPrivate* data) : m_stopRunning(false), m_data(data) { } void stopThread() { { QMutexLocker lock(&m_waitMutex); m_stopRunning = true; m_wait.wakeAll(); //Wakes the thread up, so it notices it should exit } wait(); } private: void run() override { while(1) { for(uint s = 0; s < cleanupEverySeconds; ++s) { if(m_stopRunning) break; QMutexLocker lock(&m_waitMutex); m_wait.wait(&m_waitMutex, 1000); } if(m_stopRunning) break; //Just to make sure the cache is cleared periodically ModificationRevisionSet::clearCache(); m_data->doMoreCleanup(SOFT_CLEANUP_STEPS, TryLock); if(m_stopRunning) break; } } bool m_stopRunning; QWaitCondition m_wait; QMutex m_waitMutex; DUChainPrivate* m_data; }; public: DUChainPrivate() : m_chainsMutex(QMutex::Recursive), m_cleanupMutex(QMutex::Recursive), instance(nullptr), m_cleanupDisabled(false), m_destroyed(false), m_environmentListInfo(QStringLiteral("Environment Lists")), m_environmentInfo(QStringLiteral("Environment Information")) { #if defined(TEST_NO_CLEANUP) m_cleanupDisabled = true; #endif duChainPrivateSelf = this; qRegisterMetaType("KDevelop::DUChainBasePointer"); qRegisterMetaType("KDevelop::DUContextPointer"); qRegisterMetaType("KDevelop::TopDUContextPointer"); qRegisterMetaType("KDevelop::DeclarationPointer"); qRegisterMetaType("KDevelop::FunctionDeclarationPointer"); qRegisterMetaType("KDevelop::IndexedString"); qRegisterMetaType("KDevelop::IndexedTopDUContext"); qRegisterMetaType("KDevelop::ReferencedTopDUContext"); instance = new DUChain(); m_cleanup = new CleanupThread(this); m_cleanup->start(); DUChain::m_deleted = false; ///Loading of some static data: { ///@todo Solve this more duchain-like QFile f(globalItemRepositoryRegistry().path() + "/parsing_environment_data"); bool opened = f.open(QIODevice::ReadOnly); ///FIXME: ugh, so ugly ParsingEnvironmentFile::m_staticData = reinterpret_cast( new char[sizeof(StaticParsingEnvironmentData)]); if(opened) { qCDebug(LANGUAGE) << "reading parsing-environment static data"; //Read f.read((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); }else{ qCDebug(LANGUAGE) << "creating new parsing-environment static data"; //Initialize new (ParsingEnvironmentFile::m_staticData) StaticParsingEnvironmentData(); } } ///Read in the list of available top-context indices { QFile f(globalItemRepositoryRegistry().path() + "/available_top_context_indices"); bool opened = f.open(QIODevice::ReadOnly); if(opened) { Q_ASSERT( (f.size() % sizeof(uint)) == 0); m_availableTopContextIndices.resize(f.size()/(int)sizeof(uint)); f.read((char*)m_availableTopContextIndices.data(), f.size()); } } } ~DUChainPrivate() { qCDebug(LANGUAGE) << "Destroying"; DUChain::m_deleted = true; m_cleanup->stopThread(); delete m_cleanup; delete instance; } void clear() { if(!m_cleanupDisabled) doMoreCleanup(); DUChainWriteLocker writeLock(DUChain::lock()); QMutexLocker l(&m_chainsMutex); foreach(TopDUContext* top, m_chainsByUrl) removeDocumentChainFromMemory(top); m_indexEnvironmentInformations.clear(); m_fileEnvironmentInformations.clear(); Q_ASSERT(m_fileEnvironmentInformations.isEmpty()); Q_ASSERT(m_chainsByUrl.isEmpty()); } ///DUChain must be write-locked ///Also removes from the environment-manager if the top-context is not on disk void removeDocumentChainFromMemory(TopDUContext* context) { QMutexLocker l(&m_chainsMutex); { QMutexLocker l(&m_referenceCountsMutex); if(m_referenceCounts.contains(context)) { //This happens during shutdown, since everything is unloaded qCDebug(LANGUAGE) << "removed a top-context that was reference-counted:" << context->url().str() << context->ownIndex(); m_referenceCounts.remove(context); } } uint index = context->ownIndex(); // qCDebug(LANGUAGE) << "duchain: removing document" << context->url().str(); Q_ASSERT(hasChainForIndex(index)); Q_ASSERT(m_chainsByUrl.contains(context->url(), context)); m_chainsByUrl.remove(context->url(), context); if(!context->isOnDisk()) instance->removeFromEnvironmentManager(context); l.unlock(); //DUChain is write-locked, so we can do whatever we want on the top-context, including deleting it context->deleteSelf(); l.relock(); Q_ASSERT(hasChainForIndex(index)); QMutexLocker lock(&DUChain::chainsByIndexLock); DUChain::chainsByIndex[index] = nullptr; } ///Must be locked before accessing content of this class. ///Should be released during expensive disk-operations and such. QMutex m_chainsMutex; QMutex m_cleanupMutex; CleanupThread* m_cleanup; DUChain* instance; DUChainLock lock; QMultiMap m_chainsByUrl; //Must be locked before accessing m_referenceCounts QMutex m_referenceCountsMutex; QHash m_referenceCounts; Definitions m_definitions; Uses m_uses; QSet m_loading; bool m_cleanupDisabled; //List of available top-context indices, protected by m_chainsMutex QVector m_availableTopContextIndices; ///Used to keep alive the top-context that belong to documents loaded in the editor QSet m_openDocumentContexts; bool m_destroyed; ///The item must not be stored yet ///m_chainsMutex should not be locked, since this can trigger I/O void addEnvironmentInformation(ParsingEnvironmentFilePointer info) { Q_ASSERT(!findInformation(info->indexedTopContext().index())); Q_ASSERT(m_environmentInfo.findIndex(info->indexedTopContext().index()) == 0); QMutexLocker lock(&m_chainsMutex); m_fileEnvironmentInformations.insert(info->url(), info); m_indexEnvironmentInformations.insert(info->indexedTopContext().index(), info); Q_ASSERT(info->d_func()->classId); } ///The item must be managed currently ///m_chainsMutex does not need to be locked void removeEnvironmentInformation(ParsingEnvironmentFilePointer info) { info->makeDynamic(); //By doing this, we make sure the data is actually being destroyed in the destructor bool removed = false; bool removed2 = false; { QMutexLocker lock(&m_chainsMutex); removed = m_fileEnvironmentInformations.remove(info->url(), info); removed2 = m_indexEnvironmentInformations.remove(info->indexedTopContext().index()); } { //Remove it from the environment information lists if it was there QMutexLocker lock(m_environmentListInfo.mutex()); uint index = m_environmentListInfo.findIndex(info->url()); if(index) { EnvironmentInformationListItem item(*m_environmentListInfo.itemFromIndex(index)); if(item.itemsList().removeOne(info->indexedTopContext().index())) { m_environmentListInfo.deleteItem(index); if(!item.itemsList().isEmpty()) m_environmentListInfo.index(EnvironmentInformationListRequest(info->url(), item)); } } } QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(info->indexedTopContext().index()); if(index) { m_environmentInfo.deleteItem(index); } Q_UNUSED(removed); Q_UNUSED(removed2); Q_ASSERT(index || (removed && removed2)); Q_ASSERT(!findInformation(info->indexedTopContext().index())); } ///m_chainsMutex should _not_ be locked, because this may trigger I/O QList getEnvironmentInformation(IndexedString url) { QList ret; uint listIndex = m_environmentListInfo.findIndex(url); if(listIndex) { KDevVarLengthArray topContextIndices; { //First store all the possible intices into the KDevVarLengthArray, so we can unlock the mutex before processing them. QMutexLocker lock(m_environmentListInfo.mutex()); //Lock the mutex to make sure the item isn't changed while it's being iterated const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(listIndex); FOREACH_FUNCTION(uint topContextIndex, item->items) topContextIndices << topContextIndex; } //Process the indices in a separate step after copying them from the array, so we don't need m_environmentListInfo.mutex locked, //and can call loadInformation(..) safely, which else might lead to a deadlock. foreach (uint topContextIndex, topContextIndices) { QExplicitlySharedDataPointer< ParsingEnvironmentFile > p = ParsingEnvironmentFilePointer(loadInformation(topContextIndex)); if(p) { ret << p; }else{ qCDebug(LANGUAGE) << "Failed to load enviromment-information for" << TopDUContextDynamicData::loadUrl(topContextIndex).str(); } } } QMutexLocker l(&m_chainsMutex); //Add those information that have not been added to the stored lists yet foreach(const ParsingEnvironmentFilePointer& file, m_fileEnvironmentInformations.values(url)) if(!ret.contains(file)) ret << file; return ret; } ///Must be called _without_ the chainsByIndex spin-lock locked static inline bool hasChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); return (DUChain::chainsByIndex.size() > index) && DUChain::chainsByIndex[index]; } ///Must be called _without_ the chainsByIndex spin-lock locked. Returns the top-context if it is loaded. static inline TopDUContext* readChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() > index) return DUChain::chainsByIndex[index]; else return nullptr; } ///Makes sure that the chain with the given index is loaded ///@warning m_chainsMutex must NOT be locked when this is called void loadChain(uint index, QSet& loaded) { QMutexLocker l(&m_chainsMutex); if(!hasChainForIndex(index)) { if(m_loading.contains(index)) { //It's probably being loaded by another thread. So wait until the load is ready while(m_loading.contains(index)) { l.unlock(); qCDebug(LANGUAGE) << "waiting for another thread to load index" << index; QThread::usleep(50000); l.relock(); } loaded.insert(index); return; } m_loading.insert(index); loaded.insert(index); l.unlock(); qCDebug(LANGUAGE) << "loading top-context" << index; TopDUContext* chain = TopDUContextDynamicData::load(index); if(chain) { chain->setParsingEnvironmentFile(loadInformation(chain->ownIndex())); if(!chain->usingImportsCache()) { //Eventually also load all the imported chains, so the import-structure is built foreach(const DUContext::Import &import, chain->DUContext::importedParentContexts()) { if(!loaded.contains(import.topContextIndex())) { loadChain(import.topContextIndex(), loaded); } } } chain->rebuildDynamicImportStructure(); chain->setInDuChain(true); instance->addDocumentChain(chain); } l.relock(); m_loading.remove(index); } } ///Stores all environment-information ///Also makes sure that all information that stays is referenced, so it stays alive. ///@param atomic If this is false, the write-lock will be released time by time void storeAllInformation(bool atomic, DUChainWriteLocker& locker) { uint cnt = 0; QList urls; { QMutexLocker lock(&m_chainsMutex); urls += m_fileEnvironmentInformations.keys(); } foreach(const IndexedString &url, urls) { QList check; { QMutexLocker lock(&m_chainsMutex); check = m_fileEnvironmentInformations.values(url); } foreach(ParsingEnvironmentFilePointer file, check) { EnvironmentInformationRequest req(file.data()); QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(req); if(file->d_func()->isDynamic()) { //This item has been changed, or isn't in the repository yet //Eventually remove an old entry if(index) m_environmentInfo.deleteItem(index); //Add the new entry to the item repository index = m_environmentInfo.index(req); Q_ASSERT(index); EnvironmentInformationItem* item = const_cast(m_environmentInfo.itemFromIndex(index)); DUChainBaseData* theData = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); Q_ASSERT(theData->m_range == file->d_func()->m_range); Q_ASSERT(theData->m_dynamic == false); Q_ASSERT(theData->classId == file->d_func()->classId); file->setData( theData ); ++cnt; }else{ m_environmentInfo.itemFromIndex(index); //Prevent unloading of the data, by accessing the item } } ///We must not release the lock while holding a reference to a ParsingEnvironmentFilePointer, else we may miss the deletion of an ///information, and will get crashes. if(!atomic && (cnt % 100 == 0)) { //Release the lock on a regular basis locker.unlock(); locker.lock(); } storeInformationList(url); //Access the data in the repository, so the bucket isn't unloaded uint index = m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)); if(index) { m_environmentListInfo.itemFromIndex(index); }else{ QMutexLocker lock(&m_chainsMutex); qCDebug(LANGUAGE) << "Did not find stored item for" << url.str() << "count:" << m_fileEnvironmentInformations.values(url); } if(!atomic) { locker.unlock(); locker.lock(); } } } QMutex& cleanupMutex() { return m_cleanupMutex; } /// defines how we interact with the ongoing language parse jobs enum LockFlag { /// no locking required, only used when we locked previously NoLock = 0, /// lock all parse jobs and block until we succeeded. required at shutdown BlockingLock = 1, /// only try to lock and abort on failure, good for the intermittent cleanups TryLock = 2, }; ///@param retries When this is nonzero, then doMoreCleanup will do the specified amount of cycles ///doing the cleanup without permanently locking the du-chain. During these steps the consistency ///of the disk-storage is not guaranteed, but only few changes will be done during these steps, ///so the final step where the duchain is permanently locked is much faster. void doMoreCleanup(int retries = 0, LockFlag lockFlag = BlockingLock) { if(m_cleanupDisabled) return; //This mutex makes sure that there's never 2 threads at he same time trying to clean up QMutexLocker lockCleanupMutex(&cleanupMutex()); if(m_destroyed || m_cleanupDisabled) return; Q_ASSERT(!instance->lock()->currentThreadHasReadLock() && !instance->lock()->currentThreadHasWriteLock()); DUChainWriteLocker writeLock(instance->lock()); //This is used to stop all parsing before starting to do the cleanup. This way less happens during the //soft cleanups, and we have a good chance that during the "hard" cleanup only few data has to be written. QList locked; if (lockFlag != NoLock) { QList languages; if (ICore* core = ICore::self()) if (ILanguageController* lc = core->languageController()) languages = lc->loadedLanguages(); writeLock.unlock(); //Here we wait for all parsing-threads to stop their processing foreach(const auto language, languages) { if (lockFlag == TryLock) { if (!language->parseLock()->tryLockForWrite()) { qCDebug(LANGUAGE) << "Aborting cleanup because language plugin is still parsing:" << language->name(); // some language is still parsing, don't interfere with the cleanup foreach(auto* lock, locked) { lock->unlock(); } return; } } else { language->parseLock()->lockForWrite(); } locked << language->parseLock(); } writeLock.lock(); globalItemRepositoryRegistry().lockForWriting(); qCDebug(LANGUAGE) << "starting cleanup"; } QTime startTime = QTime::currentTime(); PersistentSymbolTable::self().clearCache(); storeAllInformation(!retries, writeLock); //Puts environment-information into a repository //We don't need to increase the reference-count, since the cleanup-mutex is locked QSet workOnContexts; { QMutexLocker l(&m_chainsMutex); workOnContexts.reserve(m_chainsByUrl.size()); foreach(TopDUContext* top, m_chainsByUrl) { workOnContexts << top; Q_ASSERT(hasChainForIndex(top->ownIndex())); } } foreach(TopDUContext* context, workOnContexts) { context->m_dynamicData->store(); if(retries) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } //Unload all top-contexts that don't have a reference-count and that are not imported by a referenced one QSet unloadedNames; bool unloadedOne = true; bool unloadAllUnreferenced = !retries; //Now unload contexts, but only ones that are not imported by any other currently loaded context //The complication: Since during the lock-break new references may be added, we must never keep //the du-chain in an invalid state. Thus we can only unload contexts that are not imported by any //currently loaded contexts. In case of loops, we have to unload everything at once. while(unloadedOne) { unloadedOne = false; int hadUnloadable = 0; unloadContexts: foreach(TopDUContext* unload, workOnContexts) { bool hasReference = false; { QMutexLocker l(&m_referenceCountsMutex); //Test if the context is imported by a referenced one for (auto it = m_referenceCounts.constBegin(), end = m_referenceCounts.constEnd(); it != end; ++it) { auto* context = it.key(); if(context == unload || context->imports(unload, CursorInRevision())) { workOnContexts.remove(unload); hasReference = true; } } } if(!hasReference) ++hadUnloadable; //We have found a context that is not referenced else continue; //This context is referenced bool isImportedByLoaded = !unload->loadedImporters().isEmpty(); //If we unload a context that is imported by other contexts, we create a bad loaded state if(isImportedByLoaded && !unloadAllUnreferenced) continue; unloadedNames.insert(unload->url()); //Since we've released the write-lock in between, we've got to call store() again to be sure that none of the data is dynamic //If nothing has changed, it is only a low-cost call. unload->m_dynamicData->store(); Q_ASSERT(!unload->d_func()->m_dynamic); removeDocumentChainFromMemory(unload); workOnContexts.remove(unload); unloadedOne = true; if(!unloadAllUnreferenced) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } if(hadUnloadable && !unloadedOne) { Q_ASSERT(!unloadAllUnreferenced); //This can happen in case of loops. We have o unload everything at one time. qCDebug(LANGUAGE) << "found" << hadUnloadable << "unloadable contexts, but could not unload separately. Unloading atomically."; unloadAllUnreferenced = true; hadUnloadable = 0; //Reset to 0, so we cannot loop forever goto unloadContexts; } } if(retries == 0) { QMutexLocker lock(&m_chainsMutex); //Do this atomically, since we must be sure that _everything_ is already saved for(QMultiMap::iterator it = m_fileEnvironmentInformations.begin(); it != m_fileEnvironmentInformations.end(); ) { ParsingEnvironmentFile* f = it->data(); Q_ASSERT(f->d_func()->classId); if(f->ref.load() == 1) { Q_ASSERT(!f->d_func()->isDynamic()); //It cannot be dynamic, since we have stored before //The ParsingEnvironmentFilePointer is only referenced once. This means that it does not belong to any //loaded top-context, so just remove it to save some memory and processing time. ///@todo use some kind of timeout before removing it = m_fileEnvironmentInformations.erase(it); }else{ ++it; } } } if(retries) writeLock.unlock(); //This must be the last step, due to the on-disk reference counting globalItemRepositoryRegistry().store(); //Stores all repositories { //Store the static parsing-environment file data ///@todo Solve this more elegantly, using a general mechanism to store static duchain-like data Q_ASSERT(ParsingEnvironmentFile::m_staticData); QFile f(globalItemRepositoryRegistry().path() + "/parsing_environment_data"); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); } ///Write out the list of available top-context indices { QMutexLocker lock(&m_chainsMutex); QFile f(globalItemRepositoryRegistry().path() + "/available_top_context_indices"); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)m_availableTopContextIndices.data(), m_availableTopContextIndices.size() * sizeof(uint)); } if(retries) { doMoreCleanup(retries-1, NoLock); writeLock.lock(); } if(lockFlag != NoLock) { globalItemRepositoryRegistry().unlockForWriting(); int elapsedSeconds = startTime.secsTo(QTime::currentTime()); qCDebug(LANGUAGE) << "seconds spent doing cleanup: " << elapsedSeconds << "top-contexts still open:" << m_chainsByUrl.size(); } if(!retries) { int elapesedMilliSeconds = startTime.msecsTo(QTime::currentTime()); qCDebug(LANGUAGE) << "milliseconds spent doing cleanup with locked duchain: " << elapesedMilliSeconds; } foreach(QReadWriteLock* lock, locked) lock->unlock(); #if HAVE_MALLOC_TRIM // trim unused memory but keep a pad buffer of about 50 MB // this can greatly decrease the perceived memory consumption of kdevelop // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(50 * 1024 * 1024); #endif } ///Checks whether the information is already loaded. ParsingEnvironmentFile* findInformation(uint topContextIndex) { QMutexLocker lock(&m_chainsMutex); QHash::iterator it = m_indexEnvironmentInformations.find(topContextIndex); if(it != m_indexEnvironmentInformations.end()) return (*it).data(); return nullptr; } ///Loads/gets the environment-information for the given top-context index, or returns zero if none exists ///@warning m_chainsMutex should NOT be locked when this is called, because it triggers I/O ///@warning no other mutexes should be locked, as that may lead to a dedalock ParsingEnvironmentFile* loadInformation(uint topContextIndex) { ParsingEnvironmentFile* alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; //Step two: Check if it is on disk, and if is, load it uint dataIndex = m_environmentInfo.findIndex(EnvironmentInformationRequest(topContextIndex)); if(!dataIndex) { //No environment-information stored for this top-context return nullptr; } const EnvironmentInformationItem& item(*m_environmentInfo.itemFromIndex(dataIndex)); QMutexLocker lock(&m_chainsMutex); //Due to multi-threading, we must do this check after locking the mutex, so we can be sure we don't create the same item twice at the same time alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; ///FIXME: ugly, and remove const_cast ParsingEnvironmentFile* ret = dynamic_cast(DUChainItemSystem::self().create( const_cast(reinterpret_cast(reinterpret_cast(&item) + sizeof(EnvironmentInformationItem))) )); if(ret) { Q_ASSERT(ret->d_func()->classId); Q_ASSERT(ret->indexedTopContext().index() == topContextIndex); ParsingEnvironmentFilePointer retPtr(ret); m_fileEnvironmentInformations.insert(ret->url(), retPtr); Q_ASSERT(!m_indexEnvironmentInformations.contains(ret->indexedTopContext().index())); m_indexEnvironmentInformations.insert(ret->indexedTopContext().index(), retPtr); } return ret; } struct CleanupListVisitor { QList checkContexts; bool operator()(const EnvironmentInformationItem* item) { checkContexts << item->m_topContext; return true; } }; ///Will check a selection of all top-contexts for up-to-date ness, and remove them if out of date void cleanupTopContexts() { DUChainWriteLocker lock( DUChain::lock() ); qCDebug(LANGUAGE) << "cleaning top-contexts"; CleanupListVisitor visitor; uint startPos = 0; m_environmentInfo.visitAllItems(visitor); int checkContextsCount = maxFinalCleanupCheckContexts; int percentageOfContexts = (visitor.checkContexts.size() * 100) / minimumFinalCleanupCheckContextsPercentage; if(checkContextsCount < percentageOfContexts) checkContextsCount = percentageOfContexts; if(visitor.checkContexts.size() > (int)checkContextsCount) startPos = qrand() % (visitor.checkContexts.size() - checkContextsCount); int endPos = startPos + maxFinalCleanupCheckContexts; if(endPos > visitor.checkContexts.size()) endPos = visitor.checkContexts.size(); QSet< uint > check; for(int a = startPos; a < endPos && check.size() < checkContextsCount; ++a) if(check.size() < checkContextsCount) addContextsForRemoval(check, IndexedTopDUContext(visitor.checkContexts[a])); foreach(uint topIndex, check) { IndexedTopDUContext top(topIndex); if(top.data()) { qCDebug(LANGUAGE) << "removing top-context for" << top.data()->url().str() << "because it is out of date"; instance->removeDocumentChain(top.data()); } } qCDebug(LANGUAGE) << "check ready"; } private: void addContextsForRemoval(QSet& topContexts, IndexedTopDUContext top) { if(topContexts.contains(top.index())) return; QExplicitlySharedDataPointer info( instance->environmentFileForDocument(top) ); ///@todo Also check if the context is "useful"(Not a duplicate context, imported by a useful one, ...) if(info && info->needsUpdate()) { //This context will be removed }else{ return; } topContexts.insert(top.index()); if(info) { //Check whether importers need to be removed as well QList< QExplicitlySharedDataPointer > importers = info->importers(); QSet< QExplicitlySharedDataPointer > checkNext; //Do breadth first search, so less imports/importers have to be loaded, and a lower depth is reached for(QList< QExplicitlySharedDataPointer >::iterator it = importers.begin(); it != importers.end(); ++it) { IndexedTopDUContext c = (*it)->indexedTopContext(); if(!topContexts.contains(c.index())) { topContexts.insert(c.index()); //Prevent useless recursion checkNext.insert(*it); } } for(QSet< QExplicitlySharedDataPointer >::const_iterator it = checkNext.begin(); it != checkNext.end(); ++it) { topContexts.remove((*it)->indexedTopContext().index()); //Enable full check again addContextsForRemoval(topContexts, (*it)->indexedTopContext()); } } } ///Stores the environment-information for the given url void storeInformationList(IndexedString url) { QMutexLocker lock(m_environmentListInfo.mutex()); EnvironmentInformationListItem newItem; newItem.m_file = url; QSet newItems; { QMutexLocker lock(&m_chainsMutex); QMultiMap::iterator start = m_fileEnvironmentInformations.lowerBound(url); QMultiMap::iterator end = m_fileEnvironmentInformations.upperBound(url); for(QMultiMap::iterator it = start; it != end; ++it) { uint topContextIndex = (*it)->indexedTopContext().index(); newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } uint index = m_environmentListInfo.findIndex(url); if(index) { //We only handle adding items here, since we can never be sure whether everything is loaded //Removal is handled directly in removeEnvironmentInformation const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(index); QSet oldItems; FOREACH_FUNCTION(uint topContextIndex, item->items) { oldItems.insert(topContextIndex); if(!newItems.contains(topContextIndex)) { newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } if(oldItems == newItems) return; ///Update/insert a new list m_environmentListInfo.deleteItem(index); //Remove the previous item } Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)) == 0); //Insert the new item m_environmentListInfo.index(EnvironmentInformationListRequest(url, newItem)); Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url))); } - //Loaded environment-informations. Protected by m_chainsMutex + //Loaded environment information. Protected by m_chainsMutex QMultiMap m_fileEnvironmentInformations; QHash m_indexEnvironmentInformations; ///The following repositories are thread-safe, and m_chainsMutex should not be locked when using them, because ///they may trigger I/O. Still it may be required to lock their local mutexes. ///Maps filenames to a list of top-contexts/environment-information. ItemRepository m_environmentListInfo; ///Maps top-context-indices to environment-information item. ItemRepository m_environmentInfo; }; Q_GLOBAL_STATIC(DUChainPrivate, sdDUChainPrivate) DUChain::DUChain() { Q_ASSERT(ICore::self()); connect(ICore::self()->documentController(), &IDocumentController::documentLoadedPrepare, this, &DUChain::documentLoadedPrepare); connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this, &DUChain::documentRenamed); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &DUChain::documentActivated); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &DUChain::documentClosed); } DUChain::~DUChain() { DUChain::m_deleted = true; } DUChain* DUChain::self() { return sdDUChainPrivate->instance; } extern void initModificationRevisionSetRepository(); extern void initDeclarationRepositories(); extern void initIdentifierRepository(); extern void initTypeRepository(); extern void initInstantiationInformationRepository(); void DUChain::initialize() { // Initialize the global item repository as first thing after loading the session Q_ASSERT(ICore::self()); Q_ASSERT(ICore::self()->activeSession()); ItemRepositoryRegistry::initialize(ICore::self()->activeSessionLock()); initReferenceCounting(); // This needs to be initialized here too as the function is not threadsafe, but can // sometimes be called from different threads. This results in the underlying QFile // being 0 and hence crashes at some point later when accessing the contents via // read. See https://bugs.kde.org/show_bug.cgi?id=250779 RecursiveImportRepository::repository(); RecursiveImportCacheRepository::repository(); // similar to above, see https://bugs.kde.org/show_bug.cgi?id=255323 initDeclarationRepositories(); initModificationRevisionSetRepository(); initIdentifierRepository(); initTypeRepository(); initInstantiationInformationRepository(); Importers::self(); globalImportIdentifier(); globalIndexedImportIdentifier(); globalAliasIdentifier(); globalIndexedAliasIdentifier(); } DUChainLock* DUChain::lock() { return &sdDUChainPrivate->lock; } QList DUChain::allChains() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); return sdDUChainPrivate->m_chainsByUrl.values(); } void DUChain::updateContextEnvironment( TopDUContext* context, ParsingEnvironmentFile* file ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); removeFromEnvironmentManager( context ); context->setParsingEnvironmentFile( file ); addToEnvironmentManager( context ); } void DUChain::removeDocumentChain( TopDUContext* context ) { ENSURE_CHAIN_WRITE_LOCKED; IndexedTopDUContext indexed(context->indexed()); Q_ASSERT(indexed.data() == context); ///This assertion fails if you call removeDocumentChain(..) on a document that has not been added to the du-chain context->m_dynamicData->deleteOnDisk(); Q_ASSERT(indexed.data() == context); sdDUChainPrivate->removeDocumentChainFromMemory(context); Q_ASSERT(!indexed.data()); Q_ASSERT(!environmentFileForDocument(indexed)); QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); sdDUChainPrivate->m_availableTopContextIndices.push_back(indexed.index()); } void DUChain::addDocumentChain( TopDUContext * chain ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // qCDebug(LANGUAGE) << "duchain: adding document" << chain->url().str() << " " << chain; Q_ASSERT(chain); Q_ASSERT(!sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() <= chain->ownIndex()) DUChain::chainsByIndex.resize(chain->ownIndex() + 100, nullptr); DUChain::chainsByIndex[chain->ownIndex()] = chain; } { Q_ASSERT(DUChain::chainsByIndex[chain->ownIndex()]); } Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); sdDUChainPrivate->m_chainsByUrl.insert(chain->url(), chain); Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); chain->setInDuChain(true); l.unlock(); addToEnvironmentManager(chain); // This function might be called during shutdown by stale parse jobs // Make sure we don't access null-pointers here if (ICore::self() && ICore::self()->languageController() && ICore::self()->languageController()->backgroundParser()->trackerForUrl(chain->url())) { //Make sure the context stays alive at least as long as the context is open ReferencedTopDUContext ctx(chain); sdDUChainPrivate->m_openDocumentContexts.insert(ctx); } } void DUChain::addToEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage Q_ASSERT(file->indexedTopContext().index() == chain->ownIndex()); if(ParsingEnvironmentFile* alreadyHave = sdDUChainPrivate->findInformation(file->indexedTopContext().index())) { ///If this triggers, there has already been another environment-information registered for this top-context. ///removeFromEnvironmentManager should have been called before to remove the old environment-information. Q_ASSERT(alreadyHave == file.data()); Q_UNUSED(alreadyHave); return; } sdDUChainPrivate->addEnvironmentInformation(file); } void DUChain::removeFromEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage sdDUChainPrivate->removeEnvironmentInformation(file); } TopDUContext* DUChain::chainForDocument(const QUrl& document, bool proxyContext) const { return chainForDocument(IndexedString(document), proxyContext); } bool DUChain::isInMemory(uint topContextIndex) const { return DUChainPrivate::hasChainForIndex(topContextIndex); } IndexedString DUChain::urlForIndex(uint index) const { { TopDUContext* chain = DUChainPrivate::readChainForIndex(index); if(chain) return chain->url(); } return TopDUContextDynamicData::loadUrl(index); } TopDUContext* DUChain::loadChain(uint index) { QSet loaded; sdDUChainPrivate->loadChain(index, loaded); { QMutexLocker lock(&chainsByIndexLock); if(chainsByIndex.size() > index) { TopDUContext* top = chainsByIndex[index]; if(top) return top; } } return nullptr; } TopDUContext* DUChain::chainForDocument(const KDevelop::IndexedString& document, bool proxyContext) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return nullptr; QList list = sdDUChainPrivate->getEnvironmentInformation(document); foreach(const ParsingEnvironmentFilePointer &file, list) if(isInMemory(file->indexedTopContext().index()) && file->isProxyContext() == proxyContext) { return file->topContext(); } foreach(const ParsingEnvironmentFilePointer &file, list) if(proxyContext == file->isProxyContext()) { return file->topContext(); } //Allow selecting a top-context even if there is no ParsingEnvironmentFile QList< TopDUContext* > ret = chainsForDocument(document); foreach(TopDUContext* ctx, ret) { if(!ctx->parsingEnvironmentFile() || (ctx->parsingEnvironmentFile()->isProxyContext() == proxyContext)) return ctx; } return nullptr; } QList DUChain::chainsForDocument(const QUrl& document) const { return chainsForDocument(IndexedString(document)); } QList DUChain::chainsForDocument(const IndexedString& document) const { QList chains; if(sdDUChainPrivate->m_destroyed) return chains; QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // Match all parsed versions of this document for (auto it = sdDUChainPrivate->m_chainsByUrl.lowerBound(document); it != sdDUChainPrivate->m_chainsByUrl.end(); ++it) { if (it.key() == document) chains << it.value(); else break; } return chains; } TopDUContext* DUChain::chainForDocument( const QUrl& document, const KDevelop::ParsingEnvironment* environment, bool proxyContext ) const { return chainForDocument( IndexedString(document), environment, proxyContext ); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return ParsingEnvironmentFilePointer(); QList< ParsingEnvironmentFilePointer> list = sdDUChainPrivate->getEnvironmentInformation(document); // qCDebug(LANGUAGE) << document.str() << ": matching" << list.size() << (onlyProxyContexts ? "proxy-contexts" : (noProxyContexts ? "content-contexts" : "contexts")); auto it = list.constBegin(); while(it != list.constEnd()) { if(*it && ((*it)->isProxyContext() == proxyContext) && (*it)->matchEnvironment(environment) && // Verify that the environment-file and its top-context are "good": The top-context must exist, // and there must be a content-context associated to the proxy-context. (*it)->topContext() && (!proxyContext || DUChainUtils::contentContextFromProxyContext((*it)->topContext())) ) { return *it; } ++it; } return ParsingEnvironmentFilePointer(); } QList DUChain::allEnvironmentFiles(const IndexedString& document) { return sdDUChainPrivate->getEnvironmentInformation(document); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument(IndexedTopDUContext topContext) const { if(topContext.index() == 0) return ParsingEnvironmentFilePointer(); return ParsingEnvironmentFilePointer(sdDUChainPrivate->loadInformation(topContext.index())); } TopDUContext* DUChain::chainForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { if(sdDUChainPrivate->m_destroyed) return nullptr; ParsingEnvironmentFilePointer envFile = environmentFileForDocument(document, environment, proxyContext); if(envFile) { return envFile->topContext(); }else{ return nullptr; } } QList DUChain::documents() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; ret.reserve(sdDUChainPrivate->m_chainsByUrl.count()); foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl) { ret << top->url().toUrl(); } return ret; } QList DUChain::indexedDocuments() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; ret.reserve(sdDUChainPrivate->m_chainsByUrl.count()); foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl) { ret << top->url(); } return ret; } void DUChain::documentActivated(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; DUChainReadLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); auto backgroundParser = ICore::self()->languageController()->backgroundParser(); auto addWithHighPriority = [backgroundParser, doc]() { backgroundParser->addDocument(IndexedString(doc->url()), TopDUContext::VisibleDeclarationsAndContexts, BackgroundParser::BestPriority); }; TopDUContext* ctx = DUChainUtils::standardContextForUrl(doc->url(), true); //Check whether the document has an attached environment-manager, and whether that one thinks the document needs to be updated. //If yes, update it. if (ctx && ctx->parsingEnvironmentFile() && ctx->parsingEnvironmentFile()->needsUpdate()) { qCDebug(LANGUAGE) << "Document needs update, using best priority since it just got activated:" << doc->url(); addWithHighPriority(); } else if (backgroundParser->managedDocuments().contains(IndexedString(doc->url()))) { // increase priority if there's already parse job of this document in the queue qCDebug(LANGUAGE) << "Prioritizing activated document:" << doc->url(); addWithHighPriority(); } } void DUChain::documentClosed(IDocument* document) { if(sdDUChainPrivate->m_destroyed) return; IndexedString url(document->url()); foreach(const ReferencedTopDUContext &top, sdDUChainPrivate->m_openDocumentContexts) if(top->url() == url) sdDUChainPrivate->m_openDocumentContexts.remove(top); } void DUChain::documentLoadedPrepare(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; const IndexedString url(doc->url()); DUChainWriteLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(doc->url()); QList chains = chainsForDocument(url); auto languages = ICore::self()->languageController()->languagesForUrl(doc->url()); if(standardContext) { Q_ASSERT(chains.contains(standardContext)); //We have just loaded it Q_ASSERT((standardContext->url() == url)); sdDUChainPrivate->m_openDocumentContexts.insert(standardContext); bool needsUpdate = standardContext->parsingEnvironmentFile() && standardContext->parsingEnvironmentFile()->needsUpdate(); if(!needsUpdate) { //Only apply the highlighting if we don't need to update, else we might highlight total crap //Do instant highlighting only if all imports are loaded, to make sure that we don't block the user-interface too long //Else the highlighting will be done in the background-thread //This is not exactly right, as the direct imports don't necessarily equal the real imports used by uses //but it approximates the correct behavior. bool allImportsLoaded = true; foreach(const DUContext::Import& import, standardContext->importedParentContexts()) if(!import.indexedContext().indexedTopContext().isLoaded()) allImportsLoaded = false; if(allImportsLoaded) { l.unlock(); lock.unlock(); foreach(const auto language, languages) { if(language->codeHighlighting()) { language->codeHighlighting()->highlightDUChain(standardContext); } } qCDebug(LANGUAGE) << "highlighted" << doc->url() << "in foreground"; return; } }else{ qCDebug(LANGUAGE) << "not highlighting the duchain because the documents needs an update"; } if(needsUpdate || !(standardContext->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); return; } } //Add for highlighting etc. ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), TopDUContext::AllDeclarationsContextsAndUses); } void DUChain::documentRenamed(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; if(!doc->url().isValid()) { ///Maybe this happens when a file was deleted? qCWarning(LANGUAGE) << "Strange, url of renamed document is invalid!"; }else{ ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); } } Uses* DUChain::uses() { return &sdDUChainPrivate->m_uses; } Definitions* DUChain::definitions() { return &sdDUChainPrivate->m_definitions; } static void finalCleanup() { DUChainWriteLocker writeLock(DUChain::lock()); qCDebug(LANGUAGE) << "doing final cleanup"; int cleaned = 0; while((cleaned = globalItemRepositoryRegistry().finalCleanup())) { qCDebug(LANGUAGE) << "cleaned" << cleaned << "B"; if(cleaned < 1000) { qCDebug(LANGUAGE) << "cleaned enough"; break; } } qCDebug(LANGUAGE) << "final cleanup ready"; } void DUChain::shutdown() { // if core is not shutting down, we can end up in deadlocks or crashes // since language plugins might still try to access static duchain stuff Q_ASSERT(!ICore::self() || ICore::self()->shuttingDown()); qCDebug(LANGUAGE) << "Cleaning up and shutting down DUChain"; QMutexLocker lock(&sdDUChainPrivate->cleanupMutex()); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); sdDUChainPrivate->cleanupTopContexts(); globalItemRepositoryRegistry().unlockForWriting(); } sdDUChainPrivate->doMoreCleanup(); //Must be done _before_ finalCleanup, else we may be deleting yet needed data sdDUChainPrivate->m_openDocumentContexts.clear(); sdDUChainPrivate->m_destroyed = true; sdDUChainPrivate->clear(); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); finalCleanup(); globalItemRepositoryRegistry().unlockForWriting(); } globalItemRepositoryRegistry().shutdown(); } uint DUChain::newTopContextIndex() { { QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); if(!sdDUChainPrivate->m_availableTopContextIndices.isEmpty()) { uint ret = sdDUChainPrivate->m_availableTopContextIndices.back(); sdDUChainPrivate->m_availableTopContextIndices.pop_back(); if(TopDUContextDynamicData::fileExists(ret)) { qCWarning(LANGUAGE) << "Problem in the management of availalbe top-context indices"; return newTopContextIndex(); } return ret; } } static QAtomicInt& currentId( globalItemRepositoryRegistry().getCustomCounter(QStringLiteral("Top-Context Counter"), 1) ); return currentId.fetchAndAddRelaxed(1); } void DUChain::refCountUp(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); // note: value is default-constructed to zero if it does not exist ++sdDUChainPrivate->m_referenceCounts[top]; } bool DUChain::deleted() { return m_deleted; } void DUChain::refCountDown(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); auto it = sdDUChainPrivate->m_referenceCounts.find(top); if (it == sdDUChainPrivate->m_referenceCounts.end()) { //qCWarning(LANGUAGE) << "tried to decrease reference-count for" << top->url().str() << "but this top-context is not referenced"; return; } auto& refCount = *it; --refCount; if (!refCount) { sdDUChainPrivate->m_referenceCounts.erase(it); } } void DUChain::emitDeclarationSelected(const DeclarationPointer& decl) { if(sdDUChainPrivate->m_destroyed) return; emit declarationSelected(decl); } void DUChain::emitUpdateReady(const IndexedString& url, const ReferencedTopDUContext& topContext) { if(sdDUChainPrivate->m_destroyed) return; emit updateReady(url, topContext); } KDevelop::ReferencedTopDUContext DUChain::waitForUpdate(const KDevelop::IndexedString& document, KDevelop::TopDUContext::Features minFeatures, bool proxyContext) { Q_ASSERT(!lock()->currentThreadHasReadLock() && !lock()->currentThreadHasWriteLock()); WaitForUpdate waiter; updateContextForUrl(document, minFeatures, &waiter); // waiter.m_waitMutex.lock(); // waiter.m_dataMutex.unlock(); while(!waiter.m_ready) { // we might have been shut down in the meanwhile if (!ICore::self()) { return nullptr; } QMetaObject::invokeMethod(ICore::self()->languageController()->backgroundParser(), "parseDocuments"); QApplication::processEvents(); QThread::usleep(1000); } if(!proxyContext) { DUChainReadLocker readLock(DUChain::lock()); return DUChainUtils::contentContextFromProxyContext(waiter.m_topContext); } return waiter.m_topContext; } void DUChain::updateContextForUrl(const IndexedString& document, TopDUContext::Features minFeatures, QObject* notifyReady, int priority) const { DUChainReadLocker lock( DUChain::lock() ); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(document.toUrl()); if(standardContext && standardContext->parsingEnvironmentFile() && !standardContext->parsingEnvironmentFile()->needsUpdate() && standardContext->parsingEnvironmentFile()->featuresSatisfied(minFeatures)) { lock.unlock(); if(notifyReady) QMetaObject::invokeMethod(notifyReady, "updateReady", Qt::DirectConnection, Q_ARG(KDevelop::IndexedString, document), Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext(standardContext))); }else{ ///Start a parse-job for the given document ICore::self()->languageController()->backgroundParser()->addDocument(document, minFeatures, priority, notifyReady); } } void DUChain::disablePersistentStorage(bool disable) { sdDUChainPrivate->m_cleanupDisabled = disable; } void DUChain::storeToDisk() { bool wasDisabled = sdDUChainPrivate->m_cleanupDisabled; sdDUChainPrivate->m_cleanupDisabled = false; sdDUChainPrivate->doMoreCleanup(); sdDUChainPrivate->m_cleanupDisabled = wasDisabled; } bool DUChain::compareToDisk() { DUChainWriteLocker writeLock(DUChain::lock()); ///Step 1: Compare the repositories return true; } } diff --git a/plugins/execute/nativeappconfig.h b/plugins/execute/nativeappconfig.h index 792067dad..7b4a9527e 100644 --- a/plugins/execute/nativeappconfig.h +++ b/plugins/execute/nativeappconfig.h @@ -1,94 +1,94 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat 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. */ #ifndef KDEVPLATFORM_PLUGIN_NATIVEAPPCONFIGTYPE_H #define KDEVPLATFORM_PLUGIN_NATIVEAPPCONFIGTYPE_H #include #include #include #include #include "ui_nativeappconfig.h" //TODO: Split the page into two, one concerning executable/arguments/behaviour the other for dependencies class NativeAppConfigPage : public KDevelop::LaunchConfigurationPage, Ui::NativeAppPage { Q_OBJECT public: explicit NativeAppConfigPage( QWidget* parent ); void loadFromConfiguration( const KConfigGroup& cfg, KDevelop::IProject* project = nullptr ) override; void saveToConfiguration( KConfigGroup cfg, KDevelop::IProject* project = nullptr ) const override; QString title() const override; QIcon icon() const override; private slots: void activateDeps( int ); }; class NativeAppLauncher : public KDevelop::ILauncher { public: NativeAppLauncher(); QList< KDevelop::LaunchConfigurationPageFactory* > configPages() const override; QString description() const override; QString id() override; QString name() const override; KJob* start(const QString& launchMode, KDevelop::ILaunchConfiguration* cfg) override; QStringList supportedModes() const override; }; class NativeAppPageFactory : public KDevelop::LaunchConfigurationPageFactory { public: NativeAppPageFactory(); KDevelop::LaunchConfigurationPage* createWidget(QWidget* parent) override; }; /** * A specific configuration to start a launchable, this could be a native * compiled application, or some script file or byte-compiled file or something else - * Provides access to the various configured informations, as well as its type and a name + * Provides access to the various configured information, as well as its type and a name */ class NativeAppConfigType : public KDevelop::LaunchConfigurationType { Q_OBJECT public: NativeAppConfigType(); ~NativeAppConfigType() override; QString id() const override; QString name() const override; QList configPages() const override; QIcon icon() const override; bool canLaunch( KDevelop::ProjectBaseItem* item ) const override; bool canLaunch( const QUrl& file ) const override; void configureLaunchFromItem ( KConfigGroup cfg, KDevelop::ProjectBaseItem* item ) const override; void configureLaunchFromCmdLineArguments ( KConfigGroup cfg, const QStringList& args ) const override; QMenu* launcherSuggestions() override; private: QList factoryList; public slots: void suggestionTriggered(); }; #endif diff --git a/plugins/executescript/scriptappconfig.h b/plugins/executescript/scriptappconfig.h index 77edc8aac..25b2c8cf7 100644 --- a/plugins/executescript/scriptappconfig.h +++ b/plugins/executescript/scriptappconfig.h @@ -1,90 +1,90 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat Copyright 2009 Niko Sams 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. */ #ifndef KDEVPLATFORM_PLUGIN_SCRIPTAPPCONFIGTYPE_H #define KDEVPLATFORM_PLUGIN_SCRIPTAPPCONFIGTYPE_H #include #include #include #include #include "ui_scriptappconfig.h" class ExecuteScriptPlugin; class ScriptAppConfigPage : public KDevelop::LaunchConfigurationPage, Ui::ScriptAppPage { Q_OBJECT public: explicit ScriptAppConfigPage( QWidget* parent ); void loadFromConfiguration( const KConfigGroup& cfg, KDevelop::IProject* project = nullptr ) override; void saveToConfiguration( KConfigGroup cfg, KDevelop::IProject* project = nullptr ) const override; QString title() const override; QIcon icon() const override; }; class ScriptAppLauncher : public KDevelop::ILauncher { public: explicit ScriptAppLauncher( ExecuteScriptPlugin* ); QList< KDevelop::LaunchConfigurationPageFactory* > configPages() const override; QString description() const override; QString id() override; QString name() const override; KJob* start(const QString& launchMode, KDevelop::ILaunchConfiguration* cfg) override; QStringList supportedModes() const override; private: ExecuteScriptPlugin* m_plugin; }; class ScriptAppPageFactory : public KDevelop::LaunchConfigurationPageFactory { public: ScriptAppPageFactory(); KDevelop::LaunchConfigurationPage* createWidget(QWidget* parent) override; }; /** * A specific configuration to start a launchable, this could be a native * compiled application, or some script file or byte-compiled file or something else - * Provides access to the various configured informations, as well as its type and a name + * Provides access to the various configured information, as well as its type and a name */ class ScriptAppConfigType : public KDevelop::LaunchConfigurationType { Q_OBJECT public: ScriptAppConfigType(); ~ScriptAppConfigType() override; QString id() const override; QString name() const override; QList configPages() const override; QIcon icon() const override; bool canLaunch( const QUrl& file ) const override; bool canLaunch(KDevelop::ProjectBaseItem* item) const override; void configureLaunchFromItem(KConfigGroup config, KDevelop::ProjectBaseItem* item) const override; void configureLaunchFromCmdLineArguments(KConfigGroup config, const QStringList& args) const override; private: QList factoryList; }; #endif diff --git a/plugins/externalscript/externalscriptview.cpp b/plugins/externalscript/externalscriptview.cpp index 656d9028e..db1b75046 100644 --- a/plugins/externalscript/externalscriptview.cpp +++ b/plugins/externalscript/externalscriptview.cpp @@ -1,181 +1,181 @@ /* This plugin is part of KDevelop. Copyright (C) 2010 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "externalscriptview.h" #include "externalscriptplugin.h" #include "externalscriptitem.h" #include "editexternalscript.h" #include #include #include #include #include #include ExternalScriptView::ExternalScriptView( ExternalScriptPlugin* plugin, QWidget* parent ) : QWidget( parent ), m_plugin( plugin ) { Ui::ExternalScriptViewBase::setupUi( this ); setFocusProxy(filterText); setWindowTitle( i18n( "External Scripts" ) ); setWindowIcon( QIcon::fromTheme(QStringLiteral("dialog-scripts"), windowIcon()) ); m_model = new QSortFilterProxyModel( this ); m_model->setSourceModel( m_plugin->model() ); m_model->setDynamicSortFilter( true ); m_model->sort( 0 ); connect( filterText, &QLineEdit::textEdited, m_model, &QSortFilterProxyModel::setFilterWildcard ); scriptTree->setModel( m_model ); scriptTree->setContextMenuPolicy( Qt::CustomContextMenu ); scriptTree->viewport()->installEventFilter( this ); scriptTree->header()->hide(); connect(scriptTree, &QTreeView::customContextMenuRequested, this, &ExternalScriptView::contextMenu); m_addScriptAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add External Script"), this); connect(m_addScriptAction, &QAction::triggered, this, &ExternalScriptView::addScript); addAction(m_addScriptAction); m_editScriptAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit External Script"), this); connect(m_editScriptAction, &QAction::triggered, this, &ExternalScriptView::editScript); addAction(m_editScriptAction); m_removeScriptAction = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Remove External Script"), this); connect(m_removeScriptAction, &QAction::triggered, this, &ExternalScriptView::removeScript); addAction(m_removeScriptAction); connect(scriptTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExternalScriptView::validateActions); validateActions(); } ExternalScriptView::~ExternalScriptView() { } ExternalScriptItem* ExternalScriptView::currentItem() const { return itemForIndex( scriptTree->currentIndex() ); } ExternalScriptItem* ExternalScriptView::itemForIndex( const QModelIndex& index ) const { if ( !index.isValid() ) { return nullptr; } const QModelIndex mappedIndex = m_model->mapToSource( index ); return static_cast( m_plugin->model()->itemFromIndex( mappedIndex ) ); } void ExternalScriptView::validateActions() { bool itemSelected = currentItem(); m_removeScriptAction->setEnabled( itemSelected ); m_editScriptAction->setEnabled( itemSelected ); } void ExternalScriptView::contextMenu( const QPoint& pos ) { QMenu menu; menu.addActions( actions() ); menu.exec( scriptTree->mapToGlobal( pos ) ); } bool ExternalScriptView::eventFilter( QObject* obj, QEvent* e ) { - // no, listening to activated() is not enough since that would also trigger the edit mode which we _dont_ want here + // no, listening to activated() is not enough since that would also trigger the edit mode which we do _not_ want here // users may still rename stuff via select + F2 though if ( obj == scriptTree->viewport() ) { // const bool singleClick = KGlobalSettings::singleClick(); const bool singleClick = true; //FIXME: enable singleClick for the sake of porting, should find a proper way if ( ( !singleClick && e->type() == QEvent::MouseButtonDblClick ) || ( singleClick && e->type() == QEvent::MouseButtonRelease ) ) { QMouseEvent* mouseEvent = dynamic_cast(e); Q_ASSERT( mouseEvent ); ExternalScriptItem* item = itemForIndex( scriptTree->indexAt( mouseEvent->pos() ) ); if ( item ) { m_plugin->execute( item ); e->accept(); return true; } } } return QObject::eventFilter( obj, e ); } void ExternalScriptView::addScript() { ExternalScriptItem* item = new ExternalScriptItem; EditExternalScript dlg( item, this ); int ret = dlg.exec(); if ( ret == QDialog::Accepted) { m_plugin->model()->appendRow( item ); } else { delete item; } } void ExternalScriptView::removeScript() { ExternalScriptItem* item = currentItem(); if ( !item ) { return; } int ret = KMessageBox::questionYesNo( this, i18n("

Do you really want to remove the external script configuration for %1?

" "

Note: The script itself will not be removed.

", item->text()), i18n("Confirm External Script Removal") ); if ( ret == KMessageBox::Yes ) { m_plugin->model()->removeRow( m_plugin->model()->indexFromItem( item ).row() ); } } void ExternalScriptView::editScript() { ExternalScriptItem* item = currentItem(); if ( !item ) { return; } EditExternalScript dlg( item, this ); int ret = dlg.exec(); if (ret == QDialog::Accepted) { item->save(); } } // kate: indent-mode cstyle; space-indent on; indent-width 2; replace-tabs on; diff --git a/plugins/problemreporter/tests/test_problemsview.cpp b/plugins/problemreporter/tests/test_problemsview.cpp index a4dc8d909..db4776872 100644 --- a/plugins/problemreporter/tests/test_problemsview.cpp +++ b/plugins/problemreporter/tests/test_problemsview.cpp @@ -1,211 +1,211 @@ /* * Copyright 2015 Laszlo Kis-Adam * * 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 #include #include #include "../problemsview.h" #include #include #include #include #include #include using namespace KDevelop; class TestProblemsView : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testLoad(); void testAddModel(); void testSwitchTab(); void testRemoveModel(); void testAddRemoveProblems(); void testSetProblems(); private: QTabWidget* tabWidget(); QScopedPointer m_view; }; void TestProblemsView::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); ProblemModelSet* pms = ICore::self()->languageController()->problemModelSet(); ProblemModel* model = new ProblemModel(pms); IProblem::Ptr p(new DetectedProblem()); model->addProblem(p); pms->addModel(QStringLiteral("MODEL1_ID"), QStringLiteral("MODEL1"), model); m_view.reset(new ProblemsView()); } void TestProblemsView::cleanupTestCase() { TestCore::shutdown(); } void TestProblemsView::testLoad() { m_view->load(); - // Check that the inital model's tab shows up + // Check that the initial model's tab shows up QTabWidget* tab = tabWidget(); QVERIFY(tab); QCOMPARE(tab->count(), 1); QCOMPARE(tab->tabText(0), QStringLiteral("MODEL1 (1)")); } void TestProblemsView::testAddModel() { ProblemModelSet* pms = ICore::self()->languageController()->problemModelSet(); pms->addModel(QStringLiteral("MODEL2_ID"), QStringLiteral("MODEL2"), new ProblemModel(pms)); QTabWidget* tab = tabWidget(); QVERIFY(tab); QCOMPARE(tab->count(), 2); QCOMPARE(tab->tabText(0), QStringLiteral("MODEL1 (1)")); QCOMPARE(tab->tabText(1), QStringLiteral("MODEL2 (0)")); } QVector visibilites(const QList actions) { QVector visibilites; foreach (auto action, actions) { visibilites << action->isVisible(); } return visibilites; } void TestProblemsView::testSwitchTab() { QTabWidget* tab = tabWidget(); QVERIFY(tab); // Check that the current widget's actions are in the toolbar QWidget* oldWidget = tab->currentWidget(); QVERIFY(oldWidget); const auto oldVisibilites = visibilites(m_view->actions()); tab->setCurrentIndex(1); // Check that the new widget's actions are in the toolbar QWidget* newWidget = tab->currentWidget(); QVERIFY(newWidget); QVERIFY(newWidget != oldWidget); const auto newVisibilites = visibilites(m_view->actions()); QCOMPARE(oldVisibilites, newVisibilites); } void TestProblemsView::testRemoveModel() { // Remove the model ProblemModelSet* pms = ICore::self()->languageController()->problemModelSet(); ProblemModel* model = pms->findModel(QStringLiteral("MODEL1_ID")); QVERIFY(model); pms->removeModel(QStringLiteral("MODEL1_ID")); delete model; model = nullptr; // Now let's see if the view has been updated! QTabWidget* tab = tabWidget(); QVERIFY(tab); QCOMPARE(tab->count(), 1); QCOMPARE(tab->tabText(0), QStringLiteral("MODEL2 (0)")); } void TestProblemsView::testAddRemoveProblems() { ProblemModelSet* pms = ICore::self()->languageController()->problemModelSet(); ProblemModel* model = pms->findModel(QStringLiteral("MODEL2_ID")); QVERIFY(model); QTabWidget* tab = tabWidget(); QVERIFY(tab); // Make sure there are no problems right now model->clearProblems(); QCOMPARE(tab->tabText(0), QStringLiteral("MODEL2 (0)")); // Let's add some problems int c = 0; for (int i = 0; i < 3; i++) { IProblem::Ptr p(new DetectedProblem()); model->addProblem(p); c++; // Check if the view has noticed the addition QString label = QStringLiteral("MODEL2 (%1)").arg(c); QCOMPARE(tab->tabText(0), label); } // Clear the problems model->clearProblems(); // Check if the view has noticed the clear QCOMPARE(tab->tabText(0), QStringLiteral("MODEL2 (0)")); } void TestProblemsView::testSetProblems() { ProblemModelSet* pms = ICore::self()->languageController()->problemModelSet(); ProblemModel* model = pms->findModel(QStringLiteral("MODEL2_ID")); QVERIFY(model); QTabWidget* tab = tabWidget(); QVERIFY(tab); // Make sure there are no problems right now model->clearProblems(); QCOMPARE(tab->tabText(0), QStringLiteral("MODEL2 (0)")); // Build a problem vector and set the problems QVector problems; for (int i = 0; i < 3; i++) { IProblem::Ptr p(new DetectedProblem()); problems.push_back(p); } model->setProblems(problems); // Check if the view has noticed QCOMPARE(tab->tabText(0), QStringLiteral("MODEL2 (3)")); } ////////////////////////////////////////////////////////////////////////////////////////////////////////// QTabWidget* TestProblemsView::tabWidget() { QTabWidget* tab = m_view->findChild(); return tab; } QTEST_MAIN(TestProblemsView) #include "test_problemsview.moc" diff --git a/plugins/subversion/kdevsvncpp/client_cat.cpp b/plugins/subversion/kdevsvncpp/client_cat.cpp index 08ca6e487..9c4b4a6c7 100644 --- a/plugins/subversion/kdevsvncpp/client_cat.cpp +++ b/plugins/subversion/kdevsvncpp/client_cat.cpp @@ -1,178 +1,178 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ #if defined( _MSC_VER) && _MSC_VER <= 1200 #pragma warning( disable: 4786 )// debug symbol truncated #endif // stl #include // Subversion api #include "svn_client.h" // svncpp #include "kdevsvncpp/client.hpp" #include "kdevsvncpp/exception.hpp" #include "kdevsvncpp/pool.hpp" #include "kdevsvncpp/status.hpp" #include "m_check.hpp" namespace svn { std::string Client::cat(const Path & path, const Revision & revision, const Revision & peg_revision) throw(ClientException) { Pool pool; svn_stringbuf_t * stringbuf = svn_stringbuf_create("", pool); svn_stream_t * stream = svn_stream_from_stringbuf(stringbuf, pool); svn_error_t * error; error = svn_client_cat2(stream, path.c_str(), peg_revision.revision(), revision.revision(), *m_context, pool); if (error != nullptr) throw ClientException(error); return std::string(stringbuf->data, stringbuf->len); } /** * Create a new temporary file in @a dstPath. If @a dstPath * is empty (""), then construct the temporary filename * from the temporary directory and the filename component * of @a path. The file-extension of @a path will be transformed * to @a dstPath and @a dstPath will be a unique filename * * @param dstPath path to temporary file. Will be constructed * from @a path and temporary dir (and unique elements) * if empty string * @param path existing filename. Necessary only for construction * of @a dstPath * @param pool pool to use * @return open file */ static apr_file_t * openTempFile(Path & dstPath, const Path & path, const Revision & revision, Pool & pool) throw(ClientException) { apr_file_t * file = nullptr; if (dstPath.length() > 0) { apr_status_t status = apr_file_open(&file, dstPath.c_str(), APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY, APR_OS_DEFAULT, pool); if (status != 0) throw ClientException(status); } else { // split the path into its components std::string dir, filename, ext; path.split(dir, filename, ext); // add the revision number to the filename char revstring[20]; if (revision.kind() == revision.HEAD) strcpy(revstring, "HEAD"); else sprintf(revstring, "%" SVN_REVNUM_T_FMT, revision.revnum()); filename += '-'; filename += revstring; // replace the dir component with tempdir Path tempPath = Path::getTempDir(); tempPath.addComponent(filename); const char * unique_name; svn_error_t * error = svn_io_open_unique_file( &file, &unique_name, tempPath.c_str(), // path ext.c_str(), // suffix - 0, // dont delete on close + 0, // do not delete on close pool); if (error != nullptr) throw ClientException(error); dstPath = unique_name; } return file; } void Client::get(Path & dstPath, const Path & path, const Revision & revision, const Revision & peg_revision) throw(ClientException) { Pool pool; // create a new file and suppose we only want // this users to be able to read and write the file apr_file_t * file = openTempFile(dstPath, path, revision, pool); // now create a stream and let svn_client_cat write to the // stream svn_stream_t * stream = svn_stream_from_aprfile(file, pool); if (stream != nullptr) { svn_error_t * error = svn_client_cat2( stream, path.c_str(), peg_revision.revision() , revision.revision(), *m_context, pool); if (error != nullptr) throw ClientException(error); svn_stream_close(stream); } // finalize stuff apr_file_close(file); } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/subversion/kdevsvncpp/client_status.cpp b/plugins/subversion/kdevsvncpp/client_status.cpp index 66c215b65..954cb420d 100644 --- a/plugins/subversion/kdevsvncpp/client_status.cpp +++ b/plugins/subversion/kdevsvncpp/client_status.cpp @@ -1,418 +1,418 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ #if defined( _MSC_VER) && _MSC_VER <= 1200 #pragma warning( disable: 4786 )// debug symbol truncated #endif // Stdlib (for strcmp) #include "string.h" // Subversion api #include "svn_client.h" #include "svn_sorts.h" //#include "svn_utf.h" // svncpp #include "kdevsvncpp/client.hpp" #include "kdevsvncpp/dirent.hpp" #include "kdevsvncpp/exception.hpp" #include "kdevsvncpp/info.hpp" #include "kdevsvncpp/pool.hpp" #include "kdevsvncpp/status.hpp" #include "kdevsvncpp/targets.hpp" #include "kdevsvncpp/url.hpp" namespace svn { static svn_error_t * logReceiver(void *baton, apr_hash_t * changedPaths, svn_revnum_t rev, const char *author, const char *date, const char *msg, apr_pool_t * pool) { LogEntries * entries = (LogEntries *) baton; entries->insert(entries->begin(), LogEntry(rev, author, date, msg)); if (changedPaths != nullptr) { LogEntry &entry = entries->front(); for (apr_hash_index_t *hi = apr_hash_first(pool, changedPaths); hi != nullptr; hi = apr_hash_next(hi)) { char *path; void *val; apr_hash_this(hi, (const void **)&path, nullptr, &val); svn_log_changed_path_t *log_item = reinterpret_cast(val); entry.changedPaths.push_back( LogChangePathEntry(path, log_item->action, log_item->copyfrom_path, log_item->copyfrom_rev)); } } return nullptr; } static void statusEntriesFunc(void *baton, const char *path, svn_wc_status2_t *status) { StatusEntries * entries = static_cast(baton); entries->push_back(Status(path, status)); } static StatusEntries localStatus(const char * path, const bool descend, const bool get_all, const bool update, const bool no_ignore, Context * context, const bool ignore_externals) { svn_error_t *error; StatusEntries entries; svn_revnum_t revnum; Revision rev(Revision::HEAD); Pool pool; error = svn_client_status2( &revnum, // revnum path, // path rev, // revision statusEntriesFunc, // status func &entries, // status baton descend, // recurse get_all, update, // need 'update' to be true to get repository lock info no_ignore, ignore_externals, // ignore_externals *context, // client ctx pool); if (error!=nullptr) throw ClientException(error); return entries; } static Status dirEntryToStatus(const char * path, const DirEntry & dirEntry) { Pool pool; svn_wc_entry_t * e = static_cast( apr_pcalloc(pool, sizeof(svn_wc_entry_t))); std::string url(path); url += '/'; url += dirEntry.name(); e->name = dirEntry.name(); e->revision = dirEntry.createdRev(); e->url = url.c_str(); e->kind = dirEntry.kind(); e->schedule = svn_wc_schedule_normal; e->text_time = dirEntry.time(); e->prop_time = dirEntry.time(); e->cmt_rev = dirEntry.createdRev(); e->cmt_date = dirEntry.time(); e->cmt_author = dirEntry.lastAuthor(); svn_wc_status2_t * s = static_cast( apr_pcalloc(pool, sizeof(svn_wc_status2_t))); s->entry = e; s->text_status = svn_wc_status_normal; s->prop_status = svn_wc_status_normal; s->locked = 0; s->switched = 0; s->repos_text_status = svn_wc_status_normal; s->repos_prop_status = svn_wc_status_normal; return Status(url.c_str(), s); } static svn_revnum_t remoteStatus(Client * client, const char * path, const bool descend, StatusEntries & entries, Context * /*context*/) { Revision rev(Revision::HEAD); DirEntries dirEntries = client->list(path, rev, descend); DirEntries::const_iterator it; svn_revnum_t revnum = 0; for (it = dirEntries.begin(); it != dirEntries.end(); ++it) { const DirEntry & dirEntry = *it; entries.push_back(dirEntryToStatus(path, dirEntry)); } if (dirEntries.size() > 0) revnum = dirEntries[0].createdRev(); return revnum; } StatusEntries Client::status(const char * path, const bool descend, const bool get_all, const bool update, const bool no_ignore, const bool ignore_externals) throw(ClientException) { if (Url::isValid(path)) { StatusEntries entries; remoteStatus(this, path, descend, entries, m_context); return entries; } else return localStatus(path, descend, get_all, update, no_ignore, m_context, ignore_externals); } struct StatusFilter; struct StatusBaton { public: const StatusFilter & filter; StatusEntries & entries; StatusBaton(const StatusFilter & filter_, StatusEntries & entries_) : filter(filter_), entries(entries_) { } }; static void filteredStatusFunc(void *baton_, const char *path, svn_wc_status2_t *status) { StatusBaton * baton = static_cast(baton_); // now we have to decide whether to return the entry or not if (nullptr == status) return; bool useStatus = false; bool isUnversioned = nullptr == status->entry; if (isUnversioned) { // unversioned if (baton->filter.showUnversioned) useStatus = true; } else { bool isUnmodified = ((svn_wc_status_normal == status->text_status) && (svn_wc_status_normal == status->prop_status)); if (isUnmodified) { if (baton->filter.showUnmodified) useStatus = true; } else { // so here we know its modified. // what are we interested in? if (baton->filter.showModified) useStatus = true; else if (baton->filter.showConflicted) { if (svn_wc_status_conflicted == status->text_status) useStatus = true; } } } if (useStatus) baton->entries.push_back(Status(path, status)); } static svn_revnum_t localFilteredStatus(const char * path, const StatusFilter & filter, const bool descend, const bool update, StatusEntries & entries, Context * context) { svn_error_t *error; svn_revnum_t revnum; Revision rev(Revision::HEAD); Pool pool; StatusBaton baton(filter, entries); error = svn_client_status2( &revnum, // revnum path, // path rev, // revision filteredStatusFunc, // status func &baton, // status baton descend, // recurse filter.showUnmodified, update, // need 'update' to be true to get repository lock info filter.showIgnored, // no_ignores !filter.showExternals, // ignore_externals *context, // client ctx pool); if (error!=nullptr) throw ClientException(error); return revnum; } svn_revnum_t Client::status(const char * path, const StatusFilter & filter, const bool descend, const bool update, StatusEntries & entries) throw(ClientException) { entries.clear(); if (Url::isValid(path)) return remoteStatus(this, path, descend, entries, m_context); else { // remote URLs only need a subset of the filters: - // we dont expect any modified, conflicting, unknown, + // we do not expect any modified, conflicting, unknown, // ignored entries. And externals arent visible there anyhow return localFilteredStatus( path, filter, descend, update, entries, m_context); } } const LogEntries * Client::log(const char * path, const Revision & revisionStart, const Revision & revisionEnd, bool discoverChangedPaths, bool strictNodeHistory) throw(ClientException) { Pool pool; Targets target(path); LogEntries * entries = new LogEntries(); svn_error_t *error; int limit = 0; error = svn_client_log2( target.array(pool), revisionStart.revision(), revisionEnd.revision(), limit, discoverChangedPaths ? 1 : 0, strictNodeHistory ? 1 : 0, logReceiver, entries, *m_context, // client ctx pool); if (error != nullptr) { delete entries; throw ClientException(error); } return entries; } /** * callback function for Client::info, will be * called for every entry svn_client_info wants to * return */ static svn_error_t * infoReceiverFunc(void * baton, const char * path, const svn_info_t * info, apr_pool_t * /*pool*/) { InfoVector * infoVector = static_cast(baton); infoVector->push_back(Info(path, info)); return nullptr; } InfoVector Client::info(const Path & pathOrUrl, bool recurse, const Revision & revision, const Revision & pegRevision) throw(ClientException) { Pool pool; InfoVector infoVector; svn_error_t * error = svn_client_info(pathOrUrl.c_str(), pegRevision.revision(), revision.revision(), infoReceiverFunc, &infoVector, recurse, *m_context, pool); if (error != nullptr) throw ClientException(error); return infoVector; } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/subversion/kdevsvncpp/context.cpp b/plugins/subversion/kdevsvncpp/context.cpp index 5e0e3e0cf..30e154094 100644 --- a/plugins/subversion/kdevsvncpp/context.cpp +++ b/plugins/subversion/kdevsvncpp/context.cpp @@ -1,716 +1,716 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ /** * @todo implement 1.3 SVN api: * * SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN * svn_client_add3 * svn_client_copy2 * svn_client_commit3 * svn_client_delete2 * svn_client_move3 * svn_client_mkdir2 * svn_client_import2 * svn_client_info */ // Apache Portable Runtime #include "apr_xlate.h" // Subversion api #include "svn_auth.h" #include "svn_config.h" #include "svn_subst.h" //#include "svn_utf.h" // svncpp #include "kdevsvncpp/apr.hpp" #include "kdevsvncpp/context.hpp" #include "kdevsvncpp/context_listener.hpp" namespace svn { struct Context::Data { public: /** The usage of Apr makes sure Apr is initialized * before any use of apr functions. */ Apr apr; ContextListener * listener; bool logIsSet; int promptCounter; Pool pool; svn_client_ctx_t * ctx; std::string username; std::string password; std::string logMessage; std::string configDir; /** * translate native c-string to utf8 */ static svn_error_t * translateString(const char * str, const char ** newStr, apr_pool_t * /*pool*/) { - // due to problems with apr_xlate we dont perform + // due to problems with apr_xlate we do not perform // any conversion at this place. YOU will have to make // sure any strings passed are UTF 8 strings // svn_string_t *string = svn_string_create ("", pool); // // string->data = str; // string->len = strlen (str); // // const char * encoding = APR_LOCALE_CHARSET; // // SVN_ERR (svn_subst_translate_string (&string, string, // encoding, pool)); // // *newStr = string->data; *newStr = str; return SVN_NO_ERROR; } /** * the @a baton is interpreted as Data * * Several checks are performed on the baton: * - baton == 0? * - baton->Data * - listener set? * * @param data returned data if everything is OK * @retval SVN_NO_ERROR if everything is fine * @retval SVN_ERR_CANCELLED on invalid values */ static svn_error_t * getData(void * baton, Data ** data) { if (baton == nullptr) return svn_error_create(SVN_ERR_CANCELLED, nullptr, "invalid baton"); Data * data_ = static_cast (baton); if (data_->listener == nullptr) return svn_error_create(SVN_ERR_CANCELLED, nullptr, "invalid listener"); *data = data_; return SVN_NO_ERROR; } Data(const std::string & configDir_) : listener(nullptr), logIsSet(false), promptCounter(0), configDir(configDir_) { const char * c_configDir = nullptr; if (configDir.length() > 0) c_configDir = configDir.c_str(); // make sure the configuration directory exists svn_config_ensure(c_configDir, pool); // initialize authentication providers // * simple // * username // * simple prompt // * ssl server trust file // * ssl server trust prompt // * ssl client cert pw file // * ssl client cert pw prompt // * ssl client cert file // =================== // 8 providers apr_array_header_t *providers = apr_array_make(pool, 8, sizeof(svn_auth_provider_object_t *)); svn_auth_provider_object_t *provider; svn_client_get_simple_provider( &provider, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_client_get_username_provider( &provider, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_client_get_simple_prompt_provider( &provider, onSimplePrompt, this, 100000000, // not very nice. should be infinite... pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; // add ssl providers // file first then prompt providers svn_client_get_ssl_server_trust_file_provider(&provider, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_client_get_ssl_client_cert_file_provider(&provider, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_client_get_ssl_client_cert_pw_file_provider(&provider, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_client_get_ssl_server_trust_prompt_provider( &provider, onSslServerTrustPrompt, this, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; // plugged in 3 as the retry limit - what is a good limit? svn_client_get_ssl_client_cert_pw_prompt_provider( &provider, onSslClientCertPwPrompt, this, 3, pool); *(svn_auth_provider_object_t **)apr_array_push(providers) = provider; svn_auth_baton_t *ab; svn_auth_open(&ab, providers, pool); // initialize ctx structure svn_client_create_context(&ctx, pool); // get the config based on the configDir passed in svn_config_get_config(&ctx->config, c_configDir, pool); // disable external diff and diff3 commands svn_config_t *config = (svn_config_t *)apr_hash_get( ctx->config, SVN_CONFIG_CATEGORY_CONFIG, APR_HASH_KEY_STRING); svn_config_set(config, SVN_CONFIG_SECTION_HELPERS, SVN_CONFIG_OPTION_DIFF_CMD, nullptr); svn_config_set(config, SVN_CONFIG_SECTION_HELPERS, SVN_CONFIG_OPTION_DIFF3_CMD, nullptr); // tell the auth functions where the config is svn_auth_set_parameter(ab, SVN_AUTH_PARAM_CONFIG_DIR, c_configDir); ctx->auth_baton = ab; ctx->log_msg_func = onLogMsg; ctx->log_msg_baton = this; ctx->notify_func = onNotify; ctx->notify_baton = this; ctx->cancel_func = onCancel; ctx->cancel_baton = this; ctx->notify_func2 = onNotify2; ctx->notify_baton2 = this; } void setAuthCache(bool value) { void *param = nullptr; if (!value) param = (void *)"1"; svn_auth_set_parameter(ctx->auth_baton, SVN_AUTH_PARAM_NO_AUTH_CACHE, param); } /** @see Context::setLogin */ void setLogin(const char * usr, const char * pwd) { username = usr; password = pwd; svn_auth_baton_t * ab = ctx->auth_baton; svn_auth_set_parameter(ab, SVN_AUTH_PARAM_DEFAULT_USERNAME, username.c_str()); svn_auth_set_parameter(ab, SVN_AUTH_PARAM_DEFAULT_PASSWORD, password.c_str()); } /** @see Context::setLogMessage */ void setLogMessage(const char * msg) { logMessage = msg; logIsSet = true; } /** * this function gets called by the subversion api function * when a log message is needed. This is the case on a commit * for example */ static svn_error_t * onLogMsg(const char **log_msg, const char **tmp_file, apr_array_header_t *, //UNUSED commit_items void *baton, apr_pool_t * pool) { Data * data = nullptr; SVN_ERR(getData(baton, &data)); std::string msg; if (data->logIsSet) msg = data->getLogMessage(); else { if (!data->retrieveLogMessage(msg)) return svn_error_create(SVN_ERR_CANCELLED, nullptr, ""); } *log_msg = apr_pstrdup(pool, msg.c_str()); *tmp_file = nullptr; return SVN_NO_ERROR; } /** * this is the callback function for the subversion * api functions to signal the progress of an action */ static void onNotify(void * baton, const char *path, svn_wc_notify_action_t action, svn_node_kind_t kind, const char *mime_type, svn_wc_notify_state_t content_state, svn_wc_notify_state_t prop_state, svn_revnum_t revision) { if (baton == nullptr) return; Data * data = static_cast (baton); data->notify(path, action, kind, mime_type, content_state, prop_state, revision); } /** * this is the callback function for the subversion 1.2 * api functions to signal the progress of an action * * @todo right now we forward only to @a onNotify, * but maybe we should a notify2 to the listener * @since subversion 1.2 */ static void onNotify2(void*baton,const svn_wc_notify_t *action,apr_pool_t *) { if (!baton) return; // for now forward the call to @a onNotify onNotify(baton, action->path, action->action, action->kind, action->mime_type, action->content_state, action->prop_state, action->revision); } /** * this is the callback function for the subversion * api functions to signal the progress of an action */ static svn_error_t * onCancel(void * baton) { if (baton == nullptr) return SVN_NO_ERROR; Data * data = static_cast (baton); if (data->cancel()) return svn_error_create(SVN_ERR_CANCELLED, nullptr, "cancelled by user"); else return SVN_NO_ERROR; } /** * @see svn_auth_simple_prompt_func_t */ static svn_error_t * onSimplePrompt(svn_auth_cred_simple_t **cred, void *baton, const char *realm, const char *username, svn_boolean_t _may_save, apr_pool_t *pool) { Data * data = nullptr; SVN_ERR(getData(baton, &data)); bool may_save = _may_save != 0; if (!data->retrieveLogin(username, realm, may_save)) return svn_error_create(SVN_ERR_CANCELLED, nullptr, ""); svn_auth_cred_simple_t* lcred = (svn_auth_cred_simple_t*) apr_palloc(pool, sizeof(svn_auth_cred_simple_t)); /* SVN_ERR (svn_utf_cstring_to_utf8 ( &lcred->password, data->getPassword (), pool)); SVN_ERR (svn_utf_cstring_to_utf8 ( &lcred->username, data->getUsername (), pool)); */ lcred->password = data->getPassword(); lcred->username = data->getUsername(); // tell svn if the credentials need to be saved lcred->may_save = may_save; *cred = lcred; return SVN_NO_ERROR; } /** * @see svn_auth_ssl_server_trust_prompt_func_t */ static svn_error_t * onSslServerTrustPrompt(svn_auth_cred_ssl_server_trust_t **cred, void *baton, const char *realm, apr_uint32_t failures, const svn_auth_ssl_server_cert_info_t *info, svn_boolean_t may_save, apr_pool_t *pool) { Data * data = nullptr; SVN_ERR(getData(baton, &data)); ContextListener::SslServerTrustData trustData(failures); if (realm != nullptr) trustData.realm = realm; trustData.hostname = info->hostname; trustData.fingerprint = info->fingerprint; trustData.validFrom = info->valid_from; trustData.validUntil = info->valid_until; trustData.issuerDName = info->issuer_dname; trustData.maySave = may_save != 0; apr_uint32_t acceptedFailures; ContextListener::SslServerTrustAnswer answer = data->listener->contextSslServerTrustPrompt( trustData, acceptedFailures); if (answer == ContextListener::DONT_ACCEPT) *cred = nullptr; else { svn_auth_cred_ssl_server_trust_t *cred_ = (svn_auth_cred_ssl_server_trust_t*) apr_palloc(pool, sizeof(svn_auth_cred_ssl_server_trust_t)); if (answer == ContextListener::ACCEPT_PERMANENTLY) { cred_->may_save = 1; cred_->accepted_failures = acceptedFailures; } *cred = cred_; } return SVN_NO_ERROR; } /** * @see svn_auth_ssl_client_cert_prompt_func_t */ static svn_error_t * onSslClientCertPrompt(svn_auth_cred_ssl_client_cert_t **cred, void *baton, apr_pool_t *pool) { Data * data = nullptr; SVN_ERR(getData(baton, &data)); std::string certFile; if (!data->listener->contextSslClientCertPrompt(certFile)) return svn_error_create(SVN_ERR_CANCELLED, nullptr, ""); svn_auth_cred_ssl_client_cert_t *cred_ = (svn_auth_cred_ssl_client_cert_t*) apr_palloc(pool, sizeof(svn_auth_cred_ssl_client_cert_t)); /* SVN_ERR (svn_utf_cstring_to_utf8 ( &cred_->cert_file, certFile.c_str (), pool)); */ cred_->cert_file = certFile.c_str(); *cred = cred_; return SVN_NO_ERROR; } /** * @see svn_auth_ssl_client_cert_pw_prompt_func_t */ static svn_error_t * onSslClientCertPwPrompt( svn_auth_cred_ssl_client_cert_pw_t **cred, void *baton, const char *realm, svn_boolean_t maySave, apr_pool_t *pool) { Data * data = nullptr; SVN_ERR(getData(baton, &data)); std::string password; bool may_save = maySave != 0; if (!data->listener->contextSslClientCertPwPrompt(password, realm, may_save)) return svn_error_create(SVN_ERR_CANCELLED, nullptr, ""); svn_auth_cred_ssl_client_cert_pw_t *cred_ = (svn_auth_cred_ssl_client_cert_pw_t *) apr_palloc(pool, sizeof(svn_auth_cred_ssl_client_cert_pw_t)); /* SVN_ERR (svn_utf_cstring_to_utf8 ( &cred_->password, password.c_str (), pool)); */ cred_->password = password.c_str(); cred_->may_save = may_save; *cred = cred_; return SVN_NO_ERROR; } const char * getUsername() const { return username.c_str(); } const char * getPassword() const { return password.c_str(); } const char * getLogMessage() const { return logMessage.c_str(); } /** * if the @a listener is set, use it to retrieve the log * message using ContextListener::contextGetLogMessage. * This return values is given back, then. * * if the @a listener is not set the its checked whether * the log message has been set using @a setLogMessage * yet. If not, return false otherwise true * * @param msg log message * @retval false cancel */ bool retrieveLogMessage(std::string & msg) { bool ok; if (listener == nullptr) return false; ok = listener->contextGetLogMessage(logMessage); if (ok) msg = logMessage; else logIsSet = false; return ok; } /** * if the @a listener is set and no password has been * set yet, use it to retrieve login and password using * ContextListener::contextGetLogin. * * if the @a listener is not set, check if setLogin * has been called yet. * * @return continue? * @retval false cancel */ bool retrieveLogin(const char * username_, const char * realm, bool &may_save) { bool ok; if (listener == nullptr) return false; if (username_ == nullptr) username = ""; else username = username_; ok = listener->contextGetLogin(realm, username, password, may_save); return ok; } /** * if the @a listener is set call the method * @a contextNotify */ void notify(const char *path, svn_wc_notify_action_t action, svn_node_kind_t kind, const char *mime_type, svn_wc_notify_state_t content_state, svn_wc_notify_state_t prop_state, svn_revnum_t revision) { if (listener != nullptr) { listener->contextNotify(path, action, kind, mime_type, content_state, prop_state, revision); } } /** * if the @a listener is set call the method * @a contextCancel */ bool cancel() { if (listener != nullptr) { return listener->contextCancel(); } else { // don't cancel if no listener return false; } } }; Context::Context(const std::string &configDir) { m = new Data(configDir); } Context::Context(const Context & src) { m = new Data(src.m->configDir); setLogin(src.getUsername(), src.getPassword()); } Context::~Context() { delete m; } void Context::setAuthCache(bool value) { m->setAuthCache(value); } void Context::setLogin(const char * username, const char * password) { m->setLogin(username, password); } Context::operator svn_client_ctx_t * () { return m->ctx; } svn_client_ctx_t * Context::ctx() { return m->ctx; } void Context::setLogMessage(const char * msg) { m->setLogMessage(msg); } const char * Context::getUsername() const { return m->getUsername(); } const char * Context::getPassword() const { return m->getPassword(); } const char * Context::getLogMessage() const { return m->getLogMessage(); } void Context::setListener(ContextListener * listener) { m->listener = listener; } ContextListener * Context::getListener() const { return m->listener; } void Context::reset() { m->promptCounter = 0; m->logIsSet = false; } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/subversion/kdevsvncpp/property.cpp b/plugins/subversion/kdevsvncpp/property.cpp index dbd9107ad..7db7640a0 100644 --- a/plugins/subversion/kdevsvncpp/property.cpp +++ b/plugins/subversion/kdevsvncpp/property.cpp @@ -1,171 +1,171 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ // subversion api #include "svn_client.h" //#include "svn_utf.h" // svncpp #include "kdevsvncpp/exception.hpp" #include "kdevsvncpp/path.hpp" #include "kdevsvncpp/pool.hpp" #include "kdevsvncpp/property.hpp" #include "kdevsvncpp/revision.hpp" #include "m_check.hpp" namespace svn { PropertyEntry::PropertyEntry(const char * name, const char * value) { this->name = name; this->value = value; } Property::Property(Context * context, const Path & path) : m_context(context), m_path(path) { list(); } Property::~Property() { } void Property::list() { Pool pool; Revision revision; m_entries.clear(); apr_array_header_t * props; svn_error_t * error = svn_client_proplist(&props, m_path.c_str(), revision, false, /* recurse */ *m_context, pool); if (error != nullptr) { throw ClientException(error); } for (int j = 0; j < props->nelts; ++j) { svn_client_proplist_item_t *item = ((svn_client_proplist_item_t **)props->elts)[j]; apr_hash_index_t *hi; for (hi = apr_hash_first(pool, item->prop_hash); hi; hi = apr_hash_next(hi)) { const void *key; void *val; apr_hash_this(hi, &key, nullptr, &val); m_entries.push_back(PropertyEntry( (const char *)key, getValue((const char *)key).c_str())); } } } std::string Property::getValue(const char * name) { Pool pool; Revision revision; apr_hash_t *props; svn_client_propget(&props, name, m_path.c_str(), revision, false, // recurse *m_context, pool); apr_hash_index_t *hi; hi = apr_hash_first(pool, props); if (!hi) { return ""; } const void *key; void *val; const svn_string_t *propval; apr_hash_this(hi, &key, nullptr, &val); propval = (const svn_string_t *)val; return propval->data; } void Property::set(const char * name, const char * value) { Pool pool; const svn_string_t * propval = svn_string_create((const char *) value, pool); bool recurse = false; bool skip_checks = false; svn_error_t * error = svn_client_propset2(name, propval, m_path.c_str(), recurse, skip_checks, *m_context, pool); if (error != nullptr) throw ClientException(error); } void Property::remove(const char * name) { Pool pool; svn_error_t * error = svn_client_propset(name, nullptr, // value = NULL m_path.c_str(), - false, //dont recurse + false, //do not recurse pool); if (error != nullptr) throw ClientException(error); } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/subversion/kdevsvncpp/status_selection.cpp b/plugins/subversion/kdevsvncpp/status_selection.cpp index e17539274..aa0f5d51a 100644 --- a/plugins/subversion/kdevsvncpp/status_selection.cpp +++ b/plugins/subversion/kdevsvncpp/status_selection.cpp @@ -1,278 +1,278 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ // subversion api #include "svn_types.h" // apr api #include "apr_file_info.h" // svncpp #include "kdevsvncpp/exception.hpp" #include "kdevsvncpp/path.hpp" #include "kdevsvncpp/pool.hpp" #include "kdevsvncpp/status.hpp" #include "kdevsvncpp/status_selection.hpp" #include "kdevsvncpp/targets.hpp" #include "kdevsvncpp/url.hpp" namespace svn { struct StatusSel::Data { Targets targets; StatusVector status; bool hasDirs; bool hasFiles; bool hasVersioned; bool hasUnversioned; bool hasUrl; bool hasLocal; Path emptyTarget; /** default constructor */ Data() {} /** copy constructor */ Data(const Data & src) { if (this != &src) assign(src); } /** assign new values */ void assign(const Data & src) { // clear existing... clear(); // ... and set from source StatusVector::const_iterator it; for (it = src.status.begin(); it != src.status.end(); ++it) { push_back(*it); } } void clear() { targets.clear(); status.clear(); hasDirs = false; hasFiles = false; hasVersioned = false; hasUnversioned = false; hasLocal = false; hasUrl = false; } void push_back(const Status & status_) { // skip pseudo entries if (!status_.isSet()) return; if (!status_.isVersioned()) { // for an unversioned entry we do not know // whether it's a file or a directory so // we have to check using APR apr_finfo_t finfo; Pool pool; apr_status_t apr_status = apr_stat( &finfo, status_.path(), APR_FINFO_TYPE, pool); // if we get an error the file might // have been deleted in the meantime - // anyhow: we dont want to display it + // anyhow: we do not want to display it if (apr_status != APR_SUCCESS) return; hasUnversioned = true; if (APR_DIR == finfo.filetype) hasDirs = true; else hasFiles = true; } else { hasVersioned = true; if (Url::isValid(status_.path())) hasUrl = true; else hasLocal = true; if (svn_node_dir == status_.entry().kind()) hasDirs = true; else hasFiles = true; } // add stuff only now (because of possible apr_error // which causes the function to exit) targets.push_back(status_.path()); status.push_back(status_); } }; StatusSel::StatusSel() : m(new Data) { } StatusSel::StatusSel(const StatusSel & src) : m(new Data) { // different instance? if (this != &src) m->assign(*src.m); } StatusSel & StatusSel::operator = (const StatusSel & src) { if (this != &src) { delete m; m = new Data(*src.m); } return *this; } StatusSel::~StatusSel() { delete m; } const apr_array_header_t * StatusSel::array(const Pool & pool) const { return m->targets.array(pool); } const StatusVector & StatusSel::statusVector() const { return m->status; } const Targets & StatusSel::targets() const { return m->targets; } size_t StatusSel::size() const { return m->targets.size(); } void StatusSel::push_back(const Status & status) { m->push_back(status); } void StatusSel::clear() { m->clear(); } void StatusSel::reserve(size_t size) { m->targets.reserve(size); m->status.reserve(size); } StatusSel::operator const PathVector & () const { return m->targets; } const Path & StatusSel::target() const { if (size() > 0) return m->targets.targets()[0]; else return m->emptyTarget; } bool StatusSel::hasDirs() const { return m->hasDirs; } bool StatusSel::hasFiles() const { return m->hasFiles; } bool StatusSel::hasVersioned() const { return m->hasVersioned; } bool StatusSel::hasUnversioned() const { return m->hasUnversioned; } bool StatusSel::hasLocal() const { return m->hasLocal; } bool StatusSel::hasUrl() const { return m->hasUrl; } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/subversion/kdevsvncpp/url.cpp b/plugins/subversion/kdevsvncpp/url.cpp index ca8b737a5..fbed0a0e0 100644 --- a/plugins/subversion/kdevsvncpp/url.cpp +++ b/plugins/subversion/kdevsvncpp/url.cpp @@ -1,124 +1,124 @@ /* * ==================================================================== * Copyright (c) 2002-2009 The RapidSvn Group. All rights reserved. * * 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 3 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 (in the file GPL.txt. * If not, see . * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://rapidsvn.tigris.org/. * ==================================================================== */ #if defined( _MSC_VER) && _MSC_VER <= 1200 #pragma warning( disable: 4786 )// debug symbol truncated #endif // subversion api #include "svn_path.h" // svncpp #include "kdevsvncpp/pool.hpp" #include "kdevsvncpp/url.hpp" static void findAndReplace(std::string & source, const std::string & find, const std::string & replace) { // start seaching from the beginning size_t pos = 0; size_t findLength = find.length(); size_t replaceLength = replace.length(); do { // search for the next occurrenc pos = source.find(find, pos); // found? if (pos != std::string::npos) { // yes, place source.replace(pos, findLength, replace); - // Make sure we dont search from the beginning + // Make sure we do not search from the beginning // othwise replacing % with %25 would result // in an endless loop pos = pos + replaceLength; } } while (pos != std::string::npos); } namespace svn { Url::Url() {} Url::~Url() {} bool Url::isValid(const char * urlToValidate) { return svn_path_is_url(urlToValidate) != 0; } std::string Url::escape(const char * url) { Pool pool; // First make sure % gets escaped std::string partlyEscaped(url); findAndReplace(partlyEscaped, "%", "%25"); // Let svn do the first part of the work partlyEscaped=svn_path_uri_autoescape(partlyEscaped.c_str(), pool); // Then worry about the rest findAndReplace(partlyEscaped, "#", "%23"); findAndReplace(partlyEscaped, ";", "%3B"); findAndReplace(partlyEscaped, "?", "%3F"); findAndReplace(partlyEscaped, "[", "%5B"); findAndReplace(partlyEscaped, "]", "%5D"); return partlyEscaped; } std::string Url::unescape(const char * url) { Pool pool; return svn_path_uri_decode(url, pool); } /** * the implementation of the function that pull the supported * url schemas out of the ra layer it rather dirty now since * we are lacking a higher level of abstraction */ std::vector Url::supportedSchemas() { std::vector schemas; return schemas; } } /* ----------------------------------------------------------------- * local variables: * eval: (load-file "../../rapidsvn-dev.el") * end: */ diff --git a/plugins/switchtobuddy/switchtobuddyplugin.h b/plugins/switchtobuddy/switchtobuddyplugin.h index e1428ec8d..ae631c92c 100644 --- a/plugins/switchtobuddy/switchtobuddyplugin.h +++ b/plugins/switchtobuddy/switchtobuddyplugin.h @@ -1,86 +1,86 @@ /* * This file is part of KDevelop * Copyright 2012 André Stein * Copyright 2014 Kevin Funk * * 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. */ #ifndef KDEVPLATFORM_PLUGIN_SWITCHTOBUDDY_H #define KDEVPLATFORM_PLUGIN_SWITCHTOBUDDY_H #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(PLUGIN_SWITCHTOBUDDY) /** * @short Implements a context menu extension in an editor context which provides * an action that allows switching to associated buddy documents. * * Using the @c IBuddyDocumentFinder interface, the current document's * language plugin provides potential buddy candidates. Depending on their * existence on the file system the @c SwitchToBuddyPlugin * enables a 'Switch To XXX' action which opens that buddy document * using the @c IDocumentController. * - * If a language plugin either doens't provide the @c IBuddyDocumentFinder + * If a language plugin either doesn't provide the @c IBuddyDocumentFinder * interface or no buddy exists on the file system, no context menu * extension is performed. * * @see IBuddyDocumentFinder * @see IDocumentController */ class SwitchToBuddyPlugin : public KDevelop::IPlugin { Q_OBJECT public: explicit SwitchToBuddyPlugin( QObject *parent, const QVariantList & = QVariantList()); ~SwitchToBuddyPlugin() override; KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context) override; void createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) override; private slots: /** * Context menu slot which switches to the QUrl provided * in the data part of the sending QAction. */ void switchToBuddy(const QString& url); /** * Switch between header and source files */ void switchHeaderSource(); /** * @brief Switch between definitions and declarations * * E.g. if the cursor in the currently active view points to an implementation file * this shortcut will open the header document (or any buddy file). * * Furthermore, if the cursor points to a definition, and the buddy document contains its declaration, * the cursor will be also set to the declaration's position when the buddy document is opened */ void switchDefinitionDeclaration(); private: class QSignalMapper* m_signalMapper; }; #endif // KDEVPLATFORM_PLUGIN_SWITCHTOBUDDY_H diff --git a/shell/projectsourcepage.cpp b/shell/projectsourcepage.cpp index 35f5f6520..fd4af2df8 100644 --- a/shell/projectsourcepage.cpp +++ b/shell/projectsourcepage.cpp @@ -1,315 +1,315 @@ /*************************************************************************** * Copyright (C) 2010 by Aleix Pol Gonzalez * * * * 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 "projectsourcepage.h" #include "ui_projectsourcepage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static const int FROM_FILESYSTEM_SOURCE_INDEX = 0; ProjectSourcePage::ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, QWidget* parent) : QWidget(parent) { m_ui = new Ui::ProjectSourcePage; m_ui->setupUi(this); m_ui->status->setCloseButtonVisible(false); m_ui->status->setMessageType(KMessageWidget::Error); m_ui->workingDir->setUrl(initial); m_ui->workingDir->setMode(KFile::Directory); m_ui->remoteWidget->setLayout(new QVBoxLayout(m_ui->remoteWidget)); m_ui->sources->addItem(QIcon::fromTheme(QStringLiteral("folder")), i18n("From File System")); m_plugins.append(nullptr); int preselectIndex = -1; IPluginController* pluginManager = ICore::self()->pluginController(); QList plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IBasicVersionControl") ); foreach( IPlugin* p, plugins ) { if (p == preSelectPlugin) { preselectIndex = m_plugins.count(); } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IProjectProvider") ); foreach( IPlugin* p, plugins ) { if (p == preSelectPlugin) { preselectIndex = m_plugins.count(); } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } if (preselectIndex == -1) { // "From File System" is quite unlikely to be what the user wants, so default to first real plugin... const int defaultIndex = (m_plugins.count() > 1) ? 1 : 0; KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); preselectIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); } preselectIndex = qBound(0, preselectIndex, m_ui->sources->count() - 1); m_ui->sources->setCurrentIndex(preselectIndex); setSourceWidget(preselectIndex, repoUrl); // connect as last step, otherwise KMessageWidget could get both animatedHide() and animatedShow() // during setup and due to a bug will ignore any but the first call // Only fixed for KF5 5.32 connect(m_ui->workingDir, &KUrlRequester::textChanged, this, &ProjectSourcePage::reevaluateCorrection); connect(m_ui->sources, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSourcePage::setSourceIndex); connect(m_ui->get, &QPushButton::clicked, this, &ProjectSourcePage::checkoutVcsProject); } ProjectSourcePage::~ProjectSourcePage() { KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); configGroup.writeEntry("LastProviderIndex", m_ui->sources->currentIndex()); delete m_ui; } void ProjectSourcePage::setSourceIndex(int index) { setSourceWidget(index, QUrl()); } void ProjectSourcePage::setSourceWidget(int index, const QUrl& repoUrl) { m_locationWidget = nullptr; m_providerWidget = nullptr; QLayout* remoteWidgetLayout = m_ui->remoteWidget->layout(); QLayoutItem *child; while ((child = remoteWidgetLayout->takeAt(0)) != nullptr) { delete child->widget(); delete child; } IBasicVersionControl* vcIface = vcsPerIndex(index); IProjectProvider* providerIface; bool found=false; if(vcIface) { found=true; m_locationWidget=vcIface->vcsLocation(m_ui->sourceBox); connect(m_locationWidget, &VcsLocationWidget::changed, this, &ProjectSourcePage::locationChanged); // set after connect, to trigger handler if (!repoUrl.isEmpty()) { m_locationWidget->setLocation(repoUrl); } remoteWidgetLayout->addWidget(m_locationWidget); } else { providerIface = providerPerIndex(index); if(providerIface) { found=true; m_providerWidget=providerIface->providerWidget(m_ui->sourceBox); connect(m_providerWidget, &IProjectProviderWidget::changed, this, &ProjectSourcePage::projectChanged); remoteWidgetLayout->addWidget(m_providerWidget); } } reevaluateCorrection(); m_ui->sourceBox->setVisible(found); } IBasicVersionControl* ProjectSourcePage::vcsPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } IProjectProvider* ProjectSourcePage::providerPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } VcsJob* ProjectSourcePage::jobPerCurrent() { QUrl url=m_ui->workingDir->url(); IPlugin* p=m_plugins[m_ui->sources->currentIndex()]; VcsJob* job=nullptr; if(IBasicVersionControl* iface=p->extension()) { Q_ASSERT(iface && m_locationWidget); job=iface->createWorkingCopy(m_locationWidget->location(), url); } else if(m_providerWidget) { job=m_providerWidget->createWorkingCopy(url); } return job; } void ProjectSourcePage::checkoutVcsProject() { QUrl url=m_ui->workingDir->url(); QDir d(url.toLocalFile()); if(!url.isLocalFile() && !d.exists()) { bool corr = d.mkpath(d.path()); if(!corr) { KMessageBox::error(nullptr, i18n("Could not create the directory: %1", d.path())); return; } } VcsJob* job=jobPerCurrent(); if (!job) { return; } m_ui->sources->setEnabled(false); m_ui->sourceBox->setEnabled(false); m_ui->workingDir->setEnabled(false); m_ui->get->setEnabled(false); m_ui->creationProgress->setValue(m_ui->creationProgress->minimum()); connect(job, &VcsJob::result, this, &ProjectSourcePage::projectReceived); // Can't use new signal-slot syntax, KJob::percent is private :/ connect(job, SIGNAL(percent(KJob*,ulong)), SLOT(progressChanged(KJob*,ulong))); connect(job, &VcsJob::infoMessage, this, &ProjectSourcePage::infoMessage); ICore::self()->runController()->registerJob(job); } void ProjectSourcePage::progressChanged(KJob*, unsigned long value) { m_ui->creationProgress->setValue(value); } void ProjectSourcePage::infoMessage(KJob* , const QString& text, const QString& /*rich*/) { m_ui->creationProgress->setFormat(i18nc("Format of the progress bar text. progress and info", "%1 : %p%", text)); } void ProjectSourcePage::projectReceived(KJob* job) { if (job->error()) { m_ui->creationProgress->setValue(0); } else { m_ui->creationProgress->setValue(m_ui->creationProgress->maximum()); } reevaluateCorrection(); m_ui->creationProgress->setFormat(QStringLiteral("%p%")); } void ProjectSourcePage::reevaluateCorrection() { //TODO: Probably we should just ignore remote URL's, I don't think we're ever going //to support checking out to remote directories const QUrl cwd = m_ui->workingDir->url(); const QDir dir = cwd.toLocalFile(); // case where we import a project from local file system if (m_ui->sources->currentIndex() == FROM_FILESYSTEM_SOURCE_INDEX) { emit isCorrect(dir.exists()); return; } // all other cases where remote locations need to be specified bool correct=!cwd.isRelative() && (!cwd.isLocalFile() || QDir(cwd.adjusted(QUrl::RemoveFilename).toLocalFile()).exists()); emit isCorrect(correct && m_ui->creationProgress->value() == m_ui->creationProgress->maximum()); const bool validWidget = ((m_locationWidget && m_locationWidget->isCorrect()) || (m_providerWidget && m_providerWidget->isCorrect())); const bool isFolderEmpty = (correct && cwd.isLocalFile() && dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()); const bool validToCheckout = correct && validWidget && (!dir.exists() || isFolderEmpty); m_ui->get->setEnabled(validToCheckout); m_ui->creationProgress->setEnabled(validToCheckout); if(!correct) setStatus(i18n("You need to specify a valid or nonexistent directory to check out a project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !validWidget) setStatus(i18n("You need to specify the source for your remote project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !isFolderEmpty) setStatus(i18n("You need to specify an empty folder as your project destination")); else clearStatus(); } void ProjectSourcePage::locationChanged() { Q_ASSERT(m_locationWidget); if(m_locationWidget->isCorrect()) { QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf('/')+1); QUrl current = QUrl::fromUserInput(currentUrl + m_locationWidget->projectName()); m_ui->workingDir->setUrl(current); } else reevaluateCorrection(); } void ProjectSourcePage::projectChanged(const QString& name) { Q_ASSERT(m_providerWidget); QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf('/')+1); QUrl current = QUrl::fromUserInput(currentUrl + name); m_ui->workingDir->setUrl(current); } void ProjectSourcePage::setStatus(const QString& message) { m_ui->status->setText(message); m_ui->status->animatedShow(); } void ProjectSourcePage::clearStatus() { #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5,32,0) // workaround for KMessageWidget bug: - // animatedHide() will not explicitely hide the widget if it is not yet shown. - // So if it has never been explicitely hidden otherwise, + // animatedHide() will not explicitly hide the widget if it is not yet shown. + // So if it has never been explicitly hidden otherwise, // if show() is called on the parent later the KMessageWidget will be shown as well. // As this method is sometimes called when the page is created and thus not yet shown, // we have to ensure the hidden state ourselves here. if (!m_ui->status->isVisible()) { m_ui->status->hide(); return; } #endif m_ui->status->animatedHide(); } QUrl ProjectSourcePage::workingDir() const { return m_ui->workingDir->url(); } diff --git a/vcs/dvcs/ui/branchmanager.cpp b/vcs/dvcs/ui/branchmanager.cpp index 5501b30ed..05cef416b 100644 --- a/vcs/dvcs/ui/branchmanager.cpp +++ b/vcs/dvcs/ui/branchmanager.cpp @@ -1,238 +1,238 @@ /*************************************************************************** * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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, see . * ***************************************************************************/ #include "branchmanager.h" #include #include #include #include #include "../dvcsjob.h" #include "../dvcsplugin.h" #include #include "ui_branchmanager.h" #include "../../debug.h" #include "widgets/vcsdiffpatchsources.h" #include #include #include #include #include #include using namespace KDevelop; BranchManager::BranchManager(const QString& repository, KDevelop::DistributedVersionControlPlugin* executor, QWidget *parent) : QDialog(parent) , m_repository(repository) , m_dvcPlugin(executor) { setWindowTitle(i18n("Branch Manager")); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); m_ui = new Ui::BranchDialogBase; QWidget* w = new QWidget(this); m_ui->setupUi(w); mainLayout->addWidget(w); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &BranchManager::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &BranchManager::reject); mainLayout->addWidget(buttonBox); m_model = new BranchesListModel(this); m_model->initialize(m_dvcPlugin, QUrl::fromLocalFile(repository)); m_ui->branchView->setModel(m_model); QString branchName = m_model->currentBranch(); // apply initial selection QList< QStandardItem* > items = m_model->findItems(branchName); if (!items.isEmpty()) { m_ui->branchView->setCurrentIndex(items.first()->index()); } m_ui->newButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect(m_ui->newButton, &QPushButton::clicked, this, &BranchManager::createBranch); m_ui->deleteButton->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); connect(m_ui->deleteButton, &QPushButton::clicked, this, &BranchManager::deleteBranch); m_ui->renameButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect(m_ui->renameButton, &QPushButton::clicked, this, &BranchManager::renameBranch); m_ui->checkoutButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); connect(m_ui->checkoutButton, &QPushButton::clicked, this, &BranchManager::checkoutBranch); // checkout branch on double-click connect(m_ui->branchView, &QListView::doubleClicked, this, &BranchManager::checkoutBranch); m_ui->mergeButton->setIcon(QIcon::fromTheme(QStringLiteral("merge"))); connect(m_ui->mergeButton, &QPushButton::clicked, this, &BranchManager::mergeBranch); m_ui->diffButton->setIcon(QIcon::fromTheme(QStringLiteral("text-x-patch"))); connect(m_ui->diffButton, &QPushButton::clicked, this, &BranchManager::diffFromBranch); } BranchManager::~BranchManager() { delete m_ui; } void BranchManager::createBranch() { const QModelIndex currentBranchIdx = m_ui->branchView->currentIndex(); if (!currentBranchIdx.isValid()) { KMessageBox::messageBox(this, KMessageBox::Error, i18n("You must select a base branch from the list before creating a new branch.")); return; } QString baseBranch = currentBranchIdx.data().toString(); bool branchNameEntered = false; QString newBranch = QInputDialog::getText(this, i18n("New branch"), i18n("Name of the new branch:"), QLineEdit::Normal, QString(), &branchNameEntered); if (!branchNameEntered) return; if (!m_model->findItems(newBranch).isEmpty()) { KMessageBox::messageBox(this, KMessageBox::Sorry, i18n("Branch \"%1\" already exists.\n" "Please, choose another name.", newBranch)); } else m_model->createBranch(baseBranch, newBranch); } void BranchManager::deleteBranch() { QString baseBranch = m_ui->branchView->selectionModel()->selection().indexes().first().data().toString(); if (baseBranch == m_model->currentBranch()) { KMessageBox::messageBox(this, KMessageBox::Sorry, i18n("Currently at the branch \"%1\".\n" "To remove it, please change to another branch.", baseBranch)); return; } int ret = KMessageBox::messageBox(this, KMessageBox::WarningYesNo, i18n("Are you sure you want to irreversibly remove the branch '%1'?", baseBranch)); if (ret == KMessageBox::Yes) m_model->removeBranch(baseBranch); } void BranchManager::renameBranch() { QModelIndex currentIndex = m_ui->branchView->currentIndex(); if (!currentIndex.isValid()) return; m_ui->branchView->edit(currentIndex); } void BranchManager::checkoutBranch() { QString branch = m_ui->branchView->currentIndex().data().toString(); if (branch == m_model->currentBranch()) { KMessageBox::messageBox(this, KMessageBox::Sorry, i18n("Already on branch \"%1\"\n", branch)); return; } qCDebug(VCS) << "Switching to" << branch << "in" << m_repository; KDevelop::VcsJob *branchJob = m_dvcPlugin->switchBranch(QUrl::fromLocalFile(m_repository), branch); // connect(branchJob, SIGNAL(finished(KJob*)), m_model, SIGNAL(resetCurrent())); ICore::self()->runController()->registerJob(branchJob); close(); } void BranchManager::mergeBranch() { const QModelIndex branchToMergeIdx = m_ui->branchView->currentIndex(); if (branchToMergeIdx.isValid()) { QString branchToMerge = branchToMergeIdx.data().toString(); if (m_model->findItems(branchToMerge).isEmpty()) { KMessageBox::messageBox(this, KMessageBox::Sorry, i18n("Branch \"%1\" doesn't exists.\n" "Please, choose another name.", branchToMerge)); } else { KDevelop::VcsJob* branchJob = m_dvcPlugin->mergeBranch(QUrl::fromLocalFile(m_repository), branchToMerge); ICore::self()->runController()->registerJob(branchJob); close(); } } else { KMessageBox::messageBox(this, KMessageBox::Error, i18n("You must select a branch to merge into current one from the list.")); } } void BranchManager::diffFromBranch() { const auto dest = m_model->currentBranch(); const auto src = m_ui->branchView->currentIndex().data().toString(); if (src == dest) { KMessageBox::messageBox(this, KMessageBox::Information, i18n("Already on branch \"%1\"\n", src)); return; } VcsRevision srcRev; srcRev.setRevisionValue(src, KDevelop::VcsRevision::GlobalNumber); // We have two options here: // * create a regular VcsRevision to represent the last commit on the current branch or - // * create a special branch to reflect the staging area. I choosed this one. - // If the staing area is clean it automatically defaults to the first option. + // * create a special branch to reflect the staging area. I chose this one. + // If the staging area is clean it automatically defaults to the first option. const auto destRev = VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Working); const auto job = m_dvcPlugin->diff(QUrl::fromLocalFile(m_repository), srcRev, destRev); connect(job, &VcsJob::finished, this, &BranchManager::diffJobFinished); m_dvcPlugin->core()->runController()->registerJob(job); } void BranchManager::diffJobFinished(KJob* job) { auto vcsjob = qobject_cast(job); Q_ASSERT(vcsjob); if (vcsjob->status() != KDevelop::VcsJob::JobSucceeded) { KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), vcsjob->errorString(), i18n("Unable to retrieve diff.")); return; } auto diff = vcsjob->fetchResults().value(); if(diff.isEmpty()){ KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18n("There are no committed differences."), i18n("VCS support")); return; } auto patch = new VCSDiffPatchSource(diff); showVcsDiff(patch); close(); } diff --git a/vcs/vcspluginhelper.cpp b/vcs/vcspluginhelper.cpp index ced992917..ebec03743 100644 --- a/vcs/vcspluginhelper.cpp +++ b/vcs/vcspluginhelper.cpp @@ -1,485 +1,485 @@ /*************************************************************************** * Copyright 2008 Andreas Pakulat * * Copyright 2010 Aleix Pol Gonzalez * * * * 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 "vcspluginhelper.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 #include #include "interfaces/idistributedversioncontrol.h" #include "vcsstatusinfo.h" #include "vcsevent.h" #include "widgets/vcsdiffpatchsources.h" namespace KDevelop { struct VcsPluginHelper::VcsPluginHelperPrivate { IPlugin * plugin; IBasicVersionControl * vcs; QList ctxUrls; QAction* commitAction; QAction* addAction; QAction* updateAction; QAction* historyAction; QAction* annotationAction; QAction* diffToBaseAction; QAction* revertAction; QAction* diffForRevAction; QAction* diffForRevGlobalAction; QAction* pushAction; QAction* pullAction; void createActions(VcsPluginHelper* parent) { commitAction = new QAction(QIcon::fromTheme(QStringLiteral("svn-commit")), i18n("Commit..."), parent); updateAction = new QAction(QIcon::fromTheme(QStringLiteral("svn-update")), i18n("Update"), parent); addAction = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), parent); diffToBaseAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Differences..."), parent); revertAction = new QAction(QIcon::fromTheme(QStringLiteral("archive-remove")), i18n("Revert"), parent); historyAction = new QAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("History..."), parent); annotationAction = new QAction(QIcon::fromTheme(QStringLiteral("user-properties")), i18n("Annotation..."), parent); diffForRevAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Diff..."), parent); diffForRevGlobalAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")), i18n("Show Diff (all files)..."), parent); pushAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")), i18n("Push"), parent); pullAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")), i18n("Pull"), parent); connect(commitAction, &QAction::triggered, parent, &VcsPluginHelper::commit); connect(addAction, &QAction::triggered, parent, &VcsPluginHelper::add); connect(updateAction, &QAction::triggered, parent, &VcsPluginHelper::update); connect(diffToBaseAction, &QAction::triggered, parent, &VcsPluginHelper::diffToBase); connect(revertAction, &QAction::triggered, parent, &VcsPluginHelper::revert); connect(historyAction, &QAction::triggered, parent, [=] { parent->history(); }); connect(annotationAction, &QAction::triggered, parent, &VcsPluginHelper::annotation); connect(diffForRevAction, &QAction::triggered, parent, static_cast(&VcsPluginHelper::diffForRev)); connect(diffForRevGlobalAction, &QAction::triggered, parent, &VcsPluginHelper::diffForRevGlobal); connect(pullAction, &QAction::triggered, parent, &VcsPluginHelper::pull); connect(pushAction, &QAction::triggered, parent, &VcsPluginHelper::push); } bool allLocalFiles(const QList& urls) { bool ret=true; foreach(const QUrl &url, urls) { QFileInfo info(url.toLocalFile()); ret &= info.isFile(); } return ret; } QMenu* createMenu() { bool allVersioned=true; foreach(const QUrl &url, ctxUrls) { allVersioned=allVersioned && vcs->isVersionControlled(url); if(!allVersioned) break; } QMenu* menu = new QMenu(vcs->name()); menu->setIcon(QIcon::fromTheme(ICore::self()->pluginController()->pluginInfo(plugin).iconName())); menu->addAction(commitAction); if(plugin->extension()) { menu->addAction(pushAction); menu->addAction(pullAction); } else { menu->addAction(updateAction); } menu->addSeparator(); menu->addAction(addAction); menu->addAction(revertAction); menu->addSeparator(); menu->addAction(historyAction); menu->addAction(annotationAction); menu->addAction(diffToBaseAction); const bool singleVersionedFile = ctxUrls.count() == 1 && allVersioned; historyAction->setEnabled(singleVersionedFile); annotationAction->setEnabled(singleVersionedFile && allLocalFiles(ctxUrls)); diffToBaseAction->setEnabled(singleVersionedFile); commitAction->setEnabled(singleVersionedFile); return menu; } }; VcsPluginHelper::VcsPluginHelper(KDevelop::IPlugin* parent, KDevelop::IBasicVersionControl* vcs) : QObject(parent) , d(new VcsPluginHelperPrivate()) { Q_ASSERT(vcs); Q_ASSERT(parent); d->plugin = parent; d->vcs = vcs; d->createActions(this); } VcsPluginHelper::~VcsPluginHelper() {} void VcsPluginHelper::addContextDocument(const QUrl &url) { d->ctxUrls.append(url); } void VcsPluginHelper::disposeEventually(KTextEditor::View *, bool dont) { if ( ! dont ) { deleteLater(); } } void VcsPluginHelper::disposeEventually(KTextEditor::Document *) { deleteLater(); } void VcsPluginHelper::setupFromContext(Context* context) { d->ctxUrls = context->urls(); } QList VcsPluginHelper::contextUrlList() const { return d->ctxUrls; } QMenu* VcsPluginHelper::commonActions() { /* TODO: the following logic to determine which actions need to be enabled * or disabled does not work properly. What needs to be implemented is that * project items that are vc-controlled enable all except add, project * items that are not vc-controlled enable add action. For urls that cannot * be made into a project item, or if the project has no associated VC * plugin we need to check whether a VC controls the parent dir, if we have * one we assume the urls can be added but are not currently controlled. If * the url is already version controlled then just enable all except add */ return d->createMenu(); } #define EXECUTE_VCS_METHOD( method ) \ d->plugin->core()->runController()->registerJob( d->vcs-> method ( d->ctxUrls ) ) #define SINGLEURL_SETUP_VARS \ KDevelop::IBasicVersionControl* iface = d->vcs;\ const QUrl &url = d->ctxUrls.front(); void VcsPluginHelper::revert() { VcsJob* job=d->vcs->revert(d->ctxUrls); connect(job, &VcsJob::finished, this, &VcsPluginHelper::revertDone); foreach(const QUrl &url, d->ctxUrls) { IDocument* doc=ICore::self()->documentController()->documentForUrl(url); if(doc && doc->textDocument()) { KTextEditor::ModificationInterface* modif = dynamic_cast(doc->textDocument()); if (modif) { modif->setModifiedOnDiskWarning(false); } doc->textDocument()->setModified(false); } } job->setProperty("urls", QVariant::fromValue(d->ctxUrls)); d->plugin->core()->runController()->registerJob(job); } void VcsPluginHelper::revertDone(KJob* job) { QTimer* modificationTimer = new QTimer; modificationTimer->setInterval(100); connect(modificationTimer, &QTimer::timeout, this, &VcsPluginHelper::delayedModificationWarningOn); connect(modificationTimer, &QTimer::timeout, modificationTimer, &QTimer::deleteLater); modificationTimer->setProperty("urls", job->property("urls")); modificationTimer->start(); } void VcsPluginHelper::delayedModificationWarningOn() { QObject* timer = sender(); QList urls = timer->property("urls").value>(); foreach(const QUrl &url, urls) { IDocument* doc=ICore::self()->documentController()->documentForUrl(url); if(doc) { doc->reload(); KTextEditor::ModificationInterface* modif=dynamic_cast(doc->textDocument()); modif->setModifiedOnDiskWarning(true); } } } void VcsPluginHelper::diffJobFinished(KJob* job) { KDevelop::VcsJob* vcsjob = qobject_cast(job); Q_ASSERT(vcsjob); if (vcsjob->status() == KDevelop::VcsJob::JobSucceeded) { KDevelop::VcsDiff d = vcsjob->fetchResults().value(); if(d.isEmpty()) KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18n("There are no differences."), i18n("VCS support")); else { VCSDiffPatchSource* patch=new VCSDiffPatchSource(d); showVcsDiff(patch); } } else { KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), vcsjob->errorString(), i18n("Unable to get difference.")); } } void VcsPluginHelper::diffToBase() { SINGLEURL_SETUP_VARS ICore::self()->documentController()->saveAllDocuments(); VCSDiffPatchSource* patch =new VCSDiffPatchSource(new VCSStandardDiffUpdater(iface, url)); showVcsDiff(patch); } void VcsPluginHelper::diffForRev() { if (d->ctxUrls.isEmpty()) { return; } diffForRev(d->ctxUrls.first()); } void VcsPluginHelper::diffForRevGlobal() { if (d->ctxUrls.isEmpty()) { return; } QUrl url = d->ctxUrls.first(); IProject* project = ICore::self()->projectController()->findProjectForUrl( url ); if( project ) { url = project->path().toUrl(); } diffForRev(url); } void VcsPluginHelper::diffForRev(const QUrl& url) { QAction* action = qobject_cast( sender() ); Q_ASSERT(action); Q_ASSERT(action->data().canConvert()); VcsRevision rev = action->data().value(); ICore::self()->documentController()->saveAllDocuments(); VcsRevision prev = KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Previous); KDevelop::VcsJob* job = d->vcs->diff(url, prev, rev ); connect(job, &VcsJob::finished, this, &VcsPluginHelper::diffJobFinished); d->plugin->core()->runController()->registerJob(job); } void VcsPluginHelper::history(const VcsRevision& rev) { SINGLEURL_SETUP_VARS QDialog* dlg = new QDialog(ICore::self()->uiController()->activeMainWindow()); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setWindowTitle(i18nc("%1: path or URL, %2: name of a version control system", "%2 History (%1)", url.toDisplayString(QUrl::PreferLocalFile), iface->name())); QVBoxLayout *mainLayout = new QVBoxLayout(dlg); KDevelop::VcsEventWidget* logWidget = new KDevelop::VcsEventWidget(url, rev, iface, dlg); mainLayout->addWidget(logWidget); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); dlg->connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept); dlg->connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject); mainLayout->addWidget(buttonBox); dlg->show(); } void VcsPluginHelper::annotation() { SINGLEURL_SETUP_VARS KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (!doc) doc = ICore::self()->documentController()->openDocument(url); KTextEditor::AnnotationInterface* annotateiface = qobject_cast(doc->textDocument()); KTextEditor::AnnotationViewInterface* viewiface = qobject_cast(doc->activeTextView()); if (viewiface && viewiface->isAnnotationBorderVisible()) { viewiface->setAnnotationBorderVisible(false); return; } if (doc && doc->textDocument() && iface) { KDevelop::VcsJob* job = iface->annotate(url); if( !job ) { qWarning() << "Couldn't create annotate job for:" << url << "with iface:" << iface << dynamic_cast( iface ); return; } QColor foreground(Qt::black); QColor background(Qt::white); if (KTextEditor::View* view = doc->activeTextView()) { KTextEditor::Attribute::Ptr style = view->defaultStyleAttribute(KTextEditor::dsNormal); foreground = style->foreground().color(); if (style->hasProperty(QTextFormat::BackgroundBrush)) { background = style->background().color(); } } if (annotateiface && viewiface) { KDevelop::VcsAnnotationModel* model = new KDevelop::VcsAnnotationModel(job, url, doc->textDocument(), foreground, background); annotateiface->setAnnotationModel(model); viewiface->setAnnotationBorderVisible(true); // can't use new signal slot syntax here, AnnotationInterface is not a QObject connect(doc->activeTextView(), SIGNAL(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int)), this, SLOT(annotationContextMenuAboutToShow(KTextEditor::View*,QMenu*,int))); } else { KMessageBox::error(nullptr, i18n("Cannot display annotations, missing interface KTextEditor::AnnotationInterface for the editor.")); delete job; } } else { KMessageBox::error(nullptr, i18n("Cannot execute annotate action because the " "document was not found, or was not a text document:\n%1", url.toDisplayString(QUrl::PreferLocalFile))); } } void VcsPluginHelper::annotationContextMenuAboutToShow( KTextEditor::View* view, QMenu* menu, int line ) { KTextEditor::AnnotationInterface* annotateiface = qobject_cast(view->document()); VcsAnnotationModel* model = qobject_cast( annotateiface->annotationModel() ); Q_ASSERT(model); VcsRevision rev = model->revisionForLine(line); // check if the user clicked on a row without revision information if (rev.revisionType() == VcsRevision::Invalid) { - // in this case, do not action depending on revision informations + // in this case, do not action depending on revision information return; } d->diffForRevAction->setData(QVariant::fromValue(rev)); d->diffForRevGlobalAction->setData(QVariant::fromValue(rev)); menu->addSeparator(); menu->addAction(d->diffForRevAction); menu->addAction(d->diffForRevGlobalAction); QAction* copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Revision")); connect(copyAction, &QAction::triggered, this, [this, rev]() { QApplication::clipboard()->setText(rev.revisionValue().toString()); }); QAction* historyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("History...")); connect(historyAction, &QAction::triggered, this, [this, rev]() { history(rev); }); } void VcsPluginHelper::update() { EXECUTE_VCS_METHOD(update); } void VcsPluginHelper::add() { EXECUTE_VCS_METHOD(add); } void VcsPluginHelper::commit() { Q_ASSERT(!d->ctxUrls.isEmpty()); ICore::self()->documentController()->saveAllDocuments(); QUrl url = d->ctxUrls.first(); // We start the commit UI no matter whether there is real differences, as it can also be used to commit untracked files VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(d->vcs, url)); bool ret = showVcsDiff(patchSource); if(!ret) { VcsCommitDialog *commitDialog = new VcsCommitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } void VcsPluginHelper::push() { foreach(const QUrl &url, d->ctxUrls) { VcsJob* job = d->plugin->extension()->push(url, VcsLocation()); ICore::self()->runController()->registerJob(job); } } void VcsPluginHelper::pull() { foreach(const QUrl &url, d->ctxUrls) { VcsJob* job = d->plugin->extension()->pull(VcsLocation(), url); ICore::self()->runController()->registerJob(job); } } }