diff --git a/Architecture.dox b/Architecture.dox index dc69b49654..1362988a91 100644 --- a/Architecture.dox +++ b/Architecture.dox @@ -1,407 +1,402 @@ /** \file Architecture.dox * \brief KDevelop architecture */ /** \page architecture KDevelop Architecture \section source_overview Platform Source Overview \subsection what_is_platform Platform Vs. Others The idea is that kdevplatform contains: - Everything to create platform plugins - Everything to create platform applications - Commonly used/important plugins . \subsection platform_code Platform Code Layout Platform consists of - interfaces - interfaces that expose everything necessary for plugins . - shell - implements interfaces and basically provides a ready-to-use and extend application. . - sublime - the user interface library . - project and language - additional libraries for project managers and language supports (usable for other plugins as well) . - plugins - common plugins, ie those that could be useful for other applications than just KDevelop . - outputview - allows plugins to easily implement toolviews that show line-by-line output. for example make build output. . - vcs - central & distributed version control library . . \subsection platform_for_plugins What to Use in Plugins - Plugins need to link to the interfaces - Plugins should never link to the shell - Plugins should not need to link to the sublime library - Plugins can optionally link to other helper libraries in platform when necessary . \subsection platform_coding_conventions Current Platform Coding Conventions - The preferred coding style is http://www.kdevelop.org/mediawiki/index.php/Coding_Guidelines - All platform classes shall be in the KDevelop namespace - All files should be named without a kdev prefix - All files have to be installed in subdirectories under ${INCLUDE_INSTALL_DIR}/kdevplatform/ - All interface names should start with an I (e.g. KDevelop::IFoo) and the files should be named ifoo.h and ifoo.cpp - All interface implementations are named KDevelop::Foo and their files foo.h/cpp . \section code_overview Platform Code Overview \subsection core ICore/Core Core is the central class. It provides access to all shell functionality. There's a KDevelop::ICore interface and a KDevelop::Core implementation. The KDevelop::ICore interface gives access to all controllers exposed via interfaces. Each plugin is initialized with an KDevelop::ICore pointer so it always has access to the core via the KDevelop::IPlugin::core method. Core is a singleton that needs to be manually initialized by platform applications using \ref KDevelop::Core::initialize right after the KApplication object is created and ShellExtension is initialized. KDevelop::ShellExtension the mechanism platform applications define which UI configuration files are used and what the default profile is. Use KDevelop::ShellExtension::defaultArea to define the name of default UI area (see below for more information about areas). \subsection plugin IPlugin All concrete plugins must inherit from KDevelop::IPlugin. Extension interfaces can be used to handle groups of similar plugins in a generic fashion, see below. Each plugin must have an associated .desktop file, as described in the KPluginInfo class documentation. \subsection extensions Extension Interfaces and Plugins The idea behind extension interfaces is to provide following features: - To allow plugins to expose their functionality via abstract interfaces and at the same time allow them to not care about binary compatibility and to not force a plugin to link against another plugin.\n \n The documentation plugin would be a good example of a piece of non-core functionality that could be exported via an IDocumentation extension interface with methods like lookupInDocumentation(const QString &) and others. The outputview plugin with has an IOutputView interface which is another example of such a use-case. The IOutputView interface provided by the outputview plugin can be used by other plugins to display information to the user with the other plugin not caring about the specific implementation details of the outputview plugin - To provide implementations of important functionality in plugins.\n \n Good examples are buildsystem managers, builders, and language supports in KDevelop4. When a project is opened, a buildsystem manager plugin (that implements either IBuildSystemManager interface or IProjectFileManager) is looked for by the shell and loaded. - To forget about binary compatibility issues of dependent plugins.\n \n In case the plugin has something new to expose, a new version of the extension interface has to be defined. All other plugins will either continue using the old interface or ask for the new one. . The extension interfaces framework is implemented with QExtensionManager and company. See the Qt documentation for more information. KDevelop::IBuildSystemManager is an example of an extension interface. Plugins that implement this provide build and/or compile functionality. An other example is KDevelop::ILanguageSupport which abstracts away language specific details from shell. \subsubsection declare_extension To declare an extension interface: - Create an abstract class - Add macros declaring the interface to your abstract class' header file - If your abstract class is in a namespace use the following: \code Q_DECLARE_INTERFACE( KDevelop::IMyInterface, "org.kdevelop.IMyInterface" ) \endcode - Otherwise use: \code Q_DECLARE_INTERFACE( IMyInterface, "org.kdevelop.IMyInterface" ) \endcode . \note The use of nested namespaces is not supported. . \subsubsection implement_extension To implement an extension interface: - Create a class that inherits from KDevelop::IPlugin - Add the interface you wish to implement to the inheritance chain for your new plugin class (use multiple inheritance here) - Add a Q_INTERFACES macro to your class declaration. \n \n It should be in the same section as the Q_OBJECT macro. The argument to the Q_INTERFACES macro should be the name of the interface you're inheriting from. \n \n For example, if one is using the IMyInterface interface, one would add \code Q_INTERFACES( IMyInterface ) \endcode to one's class declaration. -- Use KDEV_USE_EXTENSION_INTERFACE macro in the constructor of the plugin: - \code - KDEV_USE_EXTENSION_INTERFACE( IMyInterface ) - \endcode - Add the following to the plugin's .desktop file: \code X-KDevelop-Interfaces=IMyInterface \endcode Replacing IMyInterface which the class name of one's interface. . Here's an example putting all of the above together: \code class MyPlugin: public KDevelop::IPlugin, public KDevelop::IMyInterface { Q_OBJECT Q_INTERFACES(KDevelop::IMyInterface) public: MyPlugin(QObject* parent) : IPlugin( parent ) { - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IMyInterface ) } }; \endcode \subsubsection load_extension To load a plugin that supports a certain extension interface: - Use KDevelop::IPluginController::pluginForExtension if you have only one possible plugin implementing the extension interface - Or use KDevelop::IPluginController::allPluginsForExtension to return a list of plugins. \n \note The above methods will load plugins to verify they implement the extension being asked for if needed. - Once an KDevelop::IPlugin pointer is returned by one of two methods above, it can be asked for an extension using KDevelop::IPlugin::extension . \subsubsection interplugin_dependency To set a dependency between interfaces: - Set the list of required interfaces in plugin's .desktop file \code X-KDevelop-IRequired=IMyInterface,IMyOtherInterface \endcode . It is not possible to set direct inter-plugin dependencies. One plugin shall never depend on another plugin, it shall only depend on something that implements an interface. \subsection project Project Management Infrastructure KDevelop4 can have multiple projects open at any given time. Project management structure: \code |--------------------| |-------------------------------| |---->| KDevelop::IProject |------>| KDevelop::IProjectFileManager | | |--------------------| |-------------------------------| | | |------------------------------| | |------------------------| | KDevelop::IProjectController |------->| KDevelop::ProjectModel | |------------------------------| | |------------------------| | | | |--------------------| |-------------------------------| |---->| KDevelop::IProject |------>| KDevelop::IBuildSystemManager | |--------------------| |-------------------------------| \endcode ProjectController is a container for projects (KDevelop::IProject interface). Each project contributes its contents (files, folders, targets, etc.) to the KDevelop::ProjectModel. Each project also has a file manager plugin associated with it. A project file manager is the plugin which implements either one of two interfaces: KDevelop::IProjectFileManager or KDevelop::IBuildSystemManager. The project file manager provides the actual project management facilities. Plugins access project contents through classes defined in KDevelop::ProjectModel. Examples include ProjectFolderItem, ProjectFileItem, ProjectTargetItem, etc. %KDevelop currently supports the notion of "current project" (the one which is currently selected in the project management view). But plugins are encouraged to not depend on the notion of a current project. The selection might be empty or the project management view might be closed at any time. %KDevelop Platform provides by default a generic project manager. This project manager treats all files and subdirectories under the project directory as project items and currently provides no building facilities. The project file (<projectname>.kdev4) controls which project file manager will be loaded. For example, this .kdev4 file will load the generic manager: \code [General Options] Manager=KDevGenericManager \endcode \subsection language Language Support Infrastructure The language support infrastructure is designed to be similar to project management. Its goals are: - use as many language supports as necessary at the same time - be able to load several language supports for one source file good examples are mixed-source files like .php (php+html), .rhtml (ruby + html) - be not dependent on projects . language support structure: \code |----------------------------| |---->| KDevelop::ILanguageSupport | |---------------------| | |----------------------------| |---->| KDevelop::ILanguage |----| | |---------------------| | |----------------------------| | |---->| KDevelop::BackgroundParser | |-------------------------------| | |----------------------------| | KDevelop::ILanguageController |--| |-------------------------------| | |----------------------------| | |---->| KDevelop::ILanguageSupport | | |---------------------| | |----------------------------| |---->| KDevelop::ILanguage |----| |---------------------| | |----------------------------| |---->| KDevelop::BackgroundParser | |----------------------------| \endcode Language controller holds the set of already loaded languages and provides means to load more. For each language (defined by its "name") Language object exists. Each such language has a background parser and a actual support plugin that implements KDevelop::ILanguageSupport. This way the basic shell functionality (like language loading algorithm and background parser) is separated from language-specific stuff (like parsing). Unlike KDevelop3, language support plugin is loaded not among with a project. Instead, when the source file is opened, the language controller asks plugin controller whether there are any language plugins (those that implement KDevelop::ILanguageSupport) that support a mime type of the file (those who set X-KDevelop-SupportedMimeTypes=...). For each language support plugin found, the KDevelop::Language object (that implements KDevelop::ILanguage interface) is created and language support plugin is associated with it. Then each language is asked to return a KDevelop::ParseJob to process the file. This way several language supports are able to parse one file. If KDevelop::Language object for given mimetype already exists, it is used and no plugin loading is performed. \subsection uicontroller IUiController KDevelop::UiController is closely connected to the Sublime UI and basically is an subclass of \ref Sublime::Controller which will be explained later. The main job of UiController is to allow plugins to manager their views and toolviews. Currently only toolviews can be added and removed. Sublime UI wants plugins to add and remove not actual toolviews, but factories to create them. This is because user can request from Sublime UI to show a new toolview at any time. For example, it is possible to have more than one Konsole toolviews. The user just have to ask for them. Automatically factory will be used only once, when UI controller is asked to add a toolview. This means for example that only one Konsole toolview will be opened automatically. To create a factory, a plugin needs to subclass KDevelop::IToolViewFactory and implement two methods: - virtual QWidget* KDevelop::IToolViewFactory::create(QWidget *parent = 0) where the actual toolview widget will be created - virtual Qt::DockWidgetArea KDevelop::IToolViewFactory::defaultPosition(const QString &areaName) which will give the UI a hint on where to place the toolview . Once the factory is ready, it has to be added to the UiController using IUiController::addToolView() method. It is not absolutely necessary for a plugin to remove or delete toolview factory. UiController will delete them all upon unload. NOTE: temporarily IUiController has KDevelop::IUiController::openUrl() method which is the only way to open files in KDevelop until DocumentController is ported. \subsection launch Launch Framework - Launch Configuration (\ref KDevelop::ILaunchConfiguration)
one entry created by the user in Configure Launches (ie an executable) - has one Launch Configuration Type - has a Launcher for every possible Launch Mode - Launch Configuration Type (\ref KDevelop::LaunchConfigurationType)
native application / script - has possible Launchers - Launcher (\ref KDevelop::ILauncher)
creates the KJob for launching - has LaunchConfigurationPageFactories - supports a number of Launch Modes - Launch Mode: execute / debug / profile - Launch Configuration Page Factory (\ref KDevelop::LaunchConfigurationPageFactory)
creates Widget used in Configure Launches - Launch Configuration Page (\ref KDevelop::LaunchConfigurationPage)
Widget that configures a LaunchConfiguration - RunController (\ref KDevelop::IRunController) - has possible Launch Configuration Types \section sublime Sublime UI \subsection sublime_operation Modus Operandi - UI provides support for "areas" (alike Eclipse's perspectives) - The basic set of areas is: - code editing area with split editor windows \n (with kate/konqueror-like splitting) - debugging area \n (Xcode-like debugger window with only one editor view by default but with possiblility to show more) - profiling area \n (like KCacheGrind) - user interface design area \n (like Qt designer) - Area configuration includes code editor windows (unlike eclipse) - Each area can be displayed in usual Qt mainwindow with toolviews in dockwidgets - Areas are shown in separate mainwindows so that multiple-monitor setups become the best supported - One area can be shown in two and more mainwindows by "cloning" the area \n (unlike Eclipse that pretends that two mainwindows show the same area) - Optionally areas can be switched inside the same mainwindow without opening new ones (like in Eclipse), but this mode of operation is currently not implemented. \n Also Sublime was not optimized for this use-case. See wiki pages for a detailed discussion and explanation of possible problems. - It is possible to open as many similar toolviews as necessary (for example, several konsole's) - It is possible to open several views for the same file in code editor view (unlike KDevelop 3.x) - Instead of tabs for editor windows a "switcher" is provided. \n Currently the switcher is a combobox but that will change for sure. . \subsection sublime_architecture Brief Architecture Overview Sublime::Controller is the central point of the whole Sublime infrastructure. It contains areas, documents, and controlls mainwindows. \n Sublime::Controller::showArea() is the only way to show an area in the mainwindow. Sublime::MainWindow is just a KParts::MainWindow which knows how to "reconstruct" the area and react on area changes. Additionally it knows which view and toolview is "active" (i.e. last focused). Sublime::Area is the object that controls views, toolviews, and defines their placement inside the mainwindow. Provides various view management (e.g. add/remove/etc.) methods and signals. Also Area is responsible for storing and restoring the view layout (on-disk storage is not implemented currently). An area is identified by its name. Each KDevelop platform application, for example, has to define a concept of a "default area" in ShellExtension so that the UiController can load it by default. While areas operate on views, the Sublime Controller deals with Sublime::Document objects only. Sublime::View is only a thin wrapper around QWidget. Document is what provides views. Sublime::Document::createView() is the only way to get a view. When createView() is called, the protected Sublime::Document::createViewWidget() is called to return the actual widget for a view. There is an abstract Sublime::Document class with no createViewWidget() implementation and there's a convenience class Sublime::ToolDocument that can create widgets of user-specified type. KDevelop currently uses Sublime::PartDocument which is subclassed by KDevelop::PartDocument. This PartDocument creates a view by loading a part and then asking a part about its widget. */ diff --git a/CMakeLists.txt b/CMakeLists.txt index ec120108d2..159a4de2ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,174 +1,174 @@ # kdevplatform version set(KDEVPLATFORM_VERSION_MAJOR 5) set(KDEVPLATFORM_VERSION_MINOR 0) set(KDEVPLATFORM_VERSION_PATCH 3) # plugin versions listed in the .desktop files -set(KDEV_PLUGIN_VERSION 26) +set(KDEV_PLUGIN_VERSION 27) # Increase this to reset incompatible item-repositories set(KDEV_ITEMREPOSITORY_VERSION 86) # library version / SO version set(KDEVPLATFORM_LIB_VERSION 10.0.0) set(KDEVPLATFORM_LIB_SOVERSION 10) ################################################################################ cmake_minimum_required(VERSION 2.8.12) project(KDevPlatform) # we need some parts of the ECM CMake helpers find_package (ECM 0.0.9 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${KDevPlatform_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMOptionalAddSubdirectory) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMMarkAsTest) include(ECMMarkNonGuiExecutable) include(ECMGenerateHeaders) include(ECMPackageConfigHelpers) include(GenerateExportHeader) include(FeatureSummary) include(WriteBasicConfigVersionFile) include(CheckFunctionExists) include(KDEInstallDirs) include(KDECMakeSettings) include(KDevPlatformMacros) set(QT_MIN_VERSION "5.4.0") find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core DBus Widgets WebKitWidgets Concurrent Test) find_package(Qt5QuickWidgets ${QT_MIN_VERSION}) set_package_properties(Qt5QuickWidgets PROPERTIES PURPOSE "Qt5 QuickWidgets library (part of Qt >=5.3). Required for the Welcome Page plugin." ) set(KF5_DEP_VERSION "5.16.0") find_package(KF5 ${KF5_DEP_VERSION} REQUIRED COMPONENTS Archive Config GuiAddons WidgetsAddons IconThemes I18n ItemModels ItemViews JobWidgets KCMUtils KIO NewStuff Notifications NotifyConfig Parts Service Sonnet TextEditor ThreadWeaver WindowSystem Declarative XmlGui ) find_package(Grantlee5) set_package_properties(Grantlee5 PROPERTIES PURPOSE "Grantlee templating library, needed for file templates" URL "http://www.grantlee.org/" TYPE RECOMMENDED) set(Boost_ADDITIONAL_VERSIONS 1.39.0 1.39) find_package(Boost 1.35.0) set_package_properties(Boost PROPERTIES PURPOSE "Boost libraries for enabling the classbrowser" URL "http://www.boost.org" TYPE REQUIRED) add_definitions( -DQT_DEPRECATED_WARNINGS -DQT_DISABLE_DEPRECATED_BEFORE=0x050400 -DQT_NO_URL_CAST_FROM_STRING -DQT_STRICT_ITERATORS -DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS ) # Turn off missing-field-initializers warning for GCC to avoid noise from false positives with empty {} # See discussion: http://mail.kde.org/pipermail/kdevelop-devel/2014-February/046910.html check_cxx_compiler_flag(-Wno-missing-field-initializers HAVE_MFI_FLAG) check_cxx_compiler_flag(-Werror=undefined-bool-conversion HAVE_UBC_FLAG) check_cxx_compiler_flag(-Werror=tautological-undefined-compare HAVE_TUC_FLAG) if (HAVE_MFI_FLAG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-missing-field-initializers") endif() if (HAVE_UBC_FLAG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=undefined-bool-conversion") endif() if (HAVE_TUC_FLAG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=tautological-undefined-compare") endif() configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/config-kdevplatform.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kdevplatform.h ) include_directories(${KDevPlatform_SOURCE_DIR} ${KDevPlatform_BINARY_DIR}) string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_TOLOWER) if(CMAKE_BUILD_TYPE_TOLOWER MATCHES "debug" OR CMAKE_BUILD_TYPE_TOLOWER STREQUAL "") set(COMPILER_OPTIMIZATIONS_DISABLED TRUE) else() set(COMPILER_OPTIMIZATIONS_DISABLED FALSE) endif() add_subdirectory(sublime) add_subdirectory(interfaces) add_subdirectory(project) add_subdirectory(language) add_subdirectory(shell) add_subdirectory(util) add_subdirectory(outputview) add_subdirectory(vcs) add_subdirectory(pics) add_subdirectory(debugger) add_subdirectory(documentation) add_subdirectory(serialization) add_subdirectory(template) add_subdirectory(tests) add_subdirectory(plugins) set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KDevPlatform") ecm_configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/KDevPlatformConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KDevPlatformConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) ecm_setup_version(${KDEVPLATFORM_VERSION_MAJOR}.${KDEVPLATFORM_VERSION_MINOR}.${KDEVPLATFORM_VERSION_PATCH} VARIABLE_PREFIX KDEVPLATFORM VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kdevplatform_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KDevPlatformConfigVersion.cmake" SOVERSION ${KDEVPLATFORM_LIB_SOVERSION}) install( FILES "${KDevPlatform_BINARY_DIR}/kdevplatform_version.h" "${KDevPlatform_BINARY_DIR}/config-kdevplatform.h" DESTINATION "${KDE_INSTALL_INCLUDEDIR}/kdevplatform" ) install( FILES "${KDevPlatform_BINARY_DIR}/KDevPlatformConfig.cmake" "${KDevPlatform_BINARY_DIR}/KDevPlatformConfigVersion.cmake" cmake/modules/KDevPlatformMacros.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) install( EXPORT KDevPlatformTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" NAMESPACE KDev:: FILE KDevPlatformTargets.cmake ) include(CTest) # CTestCustom.cmake has to be in the CTEST_BINARY_DIR. # in the KDE build system, this is the same as CMAKE_BINARY_DIR. configure_file(${CMAKE_SOURCE_DIR}/CTestCustom.cmake ${CMAKE_BINARY_DIR}/CTestCustom.cmake) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/interfaces/iplugin.cpp b/interfaces/iplugin.cpp index 7a78a1126b..a96dfffd94 100644 --- a/interfaces/iplugin.cpp +++ b/interfaces/iplugin.cpp @@ -1,219 +1,208 @@ /* This file is part of the KDE project Copyright 2002 Simon Hausmann Copyright 2002 Matthias Hoelzer-Kluepfel Copyright 2002 Harald Fernengel Copyright 2002 Falk Brettschneider Copyright 2003 Julian Rockey Copyright 2003 Roberto Raggi Copyright 2003 Jens Dagerbo Copyright 2003 Mario Scalas Copyright 2003-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. */ #include "iplugin.h" #include #include #include #include #include "icore.h" #include "iplugincontroller.h" #include "iprojectcontroller.h" #include "iproject.h" #include "contextmenuextension.h" namespace KDevelop { class IPluginPrivate { public: IPluginPrivate(IPlugin *q) : q(q) {} ~IPluginPrivate() { } void guiClientAdded(KXMLGUIClient *client) { if (client != q) return; q->initializeGuiState(); updateState(); } void updateState() { const int projectCount = ICore::self()->projectController()->projectCount(); IPlugin::ReverseStateChange reverse = IPlugin::StateNoReverse; if (! projectCount) reverse = IPlugin::StateReverse; q->stateChanged(QStringLiteral("has_project"), reverse); } IPlugin *q; ICore *core; - QVector m_extensions; QString m_errorDescription; }; IPlugin::IPlugin( const QString &componentName, QObject *parent ) : QObject(parent) , KXMLGUIClient() , d(new IPluginPrivate(this)) { // The following cast is safe, there's no component in KDevPlatform that // creates plugins except the plugincontroller. The controller passes // Core::self() as parent to KServiceTypeTrader::createInstanceFromQuery // so we know the parent is always a Core* pointer. // This is the only way to pass the Core pointer to the plugin during its // creation so plugins have access to ICore during their creation. Q_ASSERT(qobject_cast(parent)); d->core = static_cast(parent); setComponentName(componentName, componentName); auto clientAdded = [=] (KXMLGUIClient* client) { d->guiClientAdded(client); }; foreach (KMainWindow* mw, KMainWindow::memberList()) { KXmlGuiWindow* guiWindow = qobject_cast(mw); if (! guiWindow) continue; connect(guiWindow->guiFactory(), &KXMLGUIFactory::clientAdded, this, clientAdded); } auto updateState = [=] { d->updateState(); }; connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, updateState); connect(ICore::self()->projectController(), &IProjectController::projectClosed, this, updateState); } IPlugin::~IPlugin() { delete d; } void IPlugin::unload() { } ICore *IPlugin::core() const { return d->core; } } -QVector KDevelop::IPlugin::extensions( ) const -{ - return d->m_extensions; -} - -void KDevelop::IPlugin::addExtension( const QByteArray& ext ) -{ - d->m_extensions << ext; -} - KDevelop::ContextMenuExtension KDevelop::IPlugin::contextMenuExtension( KDevelop::Context* ) { return KDevelop::ContextMenuExtension(); } void KDevelop::IPlugin::initializeGuiState() { } class CustomXmlGUIClient : public KXMLGUIClient { public: CustomXmlGUIClient(const QString& componentName) { // TODO KF5: Get rid off this setComponentName(componentName, componentName); } using KXMLGUIClient::setXMLFile; }; KXMLGUIClient* KDevelop::IPlugin::createGUIForMainWindow(Sublime::MainWindow* window) { QScopedPointer ret(new CustomXmlGUIClient(componentName())); QString file; createActionsForMainWindow(window, file, *ret->actionCollection()); if (ret->actionCollection()->isEmpty()) { return nullptr; } Q_ASSERT(!file.isEmpty()); //A file must have been set ret->setXMLFile(file); return ret.take(); } void KDevelop::IPlugin::createActionsForMainWindow( Sublime::MainWindow* /*window*/, QString& /*xmlFile*/, KActionCollection& /*actions*/ ) { } bool KDevelop::IPlugin::hasError() const { return !d->m_errorDescription.isEmpty(); } void KDevelop::IPlugin::setErrorDescription(const QString& description) { d->m_errorDescription = description; } QString KDevelop::IPlugin::errorDescription() const { return d->m_errorDescription; } int KDevelop::IPlugin::configPages() const { return 0; } KDevelop::ConfigPage* KDevelop::IPlugin::configPage (int, QWidget*) { return nullptr; } int KDevelop::IPlugin::perProjectConfigPages() const { return 0; } KDevelop::ConfigPage* KDevelop::IPlugin::perProjectConfigPage(int, const ProjectConfigOptions&, QWidget*) { return nullptr; } #include "moc_iplugin.cpp" diff --git a/interfaces/iplugin.h b/interfaces/iplugin.h index 73985c98cb..7eb3138031 100644 --- a/interfaces/iplugin.h +++ b/interfaces/iplugin.h @@ -1,285 +1,281 @@ /* 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 #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) \ - addExtension(QByteArray::fromRawData(qobject_interface_iid(), \ - static_cast(strlen(qobject_interface_iid())))); + _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-Language= * 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-Language is the name of the language 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; - Q_SCRIPTABLE QVector extensions() const; - - template Extension* extension() + /** + * Convenience API to access an interface inherited by this plugin + * + * @return Instance to the specified interface, or nullptr + */ + template + inline Extension* extension() { - const auto extensionIID = QByteArray::fromRawData(qobject_interface_iid(), - static_cast(strlen(qobject_interface_iid()))); - if (extensions().contains(extensionIID)) { - return qobject_cast(this); - } - return nullptr; + 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 * 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: - void addExtension( const QByteArray& ); - /** * Initialize the XML GUI State. */ virtual void initializeGuiState(); private: friend class IPluginPrivate; class IPluginPrivate* const d; }; } #endif diff --git a/plugins/appwizard/appwizardplugin.cpp b/plugins/appwizard/appwizardplugin.cpp index 59d17a5e0d..ed6274218f 100644 --- a/plugins/appwizard/appwizardplugin.cpp +++ b/plugins/appwizard/appwizardplugin.cpp @@ -1,516 +1,515 @@ /*************************************************************************** * Copyright 2001 Bernd Gehrmann * * Copyright 2004-2005 Sascha Cunz * * Copyright 2005 Ian Reinhart Geiser * * Copyright 2007 Alexander Dymo * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "appwizardplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "appwizarddialog.h" #include "projectselectionpage.h" #include "projectvcspage.h" #include "projecttemplatesmodel.h" #include "debug.h" using namespace KDevelop; Q_LOGGING_CATEGORY(PLUGIN_APPWIZARD, "kdevplatform.plugins.appwizard") K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin();) AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent) , m_templatesModel(nullptr) { - KDEV_USE_EXTENSION_INTERFACE(KDevelop::ITemplateProvider); setXMLFile(QStringLiteral("kdevappwizard.rc")); m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new")); m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); m_newFromTemplate->setText(i18n("New From Template...")); connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject); m_newFromTemplate->setToolTip( i18n("Generate a new project from a template") ); m_newFromTemplate->setWhatsThis( i18n("This starts KDevelop's application wizard. " "It helps you to generate a skeleton for your " "application from a set of templates.") ); } AppWizardPlugin::~AppWizardPlugin() { } void AppWizardPlugin::slotNewProject() { model()->refresh(); AppWizardDialog dlg(core()->pluginController(), m_templatesModel); if (dlg.exec() == QDialog::Accepted) { QString project = createProject( dlg.appInfo() ); if (!project.isEmpty()) { core()->projectController()->openProject(QUrl::fromLocalFile(project)); KConfig templateConfig(dlg.appInfo().appTemplate); KConfigGroup general(&templateConfig, "General"); QString file = general.readEntry("ShowFilesAfterGeneration"); if (!file.isEmpty()) { file = KMacroExpander::expandMacros(file, m_variables); core()->documentController()->openDocument(QUrl::fromUserInput(file)); } } else { KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n("Could not create project from template\n"), i18n("Failed to create project") ); } } } namespace { IDistributedVersionControl* toDVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } ICentralizedVersionControl* toCVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } /*! Trouble while initializing version control. Show failure message to user. */ void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString()) { QString displayDetails = details; if (displayDetails.isEmpty()) { displayDetails = i18n("Please see the Version Control toolview"); } KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18n("Version Control System Error")); KIO::del(dest)->exec(); tmpdir.remove(); } /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(dvcs); qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS"; const QUrl& dest = info.location; //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir VcsJob* job = dvcs->init(dest); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest); return false; } qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest; job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest); return false; } job = dvcs->commit(QStringLiteral("initial project import from KDevelop"), {dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString()); return false; } return true; // We're good } /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(cvcs); qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to" << info.repository.repositoryServer(); VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } qCDebug(PLUGIN_APPWIZARD) << "Checking out"; job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } return true; // initialization phase complete } QString generateIdentifier( const QString& appname ) { QString tmp = appname; QRegExp re("[^a-zA-Z0-9_]"); return tmp.replace(re, QStringLiteral("_")); } } // end anonymous namespace QString AppWizardPlugin::createProject(const ApplicationInfo& info) { QFileInfo templateInfo(info.appTemplate); if (!templateInfo.exists()) { qWarning() << "Project app template does not exist:" << info.appTemplate; return QString(); } QString templateName = templateInfo.baseName(); QString templateArchive; const QStringList filters = {templateName + QStringLiteral(".*")}; const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory); foreach(const QString& matchesPath, matchesPaths) { const QStringList files = QDir(matchesPath).entryList(filters); if(!files.isEmpty()) { templateArchive = matchesPath + files.first(); } } if(templateArchive.isEmpty()) { qWarning() << "Template name does not exist in the template list"; return QString(); } QUrl dest = info.location; //prepare variable substitution hash m_variables.clear(); m_variables[QStringLiteral("APPNAME")] = info.name; m_variables[QStringLiteral("APPNAMEUC")] = info.name.toUpper(); m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower(); m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name); m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile(); // backwards compatibility m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")]; m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName(); m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName; KArchive* arch = nullptr; if( templateArchive.endsWith(QLatin1String(".zip")) ) { arch = new KZip(templateArchive); } else { arch = new KTar(templateArchive, QStringLiteral("application/x-bzip")); } if (arch->open(QIODevice::ReadOnly)) { QTemporaryDir tmpdir; QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName ); if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension() ) ) { if( !QFileInfo::exists( dest.toLocalFile() ) ) { QDir::root().mkpath( dest.toLocalFile() ); } unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory } else { QUrl url = KIO::upUrl(dest); if(!QFileInfo::exists(url.toLocalFile())) { QDir::root().mkpath(url.toLocalFile()); } } if ( !unpackArchive( arch->directory(), unpackDir ) ) { QString errorMsg = i18n("Could not create new project"); vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir)); return QString(); } if( !info.vcsPluginName.isEmpty() ) { if (!plugin) { // Red Alert, serious program corruption. // This should never happen, the vcs dialog presented a list of vcs // systems and now the chosen system doesn't exist anymore?? tmpdir.remove(); return QString(); } IDistributedVersionControl* dvcs = toDVCS(plugin); ICentralizedVersionControl* cvcs = toCVCS(plugin); bool success = false; if (dvcs) { success = initializeDVCS(dvcs, info, tmpdir); } else if (cvcs) { success = initializeCVCS(cvcs, info, tmpdir); } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel(nullptr, QStringLiteral("Failed to initialize version control system, " "plugin is neither VCS nor DVCS."))) success = true; } if (!success) return QString(); } tmpdir.remove(); }else { qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive"; return QString(); } QString projectFileName = QDir::cleanPath( dest.toLocalFile() + '/' + info.name + ".kdev4" ); // Loop through the new project directory and try to detect the first .kdev4 file. // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the // project templates can be more complex. QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories); if(it.hasNext() == true) { projectFileName = it.next(); } qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ; if( ! QFileInfo::exists( projectFileName ) ) { qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file"; KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig ); KConfigGroup project = cfg->group( "Project" ); project.writeEntry( "Name", info.name ); QString manager = QStringLiteral("KDevGenericManager"); QDir d( dest.toLocalFile() ); auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); foreach(const KPluginMetaData& info, data) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); if (!filter.isEmpty()) { if (!d.entryList(filter).isEmpty()) { manager = info.pluginId(); break; } } } project.writeEntry( "Manager", manager ); project.sync(); cfg->sync(); KConfigGroup project2 = cfg->group( "Project" ); qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" ); } return projectFileName; } bool AppWizardPlugin::unpackArchive(const KArchiveDirectory *dir, const QString &dest) { qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest; const QStringList entries = dir->entries(); qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QStringLiteral(",")); //This extra tempdir is needed just for the files files have special names, //which may contain macros also files contain content with macros. So the //easiest way to extract the files from the archive and then rename them //and replace the macros is to use a tempdir and copy the file (and //replacing while copying). This also allows one to easily remove all files, //by just unlinking the tempdir QTemporaryDir tdir; bool ret = true; foreach (const QString& entry, entries) { if (entry.endsWith(QLatin1String(".kdevtemplate"))) continue; if (dir->entry(entry)->isDirectory()) { const KArchiveDirectory *file = (KArchiveDirectory *)dir->entry(entry); QString newdest = dest + '/' + KMacroExpander::expandMacros(file->name(), m_variables); if( !QFileInfo::exists( newdest ) ) { QDir::root().mkdir( newdest ); } ret |= unpackArchive(file, newdest); } else if (dir->entry(entry)->isFile()) { const KArchiveFile *file = (KArchiveFile *)dir->entry(entry); file->copyTo(tdir.path()); QString destName = dest + '/' + file->name(); if (!copyFileAndExpandMacros(QDir::cleanPath(tdir.path()+'/'+file->name()), KMacroExpander::expandMacros(destName, m_variables))) { KMessageBox::sorry(nullptr, i18n("The file %1 cannot be created.", dest)); return false; } } } tdir.remove(); return ret; } bool AppWizardPlugin::copyFileAndExpandMacros(const QString &source, const QString &dest) { qCDebug(PLUGIN_APPWIZARD) << "copy:" << source << "to" << dest; QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(source); if( !mime.inherits(QStringLiteral("text/plain")) ) { KIO::CopyJob* job = KIO::copy( QUrl::fromUserInput(source), QUrl::fromUserInput(dest), KIO::HideProgressInfo ); if( !job->exec() ) { return false; } return true; } else { QFile inputFile(source); QFile outputFile(dest); if (inputFile.open(QFile::ReadOnly) && outputFile.open(QFile::WriteOnly)) { QTextStream input(&inputFile); input.setCodec(QTextCodec::codecForName("UTF-8")); QTextStream output(&outputFile); output.setCodec(QTextCodec::codecForName("UTF-8")); while(!input.atEnd()) { QString line = input.readLine(); output << KMacroExpander::expandMacros(line, m_variables) << "\n"; } #ifndef Q_OS_WIN // Preserve file mode... QT_STATBUF statBuf; QT_FSTAT(inputFile.handle(), &statBuf); // Unix only, won't work in Windows, maybe KIO::chmod could be used ::fchmod(outputFile.handle(), statBuf.st_mode); #endif return true; } else { inputFile.close(); outputFile.close(); return false; } } } KDevelop::ContextMenuExtension AppWizardPlugin::contextMenuExtension(KDevelop::Context* context) { KDevelop::ContextMenuExtension ext; if ( context->type() != KDevelop::Context::ProjectItemContext || !static_cast(context)->items().isEmpty() ) { return ext; } ext.addAction(KDevelop::ContextMenuExtension::ProjectGroup, m_newFromTemplate); return ext; } ProjectTemplatesModel* AppWizardPlugin::model() { if(!m_templatesModel) m_templatesModel = new ProjectTemplatesModel(this); return m_templatesModel; } QAbstractItemModel* AppWizardPlugin::templatesModel() { return model(); } QString AppWizardPlugin::knsConfigurationFile() const { return QStringLiteral("kdevappwizard.knsrc"); } QStringList AppWizardPlugin::supportedMimeTypes() const { QStringList types; types << QStringLiteral("application/x-desktop"); types << QStringLiteral("application/x-bzip-compressed-tar"); types << QStringLiteral("application/zip"); return types; } QIcon AppWizardPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("project-development-new-template")); } QString AppWizardPlugin::name() const { return i18n("Project Templates"); } void AppWizardPlugin::loadTemplate(const QString& fileName) { model()->loadTemplateFile(fileName); } void AppWizardPlugin::reload() { model()->refresh(); } #include "appwizardplugin.moc" diff --git a/plugins/bazaar/bazaarplugin.cpp b/plugins/bazaar/bazaarplugin.cpp index ed23c2fbd2..31853d928a 100644 --- a/plugins/bazaar/bazaarplugin.cpp +++ b/plugins/bazaar/bazaarplugin.cpp @@ -1,339 +1,336 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * 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 "bazaarplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bazaarutils.h" #include "bzrannotatejob.h" #include "copyjob.h" #include "diffjob.h" using namespace KDevelop; BazaarPlugin::BazaarPlugin(QObject* parent, const QVariantList& args) : IPlugin(QStringLiteral("kdevbazaar"), parent), m_vcsPluginHelper(new KDevelop::VcsPluginHelper(this, this)) { Q_UNUSED(args); // What is this? if (QStandardPaths::findExecutable(QStringLiteral("bzr")).isEmpty()) { setErrorDescription(i18n("Unable to find Bazaar (bzr) executable Is it installed on the system?")); return; } - KDEV_USE_EXTENSION_INTERFACE(KDevelop::IBasicVersionControl) - KDEV_USE_EXTENSION_INTERFACE(KDevelop::IDistributedVersionControl) - setObjectName(QStringLiteral("Bazaar")); } BazaarPlugin::~BazaarPlugin() { } QString BazaarPlugin::name() const { return QStringLiteral("Bazaar"); } VcsJob* BazaarPlugin::add(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Add); *job << "bzr" << "add"; if(recursion == NonRecursive) *job << "--no-recurse"; *job << localLocations; return job; } VcsJob* BazaarPlugin::annotate(const QUrl& localLocation, const VcsRevision& rev) { VcsJob* job = new BzrAnnotateJob(BazaarUtils::workingCopy(localLocation), BazaarUtils::getRevisionSpec(rev), localLocation, this, KDevelop::OutputJob::Silent); return job; } VcsJob* BazaarPlugin::commit(const QString& message, const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { QDir dir = BazaarUtils::workingCopy(localLocations[0]); DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); *job << "bzr" << "commit" << BazaarUtils::handleRecursion(localLocations, recursion) << "-m" << message; return job; } VcsJob* BazaarPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { return new CopyJob(localLocationSrc, localLocationDstn, this); } VcsImportMetadataWidget* BazaarPlugin::createImportMetadataWidget(QWidget* parent) { return new DvcsImportMetadataWidget(parent); } VcsJob* BazaarPlugin::createWorkingCopy(const VcsLocation& sourceRepository, const QUrl& destinationDirectory, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); // What is the purpose of recursion parameter? DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(sourceRepository.localUrl()), this); job->setType(VcsJob::Import); *job << "bzr" << "branch" << sourceRepository.localUrl().url() << destinationDirectory; return job; } VcsJob* BazaarPlugin::diff(const QUrl& fileOrDirectory, const VcsRevision& srcRevision, const VcsRevision& dstRevision, VcsDiff::Type, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); VcsJob* job = new DiffJob(BazaarUtils::workingCopy(fileOrDirectory), BazaarUtils::getRevisionSpecRange(srcRevision, dstRevision), fileOrDirectory, this); return job; } VcsJob* BazaarPlugin::init(const QUrl& localRepositoryRoot) { DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(localRepositoryRoot), this); job->setType(VcsJob::Import); *job << "bzr" << "init"; return job; } bool BazaarPlugin::isVersionControlled(const QUrl& localLocation) { QDir workCopy = BazaarUtils::workingCopy(localLocation); DVcsJob* job = new DVcsJob(workCopy, this, OutputJob::Silent); job->setType(VcsJob::Unknown); job->setIgnoreError(true); *job << "bzr" << "ls" << "--from-root" << "-R" << "-V"; job->exec(); if (job->status() == VcsJob::JobSucceeded) { QList filesAndDirectoriesList; foreach (const QString& fod, job->output().split('\n')) { filesAndDirectoriesList.append(QFileInfo(workCopy.absolutePath() + QDir::separator() + fod)); } QFileInfo fi(localLocation.toLocalFile()); if (fi.isDir() || fi.isFile()) { QFileInfo file(localLocation.toLocalFile()); return filesAndDirectoriesList.contains(file); } } return false; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, long unsigned int limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(rev) << "-l" << QString::number(limit); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, const VcsRevision& limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(limit, rev); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } void BazaarPlugin::parseBzrLog(DVcsJob* job) { QVariantList result; auto parts = job->output().split(QStringLiteral("------------------------------------------------------------"), QString::SkipEmptyParts); foreach (const QString& part, parts) { auto event = BazaarUtils::parseBzrLogPart(part); if (event.revision().revisionType() != VcsRevision::Invalid) result.append(QVariant::fromValue(event)); } job->setResults(result); } VcsJob* BazaarPlugin::move(const QUrl& localLocationSrc, const QUrl& localLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocationSrc), this); job->setType(VcsJob::JobType::Move); *job << "bzr" << "move" << localLocationSrc << localLocationDst; return job; } VcsJob* BazaarPlugin::pull(const VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { // API describes hg pull which is git fetch equivalent // bzr has pull, but it succeeds only if fast-forward is possible // in other cases bzr merge should be used instead (bzr pull would fail) // Information about repository must be provided at least once. DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Pull); *job << "bzr" << "pull"; if (!localOrRepoLocationSrc.localUrl().isEmpty()) { *job << localOrRepoLocationSrc.localUrl(); } // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::push(const QUrl& localRepositoryLocation, const VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Push); *job << "bzr" << "push" << localOrRepoLocationDst.localUrl(); // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::remove(const QList& localLocations) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Remove); *job << "bzr" << "remove" << localLocations; return job; } VcsJob* BazaarPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::JobType::Unknown); *job << "bzr" << "root" << localLocation; // It is only to make sure connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrRoot); return job; } void BazaarPlugin::parseBzrRoot(DVcsJob* job) { QString filename = job->dvcsCommand().at(2); QString rootDirectory = job->output(); QString localFilename = QFileInfo(QUrl::fromLocalFile(filename).toLocalFile()).absoluteFilePath(); QString result = localFilename.mid(localFilename.indexOf(rootDirectory) + rootDirectory.length()); job->setResults(QVariant::fromValue(result)); } VcsJob* BazaarPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); // How to provide "a conflict solving dialog to the user"? // In any case this plugin is unable to make any conflict. } VcsJob* BazaarPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Revert); *job << "bzr" << "revert" << BazaarUtils::handleRecursion(localLocations, recursion); return job; } VcsJob* BazaarPlugin::status(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Status); *job << "bzr" << "status" << "--short" << "--no-pending" << "--no-classify" << localLocations; connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrStatus); return job; } void BazaarPlugin::parseBzrStatus(DVcsJob* job) { QVariantList result; QSet filesWithStatus; QDir workingCopy = job->directory(); foreach (const QString& line, job->output().split('\n')) { auto status = BazaarUtils::parseVcsStatusInfoLine(line); result.append(QVariant::fromValue(status)); filesWithStatus.insert(BazaarUtils::concatenatePath(workingCopy, status.url())); } QStringList command = job->dvcsCommand(); for (auto it = command.constBegin() + command.indexOf(QStringLiteral("--no-classify")) + 1, itEnd = command.constEnd(); it != itEnd; ++it) { QString path = QFileInfo(*it).absoluteFilePath(); if (!filesWithStatus.contains(path)) { filesWithStatus.insert(path); KDevelop::VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUpToDate); status.setUrl(QUrl::fromLocalFile(*it)); result.append(QVariant::fromValue(status)); } } job->setResults(result); } VcsJob* BazaarPlugin::update(const QList& localLocations, const VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { // bzr update is stronger than API (it's effectively merge) // the best approximation is bzr pull DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); Q_UNUSED(recursion); // recursion and file locations are ignored - we can update only whole // working copy job->setType(VcsJob::JobType::Update); *job << "bzr" << "pull" << BazaarUtils::getRevisionSpec(rev); return job; } VcsLocationWidget* BazaarPlugin::vcsLocation(QWidget* parent) const { return new KDevelop::StandardVcsLocationWidget(parent); } ContextMenuExtension BazaarPlugin::contextMenuExtension(Context* context) { m_vcsPluginHelper->setupFromContext(context); QList const& ctxUrlList = m_vcsPluginHelper->contextUrlList(); bool isWorkingDirectory = false; for (const QUrl & url : ctxUrlList) { if (BazaarUtils::isValidDirectory(url)) { isWorkingDirectory = true; break; } } if (!isWorkingDirectory) { // Not part of a repository return ContextMenuExtension(); } QMenu* menu = m_vcsPluginHelper->commonActions(); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, menu->menuAction()); return menuExt; } diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp index bfbd880f7a..4ded5c16a4 100644 --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -1,1497 +1,1495 @@ /* * This file is part of KDevelop * * Copyright 2007 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 "contextbrowser.h" #include "contextbrowserview.h" #include "browsemanager.h" #include "debug.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 #include #include #include #include #include #include Q_LOGGING_CATEGORY(PLUGIN_CONTEXTBROWSER, "kdevplatform.plugins.contextbrowser") using KTextEditor::Attribute; using KTextEditor::View; // Helper that follows the QObject::parent() chain, and returns the highest widget that has no parent. QWidget* masterWidget(QWidget* w) { while(w && w->parent() && qobject_cast(w->parent())) w = qobject_cast(w->parent()); return w; } namespace { const unsigned int highlightingTimeout = 150; const float highlightingZDepth = -5000; const int maxHistoryLength = 30; // Helper that determines the context to use for highlighting at a specific position DUContext* contextForHighlightingAt(const KTextEditor::Cursor& position, TopDUContext* topContext) { DUContext* ctx = topContext->findContextAt(topContext->transformToLocalRevision(position)); while(ctx && ctx->parentContext() && (ctx->type() == DUContext::Template || ctx->type() == DUContext::Helper || ctx->localScopeIdentifier().isEmpty())) { ctx = ctx->parentContext(); } return ctx; } ///Duchain must be locked DUContext* getContextAt(const QUrl& url, KTextEditor::Cursor cursor) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (!topContext) return nullptr; return contextForHighlightingAt(KTextEditor::Cursor(cursor), topContext); } DeclarationPointer cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return DeclarationPointer(); } DUChainReadLocker lock; Declaration *decl = DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor(view->cursorPosition())).declaration); return DeclarationPointer(decl); } } class ContextBrowserViewFactory: public KDevelop::IToolViewFactory { public: ContextBrowserViewFactory(ContextBrowserPlugin *plugin): m_plugin(plugin) {} QWidget* create(QWidget *parent = nullptr) override { ContextBrowserView* ret = new ContextBrowserView(m_plugin, parent); return ret; } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ContextBrowser"); } private: ContextBrowserPlugin *m_plugin; }; KXMLGUIClient* ContextBrowserPlugin::createGUIForMainWindow( Sublime::MainWindow* window ) { m_browseManager = new BrowseManager(this); KXMLGUIClient* ret = KDevelop::IPlugin::createGUIForMainWindow(window); connect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); m_previousButton = new QToolButton(); m_previousButton->setToolTip(i18n("Go back in context history")); m_previousButton->setPopupMode(QToolButton::MenuButtonPopup); m_previousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); m_previousButton->setEnabled(false); m_previousButton->setFocusPolicy(Qt::NoFocus); m_previousMenu = new QMenu(); m_previousButton->setMenu(m_previousMenu); connect(m_previousButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyPrevious); connect(m_previousMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::previousMenuAboutToShow); m_nextButton = new QToolButton(); m_nextButton->setToolTip(i18n("Go forward in context history")); m_nextButton->setPopupMode(QToolButton::MenuButtonPopup); m_nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); m_nextButton->setEnabled(false); m_nextButton->setFocusPolicy(Qt::NoFocus); m_nextMenu = new QMenu(); m_nextButton->setMenu(m_nextMenu); connect(m_nextButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyNext); connect(m_nextMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::nextMenuAboutToShow); IQuickOpen* quickOpen = KDevelop::ICore::self()->pluginController()->extensionForPlugin(QStringLiteral("org.kdevelop.IQuickOpen")); if(quickOpen) { m_outlineLine = quickOpen->createQuickOpenLine(QStringList(), QStringList() << i18n("Outline"), IQuickOpen::Outline); m_outlineLine->setDefaultText(i18n("Outline...")); m_outlineLine->setToolTip(i18n("Navigate outline of active document, click to browse.")); } connect(m_browseManager, &BrowseManager::startDelayedBrowsing, this, &ContextBrowserPlugin::startDelayedBrowsing); connect(m_browseManager, &BrowseManager::stopDelayedBrowsing, this, &ContextBrowserPlugin::stopDelayedBrowsing); connect(m_browseManager, &BrowseManager::invokeAction, this, &ContextBrowserPlugin::invokeAction); m_toolbarWidget = toolbarWidgetForMainWindow(window); m_toolbarWidgetLayout = new QHBoxLayout; m_toolbarWidgetLayout->setSizeConstraint(QLayout::SetMaximumSize); m_previousButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_nextButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_toolbarWidgetLayout->setMargin(0); m_toolbarWidgetLayout->addWidget(m_previousButton); if (m_outlineLine) { m_toolbarWidgetLayout->addWidget(m_outlineLine); m_outlineLine->setMaximumWidth(600); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, m_outlineLine.data(), &IQuickOpenLine::clear); } m_toolbarWidgetLayout->addWidget(m_nextButton); if(m_toolbarWidget->children().isEmpty()) m_toolbarWidget->setLayout(m_toolbarWidgetLayout); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ContextBrowserPlugin::documentActivated); return ret; } void ContextBrowserPlugin::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevcontextbrowser.rc"); QAction* sourceBrowseMode = actions.addAction(QStringLiteral("source_browse_mode")); sourceBrowseMode->setText( i18n("Source &Browse Mode") ); sourceBrowseMode->setIcon( QIcon::fromTheme(QStringLiteral("arrow-up")) ); sourceBrowseMode->setCheckable(true); connect(sourceBrowseMode, &QAction::triggered, m_browseManager, &BrowseManager::setBrowsing); QAction* previousContext = actions.addAction(QStringLiteral("previous_context")); previousContext->setText( i18n("&Previous Visited Context") ); previousContext->setIcon( QIcon::fromTheme(QStringLiteral("go-previous-context") ) ); actions.setDefaultShortcut( previousContext, Qt::META | Qt::Key_Left ); QObject::connect(previousContext, &QAction::triggered, this, &ContextBrowserPlugin::previousContextShortcut); QAction* nextContext = actions.addAction(QStringLiteral("next_context")); nextContext->setText( i18n("&Next Visited Context") ); nextContext->setIcon( QIcon::fromTheme(QStringLiteral("go-next-context") ) ); actions.setDefaultShortcut( nextContext, Qt::META | Qt::Key_Right ); QObject::connect(nextContext, &QAction::triggered, this, &ContextBrowserPlugin::nextContextShortcut); QAction* previousUse = actions.addAction(QStringLiteral("previous_use")); previousUse->setText( i18n("&Previous Use") ); previousUse->setIcon( QIcon::fromTheme(QStringLiteral("go-previous-use")) ); actions.setDefaultShortcut( previousUse, Qt::META | Qt::SHIFT | Qt::Key_Left ); QObject::connect(previousUse, &QAction::triggered, this, &ContextBrowserPlugin::previousUseShortcut); QAction* nextUse = actions.addAction(QStringLiteral("next_use")); nextUse->setText( i18n("&Next Use") ); nextUse->setIcon( QIcon::fromTheme(QStringLiteral("go-next-use")) ); actions.setDefaultShortcut( nextUse, Qt::META | Qt::SHIFT | Qt::Key_Right ); QObject::connect(nextUse, &QAction::triggered, this, &ContextBrowserPlugin::nextUseShortcut); QWidgetAction* outline = new QWidgetAction(this); outline->setText(i18n("Context Browser")); QWidget* w = toolbarWidgetForMainWindow(window); w->setHidden(false); outline->setDefaultWidget(w); actions.addAction(QStringLiteral("outline_line"), outline); // Add to the actioncollection so one can set global shortcuts for the action actions.addAction(QStringLiteral("find_uses"), m_findUses); } void ContextBrowserPlugin::nextContextShortcut() { // TODO: cleanup historyNext(); } void ContextBrowserPlugin::previousContextShortcut() { // TODO: cleanup historyPrevious(); } K_PLUGIN_FACTORY_WITH_JSON(ContextBrowserFactory, "kdevcontextbrowser.json", registerPlugin();) ContextBrowserPlugin::ContextBrowserPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevcontextbrowser"), parent) , m_viewFactory(new ContextBrowserViewFactory(this)) , m_nextHistoryIndex(0) , m_textHintProvider(this) { - KDEV_USE_EXTENSION_INTERFACE( IContextBrowser ) - core()->uiController()->addToolView(i18n("Code Browser"), m_viewFactory); connect( core()->documentController(), &IDocumentController::textDocumentCreated, this, &ContextBrowserPlugin::textDocumentCreated ); connect( DUChain::self(), &DUChain::updateReady, this, &ContextBrowserPlugin::updateReady); connect( ColorCache::self(), &ColorCache::colorsGotChanged, this, &ContextBrowserPlugin::colorSetupChanged ); connect( DUChain::self(), &DUChain::declarationSelected, this, &ContextBrowserPlugin::declarationSelectedInUI ); m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect( m_updateTimer, &QTimer::timeout, this, &ContextBrowserPlugin::updateViews ); //Needed global action for the context-menu extensions m_findUses = new QAction(i18n("Find Uses"), this); connect(m_findUses, &QAction::triggered, this, &ContextBrowserPlugin::findUses); } ContextBrowserPlugin::~ContextBrowserPlugin() { ///TODO: QObject inheritance should suffice? delete m_nextMenu; delete m_previousMenu; delete m_toolbarWidgetLayout; delete m_previousButton; delete m_outlineLine; delete m_nextButton; } void ContextBrowserPlugin::unload() { core()->uiController()->removeToolView(m_viewFactory); } KDevelop::ContextMenuExtension ContextBrowserPlugin::contextMenuExtension(KDevelop::Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker lock(DUChain::lock()); if(!codeContext->declaration().data()) return menuExt; qRegisterMetaType("KDevelop::IndexedDeclaration"); menuExt.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, m_findUses); return menuExt; } void ContextBrowserPlugin::showUses(const DeclarationPointer& declaration) { QMetaObject::invokeMethod(this, "showUsesDelayed", Qt::QueuedConnection, Q_ARG(KDevelop::DeclarationPointer, declaration)); } void ContextBrowserPlugin::showUsesDelayed(const DeclarationPointer& declaration) { DUChainReadLocker lock; Declaration* decl = declaration.data(); if(!decl) { return; } QWidget* toolView = ICore::self()->uiController()->findToolView(i18n("Code Browser"), m_viewFactory, KDevelop::IUiController::CreateAndRaise); if(!toolView) { return; } ContextBrowserView* view = dynamic_cast(toolView); Q_ASSERT(view); view->allowLockedUpdate(); view->setDeclaration(decl, decl->topContext(), true); //We may get deleted while the call to acceptLink, so make sure we don't crash in that case QPointer widget = dynamic_cast(view->navigationWidget()); if(widget && widget->context()) { NavigationContextPointer nextContext = widget->context()->execute( NavigationAction(declaration, KDevelop::NavigationAction::ShowUses)); if(widget) { widget->setContext( nextContext ); } } } void ContextBrowserPlugin::findUses() { showUses(cursorDeclaration()); } ContextBrowserHintProvider::ContextBrowserHintProvider(ContextBrowserPlugin* plugin) : m_plugin(plugin) { } QString ContextBrowserHintProvider::textHint(View* view, const KTextEditor::Cursor& cursor) { m_plugin->m_mouseHoverCursor = KTextEditor::Cursor(cursor); if(!view) { qWarning() << "could not cast to view"; }else{ m_plugin->m_mouseHoverDocument = view->document()->url(); m_plugin->m_updateViews << view; } m_plugin->m_updateTimer->start(1); // triggers updateViews() m_plugin->showToolTip(view, cursor); return QString(); } void ContextBrowserPlugin::stopDelayedBrowsing() { hideToolTip(); } void ContextBrowserPlugin::invokeAction(int index) { if (!m_currentNavigationWidget) return; auto navigationWidget = qobject_cast(m_currentNavigationWidget); if (!navigationWidget) return; // TODO: Add API in AbstractNavigation{Widget,Context}? QMetaObject::invokeMethod(navigationWidget->context().data(), "executeAction", Q_ARG(int, index)); } void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if(!m_currentToolTip) { showToolTip(view, view->cursorPosition()); } } void ContextBrowserPlugin::hideToolTip() { if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; m_currentToolTipProblems.clear(); m_currentToolTipDeclaration = {}; } } static QVector findProblemsUnderCursor(TopDUContext* topContext, KTextEditor::Cursor position) { QVector problems; auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); foreach (auto modelData, modelsData) { foreach (auto problem, modelData.model->problems(topContext->url())) { DocumentRange problemRange = problem->finalLocation(); if (problemRange.contains(position) || (problemRange.isEmpty() && problemRange.boundaryAtCursor(position))) problems += problem; } } return problems; } static QVector findProblemsCloseToCursor(TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::View* view) { QVector allProblems; auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); foreach (auto modelData, modelsData) { foreach (auto problem, modelData.model->problems(topContext->url())) { allProblems += problem; } } if (allProblems.isEmpty()) return allProblems; std::sort(allProblems.begin(), allProblems.end(), [position](const KDevelop::IProblem::Ptr a, const KDevelop::IProblem::Ptr b) { const auto aRange = a->finalLocation(); const auto bRange = b->finalLocation(); const auto aLineDistance = qMin(qAbs(aRange.start().line() - position.line()), qAbs(aRange.end().line() - position.line())); const auto bLineDistance = qMin(qAbs(bRange.start().line() - position.line()), qAbs(bRange.end().line() - position.line())); if (aLineDistance != bLineDistance) { return aLineDistance < bLineDistance; } if (aRange.start().line() == bRange.start().line()) { return qAbs(aRange.start().column() - position.column()) < qAbs(bRange.start().column() - position.column()); } return qAbs(aRange.end().column() - position.column()) < qAbs(bRange.end().column() - position.column()); }); QVector closestProblems; // Show problems, located on the same line foreach (auto problem, allProblems) { auto r = problem->finalLocation(); if (r.onSingleLine() && r.start().line() == position.line()) closestProblems += problem; else break; } // If not, only show it in case there's only whitespace // between the current cursor position and the problem line if (closestProblems.isEmpty()) { foreach (auto problem, allProblems) { auto r = problem->finalLocation(); KTextEditor::Range dist; KTextEditor::Cursor bound(r.start().line(), 0); if (position < r.start()) dist = KTextEditor::Range(position, bound); else { bound.setLine(r.end().line() + 1); dist = KTextEditor::Range(bound, position); } if (view->document()->text(dist).trimmed().isEmpty()) closestProblems += problem; else break; } } return closestProblems; } QWidget* ContextBrowserPlugin::navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position) { QUrl viewUrl = view->document()->url(); auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); DUChainReadLocker lock(DUChain::lock()); foreach (const auto language, languages) { auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, KTextEditor::Cursor(position)); auto navigationWidget = qobject_cast(widget); if(navigationWidget) return navigationWidget; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (topContext) { // first pass: find problems under the cursor const auto problems = findProblemsUnderCursor(topContext, position); if (!problems.isEmpty()) { if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; auto context = new ProblemNavigationContext(problems); context->setTopContext(TopDUContextPointer(topContext)); widget->setContext(NavigationContextPointer(context)); return widget; } } auto declUnderCursor = DUChainUtils::itemUnderCursor(viewUrl, position).declaration; Declaration* decl = DUChainUtils::declarationForDefinition(declUnderCursor); if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { if(m_currentToolTipDeclaration == IndexedDeclaration(decl) && m_currentToolTip) return nullptr; m_currentToolTipDeclaration = IndexedDeclaration(decl); return decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); } if (topContext) { // second pass: find closest problem to the cursor const auto problems = findProblemsCloseToCursor(topContext, position, view); if (!problems.isEmpty()) { if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; // since the problem is not under cursor: show location widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problems, ProblemNavigationContext::ShowLocation))); return widget; } } return nullptr; } void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { ContextBrowserView* contextView = browserViewForWidget(view); if(contextView && contextView->isVisible() && !contextView->isLocked()) return; // If the context-browser view is visible, it will care about updating by itself auto navigationWidget = navigationWidgetForPosition(view, position); if(navigationWidget) { // If we have an invisible context-view, assign the tooltip navigation-widget to it. // If the user makes the context-view visible, it will instantly contain the correct widget. if(contextView && !contextView->isLocked()) contextView->setNavigationWidget(navigationWidget); if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; } KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, view->mapToGlobal(view->cursorToCoordinate(position)) + QPoint(20, 40), navigationWidget); KTextEditor::Range itemRange; { DUChainReadLocker lock; auto viewUrl = view->document()->url(); itemRange = DUChainUtils::itemUnderCursor(viewUrl, position).range; } tooltip->setHandleRect(KTextEditorHelpers::getItemBoundingRect(view, itemRange)); tooltip->resize( navigationWidget->sizeHint() + QSize(10, 10) ); QObject::connect( view, &KTextEditor::View::verticalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip ); QObject::connect( view, &KTextEditor::View::horizontalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip ); qCDebug(PLUGIN_CONTEXTBROWSER) << "tooltip size" << tooltip->size(); m_currentToolTip = tooltip; m_currentNavigationWidget = navigationWidget; ActiveToolTip::showToolTip(tooltip); if ( ! navigationWidget->property("DoNotCloseOnCursorMove").toBool() ) { connect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip, Qt::UniqueConnection); } else { disconnect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip); } }else{ qCDebug(PLUGIN_CONTEXTBROWSER) << "not showing tooltip, no navigation-widget"; } } void ContextBrowserPlugin::clearMouseHover() { m_mouseHoverCursor = KTextEditor::Cursor::invalid(); m_mouseHoverDocument.clear(); } Attribute::Ptr ContextBrowserPlugin::highlightedUseAttribute(KTextEditor::View* view) const { if( !m_highlightAttribute ) { m_highlightAttribute = Attribute::Ptr( new Attribute() ); m_highlightAttribute->setDefaultStyle(KTextEditor::dsNormal); m_highlightAttribute->setForeground(m_highlightAttribute->selectedForeground()); m_highlightAttribute->setBackgroundFillWhitespace(true); auto iface = qobject_cast(view); auto background = iface->configValue(QStringLiteral("search-highlight-color")).value(); m_highlightAttribute->setBackground(background); } return m_highlightAttribute; } void ContextBrowserPlugin::colorSetupChanged() { m_highlightAttribute = Attribute::Ptr(); } Attribute::Ptr ContextBrowserPlugin::highlightedSpecialObjectAttribute(KTextEditor::View* view) const { return highlightedUseAttribute(view); } void ContextBrowserPlugin::addHighlight( View* view, KDevelop::Declaration* decl ) { if( !view || !decl ) { qCDebug(PLUGIN_CONTEXTBROWSER) << "invalid view/declaration"; return; } ViewHighlights& highlights(m_highlightedRanges[view]); KDevelop::DUChainReadLocker lock; // Highlight the declaration highlights.highlights << decl->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); // Highlight uses { QMap< IndexedString, QList< KTextEditor::Range > > currentRevisionUses = decl->usesCurrentRevision(); for(QMap< IndexedString, QList< KTextEditor::Range > >::iterator fileIt = currentRevisionUses.begin(); fileIt != currentRevisionUses.end(); ++fileIt) { for(QList< KTextEditor::Range >::const_iterator useIt = (*fileIt).constBegin(); useIt != (*fileIt).constEnd(); ++useIt) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(*useIt, fileIt.key())); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } } if( FunctionDefinition* def = FunctionDefinition::definition(decl) ) { highlights.highlights << def->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } Declaration* ContextBrowserPlugin::findDeclaration(View* view, const KTextEditor::Cursor& position, bool mouseHighlight) { Q_UNUSED(mouseHighlight); Declaration* foundDeclaration = nullptr; if(m_useDeclaration.data()) { foundDeclaration = m_useDeclaration.data(); }else{ //If we haven't found a special language object, search for a use/declaration and eventually highlight it foundDeclaration = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(view->document()->url(), position).declaration ); if (foundDeclaration && foundDeclaration->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(foundDeclaration); Q_ASSERT(alias); DUChainReadLocker lock; foundDeclaration = alias->aliasedDeclaration().declaration(); } } return foundDeclaration; } ContextBrowserView* ContextBrowserPlugin::browserViewForWidget(QWidget* widget) { foreach(ContextBrowserView* contextView, m_views) { if(masterWidget(contextView) == masterWidget(widget)) { return contextView; } } return nullptr; } void ContextBrowserPlugin::updateForView(View* view) { bool allowHighlight = true; if(view->selection()) { // If something is selected, we unhighlight everything, so that we don't conflict with the // kate plugin that highlights occurrences of the selected string, and also to reduce the // overall amount of concurrent highlighting. allowHighlight = false; } if(m_highlightedRanges[view].keep) { m_highlightedRanges[view].keep = false; return; } // Clear all highlighting m_highlightedRanges.clear(); // Re-highlight ViewHighlights& highlights = m_highlightedRanges[view]; QUrl url = view->document()->url(); IDocument* activeDoc = core()->documentController()->activeDocument(); bool mouseHighlight = (url == m_mouseHoverDocument) && (m_mouseHoverCursor.isValid()); bool shouldUpdateBrowser = (mouseHighlight || (view == ICore::self()->documentController()->activeTextDocumentView() && activeDoc && activeDoc->textDocument() == view->document())); KTextEditor::Cursor highlightPosition; if (mouseHighlight) highlightPosition = m_mouseHoverCursor; else highlightPosition = KTextEditor::Cursor(view->cursorPosition()); ///Pick a language ILanguageSupport* language = nullptr; if(ICore::self()->languageController()->languagesForUrl(url).isEmpty()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "found no language for document" << url; return; }else{ language = ICore::self()->languageController()->languagesForUrl(url).front(); } ///Check whether there is a special language object to highlight (for example a macro) KTextEditor::Range specialRange = language->specialLanguageObjectRange(url, highlightPosition); ContextBrowserView* updateBrowserView = shouldUpdateBrowser ? browserViewForWidget(view) : nullptr; if(specialRange.isValid()) { // Highlight a special language object if(allowHighlight) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(specialRange, IndexedString(url))); highlights.highlights.back()->setAttribute(highlightedSpecialObjectAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } if(updateBrowserView) updateBrowserView->setSpecialNavigationWidget(language->specialLanguageObjectNavigationWidget(url, highlightPosition)); }else{ KDevelop::DUChainReadLocker lock( DUChain::lock(), 100 ); if(!lock.locked()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "Failed to lock du-chain in time"; return; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (!topContext) return; DUContext* ctx = contextForHighlightingAt(highlightPosition, topContext); if (!ctx) return; //Only update the history if this context is around the text cursor if(core()->documentController()->activeDocument() && highlightPosition == KTextEditor::Cursor(view->cursorPosition()) && view->document() == core()->documentController()->activeDocument()->textDocument()) { updateHistory(ctx, highlightPosition); } Declaration* foundDeclaration = findDeclaration(view, highlightPosition, mouseHighlight); if( foundDeclaration ) { m_lastHighlightedDeclaration = highlights.declaration = IndexedDeclaration(foundDeclaration); if(allowHighlight) addHighlight( view, foundDeclaration ); if(updateBrowserView) updateBrowserView->setDeclaration(foundDeclaration, topContext); }else{ if(updateBrowserView) updateBrowserView->setContext(ctx); } } } void ContextBrowserPlugin::updateViews() { foreach( View* view, m_updateViews ) { updateForView(view); } m_updateViews.clear(); m_useDeclaration = IndexedDeclaration(); } void ContextBrowserPlugin::declarationSelectedInUI(const DeclarationPointer& decl) { m_useDeclaration = IndexedDeclaration(decl.data()); KTextEditor::View* view = core()->documentController()->activeTextDocumentView(); if(view) m_updateViews << view; if(!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); // triggers updateViews() } void ContextBrowserPlugin::updateReady(const IndexedString& file, const ReferencedTopDUContext& /*topContext*/) { const auto url = file.toUrl(); for(QMap< View*, ViewHighlights >::iterator it = m_highlightedRanges.begin(); it != m_highlightedRanges.end(); ++it) { if(it.key()->document()->url() == url) { if(!m_updateViews.contains(it.key())) { qCDebug(PLUGIN_CONTEXTBROWSER) << "adding view for update"; m_updateViews << it.key(); // Don't change the highlighted declaration after finished parse-jobs (*it).keep = true; } } } if(!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); } void ContextBrowserPlugin::textDocumentCreated( KDevelop::IDocument* document ) { Q_ASSERT(document->textDocument()); connect( document->textDocument(), &KTextEditor::Document::viewCreated, this, &ContextBrowserPlugin::viewCreated ); foreach( View* view, document->textDocument()->views() ) viewCreated( document->textDocument(), view ); } void ContextBrowserPlugin::documentActivated( IDocument* doc ) { if (m_outlineLine) m_outlineLine->clear(); if (View* view = doc->activeTextView()) { cursorPositionChanged(view, view->cursorPosition()); } } void ContextBrowserPlugin::viewDestroyed( QObject* obj ) { m_highlightedRanges.remove(static_cast(obj)); m_updateViews.remove(static_cast(obj)); } void ContextBrowserPlugin::selectionChanged( View* view ) { clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout/2); // triggers updateViews() } void ContextBrowserPlugin::cursorPositionChanged( View* view, const KTextEditor::Cursor& newPosition ) { if(view->document() == m_lastInsertionDocument && newPosition == m_lastInsertionPos) { //Do not update the highlighting while typing m_lastInsertionDocument = nullptr; m_lastInsertionPos = KTextEditor::Cursor(); if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = true; }else{ if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = false; } clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout/2); // triggers updateViews() } void ContextBrowserPlugin::textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text) { m_lastInsertionDocument = doc; m_lastInsertionPos = cursor + KTextEditor::Cursor(0, text.size()); } void ContextBrowserPlugin::viewCreated( KTextEditor::Document* , View* v ) { disconnect( v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged ); ///Just to make sure that multiple connections don't happen connect( v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged ); connect( v, &View::destroyed, this, &ContextBrowserPlugin::viewDestroyed ); disconnect( v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); connect(v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); disconnect(v, &View::selectionChanged, this, &ContextBrowserPlugin::selectionChanged); KTextEditor::TextHintInterface *iface = dynamic_cast(v); if( !iface ) return; iface->setTextHintDelay(highlightingTimeout); iface->registerTextHintProvider(&m_textHintProvider); } void ContextBrowserPlugin::registerToolView(ContextBrowserView* view) { m_views << view; } void ContextBrowserPlugin::previousUseShortcut() { switchUse(false); } void ContextBrowserPlugin::nextUseShortcut() { switchUse(true); } KTextEditor::Range cursorToRange(KTextEditor::Cursor cursor) { return KTextEditor::Range(cursor, cursor); } void ContextBrowserPlugin::switchUse(bool forward) { View* view = core()->documentController()->activeTextDocumentView(); if(view) { KTextEditor::Document* doc = view->document(); KDevelop::DUChainReadLocker lock( DUChain::lock() ); KDevelop::TopDUContext* chosen = DUChainUtils::standardContextForUrl(doc->url()); if( chosen ) { KTextEditor::Cursor cCurrent(view->cursorPosition()); KDevelop::CursorInRevision c = chosen->transformToLocalRevision(cCurrent); Declaration* decl = nullptr; //If we have a locked declaration, use that for jumping foreach(ContextBrowserView* view, m_views) { decl = view->lockedDeclaration().data(); ///@todo Somehow match the correct context-browser view if there is multiple if(decl) break; } if(!decl) //Try finding a declaration under the cursor decl = DUChainUtils::itemUnderCursor(doc->url(), cCurrent).declaration; if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { Declaration* target = nullptr; if(forward) //Try jumping from definition to declaration target = DUChainUtils::declarationForDefinition(decl, chosen); else if(decl->url().toUrl() == doc->url() && decl->range().contains(c)) //Try jumping from declaration to definition target = FunctionDefinition::definition(decl); if(target && target != decl) { KTextEditor::Cursor jumpTo = target->rangeInCurrentRevision().start(); QUrl document = target->url().toUrl(); lock.unlock(); core()->documentController()->openDocument( document, cursorToRange(jumpTo) ); return; }else{ //Always work with the declaration instead of the definition decl = DUChainUtils::declarationForDefinition(decl, chosen); } } if(!decl) { //Pick the last use we have highlighted decl = m_lastHighlightedDeclaration.data(); } if(decl) { KDevVarLengthArray usingFiles = DUChain::uses()->uses(decl->id()); if(DUChainUtils::contextHasUse(decl->topContext(), decl) && usingFiles.indexOf(decl->topContext()) == -1) usingFiles.insert(0, decl->topContext()); if(decl->range().contains(c) && decl->url() == chosen->url()) { //The cursor is directly on the declaration. Jump to the first or last use. if(!usingFiles.isEmpty()) { TopDUContext* top = (forward ? usingFiles[0] : usingFiles.back()).data(); if(top) { QList useRanges = allUses(top, decl, true); std::sort(useRanges.begin(), useRanges.end()); if(!useRanges.isEmpty()) { QUrl url = top->url().toUrl(); KTextEditor::Range selectUse = chosen->transformFromLocalRevision(forward ? useRanges.first() : useRanges.back()); lock.unlock(); core()->documentController()->openDocument(url, cursorToRange(selectUse.start())); } } } return; } //Check whether we are within a use QList localUses = allUses(chosen, decl, true); std::sort(localUses.begin(), localUses.end()); for(int a = 0; a < localUses.size(); ++a) { int nextUse = (forward ? a+1 : a-1); bool pick = localUses[a].contains(c); if(!pick && forward && a+1 < localUses.size() && localUses[a].end <= c && localUses[a+1].start > c) { //Special case: We aren't on a use, but we are jumping forward, and are behind this and the next use pick = true; } if(!pick && !forward && a-1 >= 0 && c < localUses[a].start && c >= localUses[a-1].end) { //Special case: We aren't on a use, but we are jumping backward, and are in front of this use, but behind the previous one pick = true; } if(!pick && a == 0 && c < localUses[a].start) { if(!forward) { //Will automatically jump to previous file }else{ nextUse = 0; //We are before the first use, so jump to it. } pick = true; } if(!pick && a == localUses.size()-1 && c >= localUses[a].end) { if(forward) { //Will automatically jump to next file }else{ //We are behind the last use, but moving backward. So pick the last use. nextUse = a; } pick = true; } if(pick) { //Make sure we end up behind the use if(nextUse != a) while(forward && nextUse < localUses.size() && (localUses[nextUse].start <= localUses[a].end || localUses[nextUse].isEmpty())) ++nextUse; //Make sure we end up before the use if(nextUse != a) while(!forward && nextUse >= 0 && (localUses[nextUse].start >= localUses[a].start || localUses[nextUse].isEmpty())) --nextUse; //Jump to the next use qCDebug(PLUGIN_CONTEXTBROWSER) << "count of uses:" << localUses.size() << "nextUse" << nextUse; if(nextUse < 0 || nextUse == localUses.size()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "jumping to next file"; //Jump to the first use in the next using top-context int indexInFiles = usingFiles.indexOf(chosen); if(indexInFiles != -1) { int nextFile = (forward ? indexInFiles+1 : indexInFiles-1); qCDebug(PLUGIN_CONTEXTBROWSER) << "current file" << indexInFiles << "nextFile" << nextFile; if(nextFile < 0 || nextFile >= usingFiles.size()) { //Open the declaration, or the definition if(nextFile >= usingFiles.size()) { Declaration* definition = FunctionDefinition::definition(decl); if(definition) decl = definition; } QUrl u = decl->url().toUrl(); KTextEditor::Range range = decl->rangeInCurrentRevision(); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); return; }else{ TopDUContext* nextTop = usingFiles[nextFile].data(); QUrl u = nextTop->url().toUrl(); QList nextTopUses = allUses(nextTop, decl, true); std::sort(nextTopUses.begin(), nextTopUses.end()); if(!nextTopUses.isEmpty()) { KTextEditor::Range range = chosen->transformFromLocalRevision(forward ? nextTopUses.front() : nextTopUses.back()); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); } return; } }else{ qCDebug(PLUGIN_CONTEXTBROWSER) << "not found own file in use list"; } }else{ QUrl url = chosen->url().toUrl(); KTextEditor::Range range = chosen->transformFromLocalRevision(localUses[nextUse]); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(url, range); return; } } } } } } } void ContextBrowserPlugin::unRegisterToolView(ContextBrowserView* view) { m_views.removeAll(view); } // history browsing QWidget* ContextBrowserPlugin::toolbarWidgetForMainWindow( Sublime::MainWindow* window ) { //TODO: support multiple windows (if that ever gets revived) if (!m_toolbarWidget) { m_toolbarWidget = new QWidget(window); } return m_toolbarWidget; } void ContextBrowserPlugin::documentJumpPerformed( KDevelop::IDocument* newDocument, const KTextEditor::Cursor& newCursor, KDevelop::IDocument* previousDocument, const KTextEditor::Cursor& previousCursor) { DUChainReadLocker lock(DUChain::lock()); /*TODO: support multiple windows if that ever gets revived if(newDocument && newDocument->textDocument() && newDocument->textDocument()->activeView() && masterWidget(newDocument->textDocument()->activeView()) != masterWidget(this)) return; */ if(previousDocument && previousCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump source"; DUContext* context = getContextAt(previousDocument->url(), previousCursor); if(context) { updateHistory(context, KTextEditor::Cursor(previousCursor), true); }else{ //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(previousDocument->url()), KTextEditor::Cursor(previousCursor)))); ++m_nextHistoryIndex; } } qCDebug(PLUGIN_CONTEXTBROWSER) << "new doc: " << newDocument << " new cursor: " << newCursor; if(newDocument && newCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump target"; DUContext* context = getContextAt(newDocument->url(), newCursor); if(context) { updateHistory(context, KTextEditor::Cursor(newCursor), true); }else{ //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(newDocument->url()), KTextEditor::Cursor(newCursor)))); ++m_nextHistoryIndex; if (m_outlineLine) m_outlineLine->clear(); } } } void ContextBrowserPlugin::updateButtonState() { m_nextButton->setEnabled( m_nextHistoryIndex < m_history.size() ); m_previousButton->setEnabled( m_nextHistoryIndex >= 2 ); } void ContextBrowserPlugin::historyNext() { if(m_nextHistoryIndex >= m_history.size()) { return; } openDocument(m_nextHistoryIndex); // opening the document at given position // will update the widget for us ++m_nextHistoryIndex; updateButtonState(); } void ContextBrowserPlugin::openDocument(int historyIndex) { Q_ASSERT_X(historyIndex >= 0, "openDocument", "negative history index"); Q_ASSERT_X(historyIndex < m_history.size(), "openDocument", "history index out of range"); DocumentCursor c = m_history[historyIndex].computePosition(); if (c.isValid() && !c.document.str().isEmpty()) { disconnect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); ICore::self()->documentController()->openDocument(c.document.toUrl(), c); connect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); updateDeclarationListBox(m_history[historyIndex].context.data()); } } void ContextBrowserPlugin::historyPrevious() { if(m_nextHistoryIndex < 2) { return; } --m_nextHistoryIndex; openDocument(m_nextHistoryIndex-1); // opening the document at given position // will update the widget for us updateButtonState(); } QString ContextBrowserPlugin::actionTextFor(int historyIndex) const { const HistoryEntry& entry = m_history.at(historyIndex); QString actionText = entry.context.data() ? entry.context.data()->scopeIdentifier(true).toString() : QString(); if(actionText.isEmpty()) actionText = entry.alternativeString; if(actionText.isEmpty()) actionText = QStringLiteral(""); actionText += QLatin1String(" @ "); QString fileName = entry.absoluteCursorPosition.document.toUrl().fileName(); actionText += QStringLiteral("%1:%2").arg(fileName).arg(entry.absoluteCursorPosition.line()+1); return actionText; } /* inline QDebug operator<<(QDebug debug, const ContextBrowserPlugin::HistoryEntry &he) { DocumentCursor c = he.computePosition(); debug << "\n\tHistoryEntry " << c.line << " " << c.document.str(); return debug; } */ void ContextBrowserPlugin::nextMenuAboutToShow() { QList indices; for(int a = m_nextHistoryIndex; a < m_history.size(); ++a) { indices << a; } fillHistoryPopup(m_nextMenu, indices); } void ContextBrowserPlugin::previousMenuAboutToShow() { QList indices; for(int a = m_nextHistoryIndex-2; a >= 0; --a) { indices << a; } fillHistoryPopup(m_previousMenu, indices); } void ContextBrowserPlugin::fillHistoryPopup(QMenu* menu, const QList& historyIndices) { menu->clear(); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); foreach(int index, historyIndices) { QAction* action = new QAction(actionTextFor(index), menu); action->setData(index); menu->addAction(action); connect(action, &QAction::triggered, this, &ContextBrowserPlugin::actionTriggered); } } bool ContextBrowserPlugin::isPreviousEntry(KDevelop::DUContext* context, const KTextEditor::Cursor& /*position*/) const { if (m_nextHistoryIndex == 0) return false; Q_ASSERT(m_nextHistoryIndex <= m_history.count()); const HistoryEntry& he = m_history.at(m_nextHistoryIndex-1); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); // is this necessary?? Q_ASSERT(context); return IndexedDUContext(context) == he.context; } void ContextBrowserPlugin::updateHistory(KDevelop::DUContext* context, const KTextEditor::Cursor& position, bool force) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating history"; if(m_outlineLine && m_outlineLine->isVisible()) updateDeclarationListBox(context); if(!context || (!context->owner() && !force)) { return; //Only add history-entries for contexts that have owners, which in practice should be functions and classes //This keeps the history cleaner } if (isPreviousEntry(context, position)) { if(m_nextHistoryIndex) { HistoryEntry& he = m_history[m_nextHistoryIndex-1]; he.setCursorPosition(position); } return; } else { // Append new history entry m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(IndexedDUContext(context), position)); ++m_nextHistoryIndex; updateButtonState(); if(m_history.size() > (maxHistoryLength + 5)) { m_history = m_history.mid(m_history.size() - maxHistoryLength); m_nextHistoryIndex = m_history.size(); } } } void ContextBrowserPlugin::updateDeclarationListBox(DUContext* context) { if(!context || !context->owner()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "not updating box"; m_listUrl = IndexedString(); ///@todo Compute the context in the document here if (m_outlineLine) m_outlineLine->clear(); return; } Declaration* decl = context->owner(); m_listUrl = context->url(); Declaration* specialDecl = SpecializationStore::self().applySpecialization(decl, decl->topContext()); FunctionType::Ptr function = specialDecl->type(); QString text = specialDecl->qualifiedIdentifier().toString(); if(function) text += function->partToString(KDevelop::FunctionType::SignatureArguments); if(m_outlineLine && !m_outlineLine->hasFocus()) { m_outlineLine->setText(text); m_outlineLine->setCursorPosition(0); } qCDebug(PLUGIN_CONTEXTBROWSER) << "updated" << text; } void ContextBrowserPlugin::actionTriggered() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); Q_ASSERT(action->data().type() == QVariant::Int); int historyPosition = action->data().toInt(); // qCDebug(PLUGIN_CONTEXTBROWSER) << "history pos" << historyPosition << m_history.size() << m_history; if(historyPosition >= 0 && historyPosition < m_history.size()) { m_nextHistoryIndex = historyPosition + 1; openDocument(historyPosition); updateButtonState(); } } void ContextBrowserPlugin::doNavigate(NavigationActionType action) { KTextEditor::View* view = qobject_cast(sender()); if(!view) { qWarning() << "sender is not a view"; return; } KTextEditor::CodeCompletionInterface* iface = qobject_cast(view); if(!iface || iface->isCompletionActive()) return; // If code completion is active, the actions should be handled by the completion widget QWidget* widget = m_currentNavigationWidget.data(); if(!widget || !widget->isVisible()) { ContextBrowserView* contextView = browserViewForWidget(view); if(contextView) widget = contextView->navigationWidget(); } if(widget) { AbstractNavigationWidget* navWidget = qobject_cast(widget); if (navWidget) { switch(action) { case Accept: navWidget->accept(); break; case Back: navWidget->back(); break; case Left: navWidget->previous(); break; case Right: navWidget->next(); break; case Up: navWidget->up(); break; case Down: navWidget->down(); break; } } } } void ContextBrowserPlugin::navigateAccept() { doNavigate(Accept); } void ContextBrowserPlugin::navigateBack() { doNavigate(Back); } void ContextBrowserPlugin::navigateDown() { doNavigate(Down); } void ContextBrowserPlugin::navigateLeft() { doNavigate(Left); } void ContextBrowserPlugin::navigateRight() { doNavigate(Right); } void ContextBrowserPlugin::navigateUp() { doNavigate(Up); } //BEGIN HistoryEntry ContextBrowserPlugin::HistoryEntry::HistoryEntry(KDevelop::DocumentCursor pos) : absoluteCursorPosition(pos) { } ContextBrowserPlugin::HistoryEntry::HistoryEntry(IndexedDUContext ctx, const KTextEditor::Cursor& cursorPosition) : context(ctx) { //Use a position relative to the context setCursorPosition(cursorPosition); if(ctx.data()) alternativeString = ctx.data()->scopeIdentifier(true).toString(); if(!alternativeString.isEmpty()) alternativeString += i18n("(changed)"); //This is used when the context was deleted in between } DocumentCursor ContextBrowserPlugin::HistoryEntry::computePosition() const { KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); DocumentCursor ret; if(context.data()) { ret = DocumentCursor(context.data()->url(), relativeCursorPosition); ret.setLine(ret.line() + context.data()->range().start.line); }else{ ret = absoluteCursorPosition; } return ret; } void ContextBrowserPlugin::HistoryEntry::setCursorPosition(const KTextEditor::Cursor& cursorPosition) { KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); if(context.data()) { absoluteCursorPosition = DocumentCursor(context.data()->url(), cursorPosition); relativeCursorPosition = cursorPosition; relativeCursorPosition.setLine(relativeCursorPosition.line() - context.data()->range().start.line); } } // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on #include "contextbrowser.moc" diff --git a/plugins/cvs/cvsplugin.cpp b/plugins/cvs/cvsplugin.cpp index 976aea0916..988ebc0908 100644 --- a/plugins/cvs/cvsplugin.cpp +++ b/plugins/cvs/cvsplugin.cpp @@ -1,489 +1,486 @@ /*************************************************************************** * Copyright 2007 Robert Gruber * * * * 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 "cvsplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cvsmainview.h" #include "cvsproxy.h" #include "cvsjob.h" #include "editorsview.h" #include "commitdialog.h" #include "cvsgenericoutputview.h" #include "checkoutdialog.h" #include "importdialog.h" #include "importmetadatawidget.h" #include "debug.h" #include #include #include Q_LOGGING_CATEGORY(PLUGIN_CVS, "kdevplatform.plugins.cvs") K_PLUGIN_FACTORY(KDevCvsFactory, registerPlugin();) // K_EXPORT_PLUGIN(KDevCvsFactory(KAboutData("kdevcvs", "kdevcvs", ki18n("CVS"), "0.1", ki18n("Support for CVS version control system"), KAboutData::License_GPL))) class KDevCvsViewFactory: public KDevelop::IToolViewFactory { public: KDevCvsViewFactory(CvsPlugin *plugin): m_plugin(plugin) {} QWidget* create(QWidget *parent = nullptr) override { return new CvsMainView(m_plugin, parent); } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.CVSView"); } private: CvsPlugin *m_plugin; }; class CvsPluginPrivate { public: explicit CvsPluginPrivate(CvsPlugin *pThis) : m_factory(new KDevCvsViewFactory(pThis)) , m_proxy(new CvsProxy(pThis)) , m_common(new KDevelop::VcsPluginHelper(pThis, pThis)) {} KDevCvsViewFactory* m_factory; QPointer m_proxy; QScopedPointer m_common; }; CvsPlugin::CvsPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevcvs"), parent) , d(new CvsPluginPrivate(this)) { - KDEV_USE_EXTENSION_INTERFACE(KDevelop::IBasicVersionControl) - KDEV_USE_EXTENSION_INTERFACE(KDevelop::ICentralizedVersionControl) - core()->uiController()->addToolView(i18n("CVS"), d->m_factory); setXMLFile(QStringLiteral("kdevcvs.rc")); setupActions(); } CvsPlugin::~CvsPlugin() { } void CvsPlugin::unload() { core()->uiController()->removeToolView( d->m_factory ); } CvsProxy* CvsPlugin::proxy() { return d->m_proxy; } void CvsPlugin::setupActions() { QAction *action; action = actionCollection()->addAction(QStringLiteral("cvs_import")); action->setText(i18n("Import Directory...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotImport); action = actionCollection()->addAction(QStringLiteral("cvs_checkout")); action->setText(i18n("Checkout...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotCheckout); action = actionCollection()->addAction(QStringLiteral("cvs_status")); action->setText(i18n("Status...")); connect(action, &QAction::triggered, this, &CvsPlugin::slotStatus); } const QUrl CvsPlugin::urlFocusedDocument() const { KParts::ReadOnlyPart *plugin = dynamic_cast(core()->partController()->activePart()); if (plugin) { if (plugin->url().isLocalFile()) { return plugin->url(); } } return QUrl(); } void CvsPlugin::slotImport() { QUrl url = urlFocusedDocument(); ImportDialog dlg(this, url); dlg.exec(); } void CvsPlugin::slotCheckout() { ///@todo don't use proxy directly; use interface instead CheckoutDialog dlg(this); dlg.exec(); } void CvsPlugin::slotStatus() { QUrl url = urlFocusedDocument(); QList urls; urls << url; KDevelop::VcsJob* j = status(urls, KDevelop::IBasicVersionControl::Recursive); CvsJob* job = dynamic_cast(j); if (job) { CvsGenericOutputView* view = new CvsGenericOutputView(job); emit addNewTabToMainView(view, i18n("Status")); KDevelop::ICore::self()->runController()->registerJob(job); } } KDevelop::ContextMenuExtension CvsPlugin::contextMenuExtension(KDevelop::Context* context) { d->m_common->setupFromContext(context); QList const & ctxUrlList = d->m_common->contextUrlList(); bool hasVersionControlledEntries = false; foreach(const QUrl &url, ctxUrlList) { if (d->m_proxy->isValidDirectory(url)) { hasVersionControlledEntries = true; break; } } qCDebug(PLUGIN_CVS) << "version controlled?" << hasVersionControlledEntries; if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu* menu = d->m_common->commonActions(); menu->addSeparator(); QAction *action; // Just add actions which are not covered by the cvscommon plugin action = new QAction(i18n("Edit"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxEdit); menu->addAction(action); action = new QAction(i18n("Unedit"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxUnEdit); menu->addAction(action); action = new QAction(i18n("Show Editors"), this); connect(action, &QAction::triggered, this, &CvsPlugin::ctxEditors); menu->addAction(action); KDevelop::ContextMenuExtension menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::VcsGroup, menu->menuAction()); return menuExt; } void CvsPlugin::ctxEdit() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); KDevelop::VcsJob* j = edit(urls.front()); CvsJob* job = dynamic_cast(j); if (job) { connect(job, &CvsJob::result, this, &CvsPlugin::jobFinished); KDevelop::ICore::self()->runController()->registerJob(job); } } void CvsPlugin::ctxUnEdit() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); KDevelop::VcsJob* j = unedit(urls.front()); CvsJob* job = dynamic_cast(j); if (job) { connect(job, &CvsJob::result, this, &CvsPlugin::jobFinished); KDevelop::ICore::self()->runController()->registerJob(job); } } void CvsPlugin::ctxEditors() { QList const & urls = d->m_common->contextUrlList(); Q_ASSERT(!urls.empty()); CvsJob* job = d->m_proxy->editors(findWorkingDir(urls.front()), urls); if (job) { KDevelop::ICore::self()->runController()->registerJob(job); EditorsView* view = new EditorsView(job); emit addNewTabToMainView(view, i18n("Editors")); } } QString CvsPlugin::findWorkingDir(const QUrl& location) { QFileInfo fileInfo(location.toLocalFile()); // find out correct working directory if (fileInfo.isFile()) { return fileInfo.absolutePath(); } else { return fileInfo.absoluteFilePath(); } } // Begin: KDevelop::IBasicVersionControl bool CvsPlugin::isVersionControlled(const QUrl & localLocation) { return d->m_proxy->isVersionControlled(localLocation); } KDevelop::VcsJob * CvsPlugin::repositoryLocation(const QUrl & localLocation) { Q_UNUSED(localLocation); return nullptr; } KDevelop::VcsJob * CvsPlugin::add(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->add(findWorkingDir(localLocations[0]), localLocations, (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false); return job; } KDevelop::VcsJob * CvsPlugin::remove(const QList & localLocations) { CvsJob* job = d->m_proxy->remove(findWorkingDir(localLocations[0]), localLocations); return job; } KDevelop::VcsJob * CvsPlugin::localRevision(const QUrl & localLocation, KDevelop::VcsRevision::RevisionType) { Q_UNUSED(localLocation) return nullptr; } KDevelop::VcsJob * CvsPlugin::status(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->status(findWorkingDir(localLocations[0]), localLocations, (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false); return job; } KDevelop::VcsJob * CvsPlugin::unedit(const QUrl& localLocation) { CvsJob* job = d->m_proxy->unedit(findWorkingDir(localLocation), QList() << localLocation); return job; } KDevelop::VcsJob * CvsPlugin::edit(const QUrl& localLocation) { CvsJob* job = d->m_proxy->edit(findWorkingDir(localLocation), QList() << localLocation); return job; } KDevelop::VcsJob * CvsPlugin::copy(const QUrl & localLocationSrc, const QUrl & localLocationDstn) { bool ok = QFile::copy(localLocationSrc.toLocalFile(), localLocationDstn.path()); if (!ok) { return nullptr; } QList listDstn; listDstn << localLocationDstn; CvsJob* job = d->m_proxy->add(findWorkingDir(localLocationDstn), listDstn, true); return job; } KDevelop::VcsJob * CvsPlugin::move(const QUrl &, const QUrl &) { return nullptr; } KDevelop::VcsJob * CvsPlugin::revert(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { KDevelop::VcsRevision rev; CvsJob* job = d->m_proxy->update(findWorkingDir(localLocations[0]), localLocations, rev, QStringLiteral("-C"), (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false, false, false); return job; } KDevelop::VcsJob * CvsPlugin::update(const QList & localLocations, const KDevelop::VcsRevision & rev, KDevelop::IBasicVersionControl::RecursionMode recursion) { CvsJob* job = d->m_proxy->update(findWorkingDir(localLocations[0]), localLocations, rev, QString(), (recursion == KDevelop::IBasicVersionControl::Recursive) ? true : false, false, false); return job; } KDevelop::VcsJob * CvsPlugin::commit(const QString & message, const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); QString msg = message; if (msg.isEmpty()) { CommitDialog dlg; if (dlg.exec() == QDialog::Accepted) { msg = dlg.message(); } } CvsJob* job = d->m_proxy->commit(findWorkingDir(localLocations[0]), localLocations, msg); return job; } KDevelop::VcsJob * CvsPlugin::diff(const QUrl & fileOrDirectory, const KDevelop::VcsRevision & srcRevision, const KDevelop::VcsRevision & dstRevision, KDevelop::VcsDiff::Type, KDevelop::IBasicVersionControl::RecursionMode) { CvsJob* job = d->m_proxy->diff(fileOrDirectory, srcRevision, dstRevision, QStringLiteral("-uN")/*always unified*/); return job; } KDevelop::VcsJob * CvsPlugin::log(const QUrl & localLocation, const KDevelop::VcsRevision & rev, unsigned long limit) { Q_UNUSED(limit) CvsJob* job = d->m_proxy->log(localLocation, rev); return job; } KDevelop::VcsJob * CvsPlugin::log(const QUrl & localLocation, const KDevelop::VcsRevision & rev, const KDevelop::VcsRevision & limit) { Q_UNUSED(limit) return log(localLocation, rev, 0); } KDevelop::VcsJob * CvsPlugin::annotate(const QUrl & localLocation, const KDevelop::VcsRevision & rev) { CvsJob* job = d->m_proxy->annotate(localLocation, rev); return job; } KDevelop::VcsJob * CvsPlugin::resolve(const QList & localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(localLocations); Q_UNUSED(recursion); return nullptr; } KDevelop::VcsJob * CvsPlugin::import(const QString& commitMessage, const QUrl& sourceDirectory, const KDevelop::VcsLocation& destinationRepository) { if (commitMessage.isEmpty() || !sourceDirectory.isLocalFile() || !destinationRepository.isValid() || destinationRepository.type() != KDevelop::VcsLocation::RepositoryLocation) { return nullptr; } qCDebug(PLUGIN_CVS) << "CVS Import requested " << "src:" << sourceDirectory.toLocalFile() << "srv:" << destinationRepository.repositoryServer() << "module:" << destinationRepository.repositoryModule(); CvsJob* job = d->m_proxy->import(sourceDirectory, destinationRepository.repositoryServer(), destinationRepository.repositoryModule(), destinationRepository.userData().toString(), destinationRepository.repositoryTag(), commitMessage); return job; } KDevelop::VcsJob * CvsPlugin::createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl & destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); if (!destinationDirectory.isLocalFile() || !sourceRepository.isValid() || sourceRepository.type() != KDevelop::VcsLocation::RepositoryLocation) { return nullptr; } qCDebug(PLUGIN_CVS) << "CVS Checkout requested " << "dest:" << destinationDirectory.toLocalFile() << "srv:" << sourceRepository.repositoryServer() << "module:" << sourceRepository.repositoryModule() << "branch:" << sourceRepository.repositoryBranch() << endl; CvsJob* job = d->m_proxy->checkout(destinationDirectory, sourceRepository.repositoryServer(), sourceRepository.repositoryModule(), QString(), sourceRepository.repositoryBranch(), true, true); return job; } QString CvsPlugin::name() const { return i18n("CVS"); } KDevelop::VcsImportMetadataWidget* CvsPlugin::createImportMetadataWidget(QWidget* parent) { return new ImportMetadataWidget(parent); } KDevelop::VcsLocationWidget* CvsPlugin::vcsLocation(QWidget* parent) const { return new KDevelop::StandardVcsLocationWidget(parent); } // End: KDevelop::IBasicVersionControl #include "cvsplugin.moc" diff --git a/plugins/execute/executeplugin.cpp b/plugins/execute/executeplugin.cpp index 2a041a0535..a1a24a8c74 100644 --- a/plugins/execute/executeplugin.cpp +++ b/plugins/execute/executeplugin.cpp @@ -1,252 +1,251 @@ /* * This file is part of KDevelop * * Copyright 2007 Hamish Rodda * * 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 "executeplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "nativeappconfig.h" #include "debug.h" #include #include #include QString ExecutePlugin::_nativeAppConfigTypeId = QStringLiteral("Native Application"); QString ExecutePlugin::workingDirEntry = QStringLiteral("Working Directory"); QString ExecutePlugin::executableEntry = QStringLiteral("Executable"); QString ExecutePlugin::argumentsEntry = QStringLiteral("Arguments"); QString ExecutePlugin::isExecutableEntry = QStringLiteral("isExecutable"); QString ExecutePlugin::dependencyEntry = QStringLiteral("Dependencies"); QString ExecutePlugin::environmentGroupEntry = QStringLiteral("EnvironmentGroup"); QString ExecutePlugin::useTerminalEntry = QStringLiteral("Use External Terminal"); QString ExecutePlugin::terminalEntry = QStringLiteral("External Terminal"); QString ExecutePlugin::userIdToRunEntry = QStringLiteral("User Id to Run"); QString ExecutePlugin::dependencyActionEntry = QStringLiteral("Dependency Action"); QString ExecutePlugin::projectTargetEntry = QStringLiteral("Project Target"); using namespace KDevelop; Q_LOGGING_CATEGORY(PLUGIN_EXECUTE, "kdevplatform.plugins.execute") K_PLUGIN_FACTORY_WITH_JSON(KDevExecuteFactory, "kdevexecute.json", registerPlugin();) ExecutePlugin::ExecutePlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevexecute"), parent) { - KDEV_USE_EXTENSION_INTERFACE( IExecutePlugin ) m_configType = new NativeAppConfigType(); m_configType->addLauncher( new NativeAppLauncher() ); qCDebug(PLUGIN_EXECUTE) << "adding native app launch config"; core()->runController()->addConfigurationType( m_configType ); } ExecutePlugin::~ExecutePlugin() { } void ExecutePlugin::unload() { core()->runController()->removeConfigurationType( m_configType ); delete m_configType; m_configType = nullptr; } QStringList ExecutePlugin::arguments( KDevelop::ILaunchConfiguration* cfg, QString& err_ ) const { if( !cfg ) { return QStringList(); } KShell::Errors err; QStringList args = KShell::splitArgs( cfg->config().readEntry( ExecutePlugin::argumentsEntry, "" ), KShell::TildeExpand | KShell::AbortOnMeta, &err ); if( err != KShell::NoError ) { if( err == KShell::BadQuoting ) { err_ = i18n("There is a quoting error in the arguments for " "the launch configuration '%1'. Aborting start.", cfg->name() ); } else { err_ = i18n("A shell meta character was included in the " "arguments for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } args = QStringList(); qWarning() << "Launch Configuration:" << cfg->name() << "arguments have meta characters"; } return args; } KJob* ExecutePlugin::dependencyJob( KDevelop::ILaunchConfiguration* cfg ) const { QVariantList deps = KDevelop::stringToQVariant( cfg->config().readEntry( dependencyEntry, QString() ) ).toList(); QString depAction = cfg->config().readEntry( dependencyActionEntry, "Nothing" ); if( depAction != QLatin1String("Nothing") && !deps.isEmpty() ) { KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); QList items; foreach( const QVariant& dep, deps ) { KDevelop::ProjectBaseItem* item = model->itemFromIndex( model->pathToIndex( dep.toStringList() ) ); if( item ) { items << item; } else { KMessageBox::error(core()->uiController()->activeMainWindow(), i18n("Couldn't resolve the dependency: %1", dep.toString())); } } KDevelop::BuilderJob* job = new KDevelop::BuilderJob(); if( depAction == QLatin1String("Build") ) { job->addItems( KDevelop::BuilderJob::Build, items ); } else if( depAction == QLatin1String("Install") ) { job->addItems( KDevelop::BuilderJob::Install, items ); } job->updateJobName(); return job; } return nullptr; } QString ExecutePlugin::environmentGroup( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QLatin1String(""); } return cfg->config().readEntry( ExecutePlugin::environmentGroupEntry, "" ); } QUrl ExecutePlugin::executable( KDevelop::ILaunchConfiguration* cfg, QString& err ) const { QUrl executable; if( !cfg ) { return executable; } KConfigGroup grp = cfg->config(); if( grp.readEntry(ExecutePlugin::isExecutableEntry, false ) ) { executable = grp.readEntry( ExecutePlugin::executableEntry, QUrl() ); } else { QStringList prjitem = grp.readEntry( ExecutePlugin::projectTargetEntry, QStringList() ); KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); KDevelop::ProjectBaseItem* item = model->itemFromIndex( model->pathToIndex(prjitem) ); if( item && item->executable() ) { // TODO: Need an option in the gui to choose between installed and builddir url here, currently cmake only supports builddir url executable = item->executable()->builtUrl(); } } if( executable.isEmpty() ) { err = i18n("No valid executable specified"); qWarning() << "Launch Configuration:" << cfg->name() << "no valid executable set"; } else { KShell::Errors err_; if( KShell::splitArgs( executable.toLocalFile(), KShell::TildeExpand | KShell::AbortOnMeta, &err_ ).isEmpty() || err_ != KShell::NoError ) { executable = QUrl(); if( err_ == KShell::BadQuoting ) { err = i18n("There is a quoting error in the executable " "for the launch configuration '%1'. " "Aborting start.", cfg->name() ); } else { err = i18n("A shell meta character was included in the " "executable for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } qWarning() << "Launch Configuration:" << cfg->name() << "executable has meta characters"; } } return executable; } bool ExecutePlugin::useTerminal( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return false; } return cfg->config().readEntry( ExecutePlugin::useTerminalEntry, false ); } QString ExecutePlugin::terminal( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QString(); } return cfg->config().readEntry( ExecutePlugin::terminalEntry, QString() ); } QUrl ExecutePlugin::workingDirectory( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QUrl(); } return cfg->config().readEntry( ExecutePlugin::workingDirEntry, QUrl() ); } QString ExecutePlugin::nativeAppConfigTypeId() const { return _nativeAppConfigTypeId; } #include "executeplugin.moc" diff --git a/plugins/executescript/executescriptplugin.cpp b/plugins/executescript/executescriptplugin.cpp index c97a54c392..65712ddaaa 100644 --- a/plugins/executescript/executescriptplugin.cpp +++ b/plugins/executescript/executescriptplugin.cpp @@ -1,263 +1,262 @@ /* * This file is part of KDevelop * * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * * 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 "executescriptplugin.h" #include #include #include #include #include #include #include #include #include #include #include "scriptappconfig.h" #include "debug.h" #include #include #include QString ExecuteScriptPlugin::_scriptAppConfigTypeId = QStringLiteral("Script Application"); QString ExecuteScriptPlugin::interpreterEntry = QStringLiteral("Interpreter"); QString ExecuteScriptPlugin::workingDirEntry = QStringLiteral("Working Directory"); QString ExecuteScriptPlugin::executableEntry = QStringLiteral("Executable"); QString ExecuteScriptPlugin::executeOnRemoteHostEntry = QStringLiteral("Execute on Remote Host"); QString ExecuteScriptPlugin::runCurrentFileEntry = QStringLiteral("Run current file"); QString ExecuteScriptPlugin::remoteHostEntry = QStringLiteral("Remote Host"); QString ExecuteScriptPlugin::argumentsEntry = QStringLiteral("Arguments"); QString ExecuteScriptPlugin::isExecutableEntry = QStringLiteral("isExecutable"); QString ExecuteScriptPlugin::environmentGroupEntry = QStringLiteral("EnvironmentGroup"); //QString ExecuteScriptPlugin::useTerminalEntry = "Use External Terminal"; QString ExecuteScriptPlugin::userIdToRunEntry = QStringLiteral("User Id to Run"); QString ExecuteScriptPlugin::projectTargetEntry = QStringLiteral("Project Target"); QString ExecuteScriptPlugin::outputFilteringEntry = QStringLiteral("Output Filtering Mode"); using namespace KDevelop; Q_LOGGING_CATEGORY(PLUGIN_EXECUTESCRIPT, "kdevplatform.plugins.executescript") K_PLUGIN_FACTORY_WITH_JSON(KDevExecuteFactory, "kdevexecutescript.json", registerPlugin();) ExecuteScriptPlugin::ExecuteScriptPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevexecutescript"), parent) { - KDEV_USE_EXTENSION_INTERFACE( IExecuteScriptPlugin ) m_configType = new ScriptAppConfigType(); m_configType->addLauncher( new ScriptAppLauncher( this ) ); qCDebug(PLUGIN_EXECUTESCRIPT) << "adding script launch config"; core()->runController()->addConfigurationType( m_configType ); } ExecuteScriptPlugin::~ExecuteScriptPlugin() { } void ExecuteScriptPlugin::unload() { core()->runController()->removeConfigurationType( m_configType ); delete m_configType; m_configType = nullptr; } QUrl ExecuteScriptPlugin::script( KDevelop::ILaunchConfiguration* cfg, QString& err_ ) const { QUrl script; if( !cfg ) { return script; } KConfigGroup grp = cfg->config(); script = grp.readEntry( ExecuteScriptPlugin::executableEntry, QUrl() ); if( !script.isLocalFile() || script.isEmpty() ) { err_ = i18n("No valid executable specified"); qWarning() << "Launch Configuration:" << cfg->name() << "no valid script set"; } else { KShell::Errors err; if( KShell::splitArgs( script.toLocalFile(), KShell::TildeExpand | KShell::AbortOnMeta, &err ).isEmpty() || err != KShell::NoError ) { script = QUrl(); if( err == KShell::BadQuoting ) { err_ = i18n("There is a quoting error in the script " "for the launch configuration '%1'. " "Aborting start.", cfg->name() ); } else { err_ = i18n("A shell meta character was included in the " "script for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } qWarning() << "Launch Configuration:" << cfg->name() << "script has meta characters"; } } return script; } QString ExecuteScriptPlugin::remoteHost(ILaunchConfiguration* cfg, QString& err) const { if (!cfg) return QString(); KConfigGroup grp = cfg->config(); if(grp.readEntry(ExecuteScriptPlugin::executeOnRemoteHostEntry, false)) { QString host = grp.readEntry(ExecuteScriptPlugin::remoteHostEntry, ""); if (host.isEmpty()) { err = i18n("No remote host set for launch configuration '%1'. " "Aborting start.", cfg->name() ); qWarning() << "Launch Configuration:" << cfg->name() << "no remote host set"; } return host; } return QString(); } QStringList ExecuteScriptPlugin::arguments( KDevelop::ILaunchConfiguration* cfg, QString& err_ ) const { if( !cfg ) { return QStringList(); } KShell::Errors err; QStringList args = KShell::splitArgs( cfg->config().readEntry( ExecuteScriptPlugin::argumentsEntry, "" ), KShell::TildeExpand | KShell::AbortOnMeta, &err ); if( err != KShell::NoError ) { if( err == KShell::BadQuoting ) { err_ = i18n("There is a quoting error in the arguments for " "the launch configuration '%1'. Aborting start.", cfg->name() ); } else { err_ = i18n("A shell meta character was included in the " "arguments for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } args = QStringList(); qWarning() << "Launch Configuration:" << cfg->name() << "arguments have meta characters"; } return args; } QString ExecuteScriptPlugin::environmentGroup( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QString(); } return cfg->config().readEntry( ExecuteScriptPlugin::environmentGroupEntry, "" ); } int ExecuteScriptPlugin::outputFilterModeId( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return 0; } return cfg->config().readEntry( ExecuteScriptPlugin::outputFilteringEntry, 0 ); } bool ExecuteScriptPlugin::runCurrentFile(ILaunchConfiguration* cfg) const { if( !cfg ) { return false; } return cfg->config().readEntry( ExecuteScriptPlugin::runCurrentFileEntry, true ); } QString ExecuteScriptPlugin::interpreter( KDevelop::ILaunchConfiguration* cfg, QString& err ) const { QString interpreter; if( !cfg ) { return interpreter; } KConfigGroup grp = cfg->config(); interpreter = grp.readEntry( ExecuteScriptPlugin::interpreterEntry, QString() ); if( interpreter.isEmpty() ) { err = i18n("No valid interpreter specified"); qWarning() << "Launch Configuration:" << cfg->name() << "no valid interpreter set"; } else { KShell::Errors err_; if( KShell::splitArgs( interpreter, KShell::TildeExpand | KShell::AbortOnMeta, &err_ ).isEmpty() || err_ != KShell::NoError ) { interpreter.clear(); if( err_ == KShell::BadQuoting ) { err = i18n("There is a quoting error in the interpreter " "for the launch configuration '%1'. " "Aborting start.", cfg->name() ); } else { err = i18n("A shell meta character was included in the " "interpreter for the launch configuration '%1', " "this is not supported currently. Aborting start.", cfg->name() ); } qWarning() << "Launch Configuration:" << cfg->name() << "interpreter has meta characters"; } } return interpreter; } /* bool ExecuteScriptPlugin::useTerminal( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return false; } return cfg->config().readEntry( ExecuteScriptPlugin::useTerminalEntry, false ); } */ QUrl ExecuteScriptPlugin::workingDirectory( KDevelop::ILaunchConfiguration* cfg ) const { if( !cfg ) { return QUrl(); } return cfg->config().readEntry( ExecuteScriptPlugin::workingDirEntry, QUrl() ); } QString ExecuteScriptPlugin::scriptAppConfigTypeId() const { return _scriptAppConfigTypeId; } #include "executescriptplugin.moc" diff --git a/plugins/filetemplates/filetemplatesplugin.cpp b/plugins/filetemplates/filetemplatesplugin.cpp index 5ea8c88a43..598a236e4c 100644 --- a/plugins/filetemplates/filetemplatesplugin.cpp +++ b/plugins/filetemplates/filetemplatesplugin.cpp @@ -1,289 +1,288 @@ #include "filetemplatesplugin.h" #include "templateclassassistant.h" #include "templatepreviewtoolview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(FileTemplatesFactory, "kdevfiletemplates.json", registerPlugin();) class TemplatePreviewFactory : public KDevelop::IToolViewFactory { public: TemplatePreviewFactory(FileTemplatesPlugin* plugin) : KDevelop::IToolViewFactory() , m_plugin(plugin) { } QWidget* create(QWidget* parent = nullptr) override { return new TemplatePreviewToolView(m_plugin, parent); } QString id() const override { return QStringLiteral("org.kdevelop.TemplateFilePreview"); } Qt::DockWidgetArea defaultPosition() override { return Qt::RightDockWidgetArea; } private: FileTemplatesPlugin* m_plugin; }; FileTemplatesPlugin::FileTemplatesPlugin(QObject* parent, const QVariantList& args) : IPlugin(QStringLiteral("kdevfiletemplates"), parent) , m_model(nullptr) { Q_UNUSED(args); - KDEV_USE_EXTENSION_INTERFACE(ITemplateProvider) setXMLFile(QStringLiteral("kdevfiletemplates.rc")); QAction* action = actionCollection()->addAction(QStringLiteral("new_from_template")); action->setText( i18n( "New From Template" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("code-class") ) ); action->setWhatsThis( i18n( "Allows you to create new source code files, such as classes or unit tests, using templates." ) ); action->setStatusTip( i18n( "Create new files from a template" ) ); connect (action, &QAction::triggered, this, &FileTemplatesPlugin::createFromTemplate); m_toolView = new TemplatePreviewFactory(this); core()->uiController()->addToolView(i18n("Template Preview"), m_toolView); } FileTemplatesPlugin::~FileTemplatesPlugin() { } void FileTemplatesPlugin::unload() { core()->uiController()->removeToolView(m_toolView); } ContextMenuExtension FileTemplatesPlugin::contextMenuExtension (Context* context) { ContextMenuExtension ext; QUrl fileUrl; if (context->type() == Context::ProjectItemContext) { ProjectItemContext* projectContext = dynamic_cast(context); QList items = projectContext->items(); if (items.size() != 1) { return ext; } QUrl url; ProjectBaseItem* item = items.first(); if (item->folder()) { url = item->path().toUrl(); } else if (item->target()) { url = item->parent()->path().toUrl(); } if (url.isValid()) { QAction* action = new QAction(i18n("Create From Template"), this); action->setIcon(QIcon::fromTheme(QStringLiteral("code-class"))); action->setData(url); connect(action, &QAction::triggered, this, &FileTemplatesPlugin::createFromTemplate); ext.addAction(ContextMenuExtension::FileGroup, action); } if (item->file()) { fileUrl = item->path().toUrl(); } } else if (context->type() == Context::EditorContext) { KDevelop::EditorContext* editorContext = dynamic_cast(context); fileUrl = editorContext->url(); } if (fileUrl.isValid() && determineTemplateType(fileUrl) != NoTemplate) { QAction* action = new QAction(i18n("Show Template Preview"), this); action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview"))); action->setData(fileUrl); connect(action, &QAction::triggered, this, &FileTemplatesPlugin::previewTemplate); ext.addAction(ContextMenuExtension::ExtensionGroup, action); } return ext; } QString FileTemplatesPlugin::name() const { return i18n("File Templates"); } QIcon FileTemplatesPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("code-class")); } QAbstractItemModel* FileTemplatesPlugin::templatesModel() { if(!m_model) { m_model = new TemplatesModel(QStringLiteral("kdevfiletemplates"), this); } return m_model; } QString FileTemplatesPlugin::knsConfigurationFile() const { return QStringLiteral("kdevfiletemplates.knsrc"); } QStringList FileTemplatesPlugin::supportedMimeTypes() const { QStringList types; types << QStringLiteral("application/x-desktop"); types << QStringLiteral("application/x-bzip-compressed-tar"); types << QStringLiteral("application/zip"); return types; } void FileTemplatesPlugin::reload() { templatesModel(); m_model->refresh(); } void FileTemplatesPlugin::loadTemplate(const QString& fileName) { templatesModel(); m_model->loadTemplateFile(fileName); } void FileTemplatesPlugin::createFromTemplate() { QUrl baseUrl; if (QAction* action = qobject_cast(sender())) { baseUrl = action->data().toUrl(); } if (!baseUrl.isValid()) { // fall-back to currently active document's parent directory IDocument* doc = ICore::self()->documentController()->activeDocument(); if (doc && doc->url().isValid()) { baseUrl = doc->url().adjusted(QUrl::RemoveFilename); } } TemplateClassAssistant* assistant = new TemplateClassAssistant(QApplication::activeWindow(), baseUrl); assistant->setAttribute(Qt::WA_DeleteOnClose); assistant->show(); } FileTemplatesPlugin::TemplateType FileTemplatesPlugin::determineTemplateType(const QUrl& url) { QDir dir(url.toLocalFile()); /* * Search for a description file in the url's directory. * If it is not found there, try cascading up a maximum of 5 directories. */ int level = 0; while (dir.cdUp() && level < 5) { QStringList filters; filters << QStringLiteral("*.kdevtemplate") << QStringLiteral("*.desktop"); foreach (const QString& entry, dir.entryList(filters)) { qCDebug(PLUGIN_FILETEMPLATES) << "Trying entry" << entry; /* * This logic is not perfect, but it works for most cases. * * Project template description files usually have the suffix * ".kdevtemplate", so those are easy to find. For project templates, * all the files in the directory are template files. * * On the other hand, file templates use the generic suffix ".desktop". * Fortunately, those explicitly list input and output files, so we * only match the explicitly listed files */ if (entry.endsWith(QLatin1String(".kdevtemplate"))) { return ProjectTemplate; } KConfig* config = new KConfig(dir.absoluteFilePath(entry), KConfig::SimpleConfig); KConfigGroup group = config->group("General"); qCDebug(PLUGIN_FILETEMPLATES) << "General group keys:" << group.keyList(); if (!group.hasKey("Name") || !group.hasKey("Category")) { continue; } if (group.hasKey("Files")) { qCDebug(PLUGIN_FILETEMPLATES) << "Group has files " << group.readEntry("Files", QStringList()); foreach (const QString& outputFile, group.readEntry("Files", QStringList())) { if (dir.absoluteFilePath(config->group(outputFile).readEntry("File")) == url.toLocalFile()) { return FileTemplate; } } } if (group.hasKey("ShowFilesAfterGeneration")) { return ProjectTemplate; } } ++level; } return NoTemplate; } void FileTemplatesPlugin::previewTemplate() { QAction* action = qobject_cast(sender()); if (!action || !action->data().toUrl().isValid()) { return; } TemplatePreviewToolView* preview = qobject_cast(core()->uiController()->findToolView(i18n("Template Preview"), m_toolView)); if (!preview) { return; } core()->documentController()->activateDocument(core()->documentController()->openDocument(action->data().toUrl())); } #include "filetemplatesplugin.moc" diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp index f1b7ab39d5..d78a918fcd 100644 --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -1,1513 +1,1509 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * 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) 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 "gitplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gitclonejob.h" #include #include #include "stashmanagerdialog.h" #include #include #include #include #include #include #include "gitjob.h" #include "gitmessagehighlighter.h" #include "gitplugincheckinrepositoryjob.h" #include "gitnameemaildialog.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_GIT, "kdevplatform.plugins.git") using namespace KDevelop; QVariant runSynchronously(KDevelop::VcsJob* job) { QVariant ret; if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) { ret = job->fetchResults(); } delete job; return ret; } namespace { QDir dotGitDirectory(const QUrl& dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir(); static const QString gitDir = QStringLiteral(".git"); while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git if (dir.isRoot()) { qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath; } return dir; } /** * Whenever a directory is provided, change it for all the files in it but not inner directories, * that way we make sure we won't get into recursion, */ static QList preventRecursion(const QList& urls) { QList ret; foreach(const QUrl& url, urls) { QDir d(url.toLocalFile()); if(d.exists()) { QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); foreach(const QString& entry, entries) { QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry)); ret += entryUrl; } } else ret += url; } return ret; } QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString()) { switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("^HEAD"); case VcsRevision::Base: return QString(); case VcsRevision::Working: return QString(); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); return currentRevision + "^1"; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: return rev.revisionValue().toString(); case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) { QString ret; if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval ret = toRevisionName(limit, QString()); else { QString dst = toRevisionName(limit); if(dst.isEmpty()) ret = dst; else { QString src = toRevisionName(rev, dst); if(src.isEmpty()) ret = src; else ret = src+".."+dst; } } return ret; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } QDir urlDir(const QList& urls) { return urlDir(urls.first()); } //TODO: could be improved } GitPlugin::GitPlugin( QObject *parent, const QVariantList & ) : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")), m_oldVersion(false), m_usePrefix(true) { if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) { setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?")); return; } - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBasicVersionControl ) - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IDistributedVersionControl ) - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBranchingVersionControl ) - setObjectName(QStringLiteral("Git")); DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent); *versionJob << "git" << "--version"; connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput); ICore::self()->runController()->registerJob(versionJob); m_watcher = new KDirWatch(this); connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged); connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged); } GitPlugin::~GitPlugin() {} bool emptyOutput(DVcsJob* job) { QScopedPointer _job(job); if(job->exec() && job->status()==VcsJob::JobSucceeded) return job->rawOutput().trimmed().isEmpty(); return false; } bool GitPlugin::hasStashes(const QDir& repository) { return !emptyOutput(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& d) { return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file) { return !emptyOutput(lsFiles(repo, QStringList() << QStringLiteral("-m") << file.path(), OutputJob::Silent)); } void GitPlugin::additionalMenuEntries(QMenu* menu, const QList& urls) { m_urls = urls; QDir dir=urlDir(urls); bool hasSt = hasStashes(dir); menu->addSeparator()->setText(i18n("Git Stashes")); menu->addAction(i18n("Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt); menu->addAction(i18n("Push Stash"), this, SLOT(ctxPushStash())); menu->addAction(i18n("Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt); } void GitPlugin::ctxPushStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxPopStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxStashManager() { QPointer d = new StashManagerDialog(urlDir(m_urls), this, nullptr); d->exec(); delete d; } DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } QString GitPlugin::name() const { return QStringLiteral("Git"); } QUrl GitPlugin::repositoryRoot(const QUrl& path) { return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath()); } bool GitPlugin::isValidDirectory(const QUrl & dirPath) { QDir dir=dotGitDirectory(dirPath); return dir.exists(QStringLiteral(".git/HEAD")); } bool GitPlugin::isVersionControlled(const QUrl &path) { QFileInfo fsObject(path.toLocalFile()); if (!fsObject.exists()) { return false; } if (fsObject.isDir()) { return isValidDirectory(path); } QString filename = fsObject.fileName(); QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent); return !otherFiles.empty(); } VcsJob* GitPlugin::init(const QUrl &directory) { DVcsJob* job = new DVcsJob(urlDir(directory), this); job->setType(VcsJob::Import); *job << "git" << "init"; return job; } VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode) { DVcsJob* job = new GitCloneJob(urlDir(dest), this); job->setType(VcsJob::Import); *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest; return job; } VcsJob* GitPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Add); *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } KDevelop::VcsJob* GitPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent); job->setType(VcsJob::Status); if(m_oldVersion) { *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old); } else { *job << "git" << "status" << "--porcelain"; job->setIgnoreError(true); connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput); } *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, VcsDiff::Type /*type*/, IBasicVersionControl::RecursionMode recursion) { //TODO: control different types DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Diff); *job << "git" << "diff" << "--no-color" << "--no-ext-diff"; if (!usePrefix()) { // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches // has become optional. *job << "--no-prefix"; } if(srcRevision.revisionType()==VcsRevision::Special && dstRevision.revisionType()==VcsRevision::Special && srcRevision.specialType()==VcsRevision::Base && dstRevision.specialType()==VcsRevision::Working) *job << "HEAD"; else if(dstRevision.specialType()==VcsRevision::Working){ *job << "--cached" << srcRevision.revisionValue().toString(); } else { QString revstr = revisionInterval(srcRevision, dstRevision); if(!revstr.isEmpty()) *job << revstr; } *job << "--"; if (recursion == IBasicVersionControl::Recursive) { *job << fileOrDirectory; } else { *job << preventRecursion(QList() << fileOrDirectory); } connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput); return job; } VcsJob* GitPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { if(localLocations.isEmpty() ) return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose); QDir repo = urlDir(repositoryRoot(localLocations.first())); QString modified; for (const auto& file: localLocations) { if (hasModifications(repo, file)) { modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "
"); } } if (!modified.isEmpty()) { auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommited changes, " "which will be lost. Continue?") + "

" + modified); if (res != KMessageBox::Yes) { return errorsFound(QString(), OutputJob::Silent); } } DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Revert); *job << "git" << "checkout" << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly... //If no files specified then commit already added files VcsJob* GitPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); const QDir dir = dotGitDirectory(localLocations.front()); if (!ensureValidGitIdentity(dir)) { return errorsFound(i18n("Email or name for Git not specified")); } DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); QList files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); addNotVersionedFiles(dir, files); *job << "git" << "commit" << "-m" << message; *job << "--" << files; return job; } bool GitPlugin::ensureValidGitIdentity(const QDir& dir) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath()); const QString name = readConfigOption(url, QStringLiteral("user.name")); const QString email = readConfigOption(url, QStringLiteral("user.email")); if (!email.isEmpty() && !name.isEmpty()) { return true; // already okay } GitNameEmailDialog dialog; dialog.setName(name); dialog.setEmail(email); if (!dialog.exec()) { return false; } runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal())); runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal())); return true; } void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList& files) { QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent); QList toadd, otherFiles; foreach(const QString& file, otherStr) { QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file)); otherFiles += v; } //We add the files that are not versioned foreach(const QUrl& file, files) { if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile()) toadd += file; } if(!toadd.isEmpty()) { VcsJob* job = add(toadd); job->exec(); } } bool isEmptyDirStructure(const QDir &dir) { foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if (i.isDir()) { if (!isEmptyDirStructure(QDir(i.filePath()))) return false; } else if (i.isFile()) { return false; } } return true; } VcsJob* GitPlugin::remove(const QList& files) { if (files.isEmpty()) return errorsFound(i18n("No files to remove")); QDir dotGitDir = dotGitDirectory(files.front()); QList files_(files); QMutableListIterator i(files_); while (i.hasNext()) { QUrl file = i.next(); QFileInfo fileInfo(file.toLocalFile()); QStringList otherStr = getLsFiles(dotGitDir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << file.toLocalFile(), KDevelop::OutputJob::Silent); if(!otherStr.isEmpty()) { //remove files not under version control QList otherFiles; foreach(const QString &f, otherStr) { otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f); } if (fileInfo.isFile()) { //if it's an unversioned file we are done, don't use git rm on it i.remove(); } auto trashJob = KIO::trash(otherFiles); trashJob->exec(); qCDebug(PLUGIN_GIT) << "other files" << otherFiles; } if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(file.toLocalFile()))) { //remove empty folders, git doesn't do that auto trashJob = KIO::trash(file); trashJob->exec(); qCDebug(PLUGIN_GIT) << "empty folder, removing" << file; //we already deleted it, don't use git rm on it i.remove(); } } } if (files_.isEmpty()) return nullptr; DVcsJob* job = new GitJob(dotGitDir, this); job->setType(VcsJob::Remove); // git refuses to delete files with local modifications // use --force to overcome this *job << "git" << "rm" << "-r" << "--force"; *job << "--" << files_; return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString rev = revisionInterval(dst, src); if(!rev.isEmpty()) *job << rev; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) *job << revStr; if(limit>0) *job << QStringLiteral("-%1").arg(limit); *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Annotate); *job << "git" << "blame" << "--porcelain" << "-w"; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput); return job; } void GitPlugin::parseGitBlameOutput(DVcsJob *job) { QVariantList results; VcsAnnotationLine* annotation = nullptr; const auto output = job->output(); const auto lines = output.splitRef('\n'); bool skipNext=false; QMap definedRevisions; for(QVector::const_iterator it=lines.constBegin(), itEnd=lines.constEnd(); it!=itEnd; ++it) { if(skipNext) { skipNext=false; results += qVariantFromValue(*annotation); continue; } if(it->isEmpty()) continue; QStringRef name = it->left(it->indexOf(' ')); QStringRef value = it->right(it->size()-name.size()-1); if(name==QLatin1String("author")) annotation->setAuthor(value.toString()); else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail? else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter? else if(name==QLatin1String("author-time")) annotation->setDate(QDateTime::fromTime_t(value.toUInt())); else if(name==QLatin1String("summary")) annotation->setCommitMessage(value.toString()); else if(name.startsWith(QStringLiteral("committer"))) {} //We will just store the authors else if(name==QLatin1String("previous")) {} //We don't need that either else if(name==QLatin1String("filename")) { skipNext=true; } else if(name==QLatin1String("boundary")) { definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine()); } else { const auto values = value.split(' '); VcsRevision rev; rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber); skipNext = definedRevisions.contains(name.toString()); if(!skipNext) definedRevisions.insert(name.toString(), VcsAnnotationLine()); annotation = &definedRevisions[name.toString()]; annotation->setLineNumber(values[1].toInt() - 1); annotation->setRevision(rev); } } job->setResults(results); } DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "ls-files" << args; return job; } DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "stash" << args; return job; } VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName) { DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "tag" << "-m" << commitMessage << tagName; if(rev.revisionValue().isValid()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch) { QDir d=urlDir(repository); if(hasModifications(d) && KMessageBox::questionYesNo(nullptr, i18n("There are pending changes, do you want to stash them first?"))==KMessageBox::Yes) { QScopedPointer stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose)); stash->exec(); } DVcsJob* job = new DVcsJob(d, this); *job << "git" << "checkout" << branch; return job; } VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "branch" << "--" << branchName; if(!rev.prettyValue().isEmpty()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-D" << branchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-m" << newBranchName << oldBranchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "merge" << branchName; return job; } VcsJob* GitPlugin::currentBranch(const QUrl& repository) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); job->setIgnoreError(true); *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } void GitPlugin::parseGitCurrentBranch(DVcsJob* job) { QString out = job->output().trimmed(); job->setResults(out); } VcsJob* GitPlugin::branches(const QUrl &repository) { DVcsJob* job=new DVcsJob(urlDir(repository)); *job << "git" << "branch" << "-a"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput); return job; } void GitPlugin::parseGitBranchOutput(DVcsJob* job) { const auto output = job->output(); const auto branchListDirty = output.splitRef('\n', QString::SkipEmptyParts); QStringList branchList; foreach(const auto & branch, branchListDirty) { // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master"); // "git rev-list" chokes on these entries and we do not need duplicate branches altogether. if (branch.contains(QStringLiteral("->"))) continue; // Skip entries such as '(no branch)' if (branch.contains(QStringLiteral("(no branch)"))) continue; QStringRef name = branch; if (name.startsWith('*')) name = branch.right(branch.size()-2); branchList << name.trimmed().toString(); } job->setResults(branchList); } /* Few words about how this hardcore works: 1. get all commits (with --paretns) 2. select master (root) branch and get all unicial commits for branches (git-rev-list br2 ^master ^br3) 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS, MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count) another setType(INITIAL) is used for "bottom/root/first" commits of branches 4. find and set merges, HEADS. It's an ittaration through all commits. - first we check if parent is from the same branch, if no then we go through all commits searching parent's index and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met) - then we check branchesShas[i][0] to mark heads 4 can be a seporate function. TODO: All this porn require refactoring (rewriting is better)! It's a very dirty implementation. FIXME: 1. HEAD which is head has extra line to connect it with further commit 2. If you menrge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be extra merge rectangle in master. If there are no extra commits in branch2, but there are another branches, then the place for branch2 will be empty (instead of be used for branch3). 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly */ QList GitPlugin::getAllCommits(const QString &repo) { initBranchHash(repo); QStringList args; args << QStringLiteral("--all") << QStringLiteral("--pretty") << QStringLiteral("--parents"); QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); static QRegExp rx_com("commit \\w{40,40}"); QListcommitList; DVcsEvent item; //used to keep where we have empty/cross/branch entry //true if it's an active branch (then cross or branch) and false if not QVector additionalFlags(branchesShas.count()); additionalFlags.fill(false); //parse output for(int i = 0; i < commits.count(); ++i) { if (commits[i].contains(rx_com)) { qCDebug(PLUGIN_GIT) << "commit found in " << commits[i]; item.setCommit(commits[i].section(' ', 1, 1).trimmed()); // qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1); QStringList parents; QString parent = commits[i].section(' ', 2); int section = 2; while (!parent.isEmpty()) { /* qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/ parents.append(parent.trimmed()); section++; parent = commits[i].section(' ', section); } item.setParents(parents); //Avoid Merge string while (!commits[i].contains(QStringLiteral("Author: "))) ++i; item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1); item.setDate(commits[++i].section(QStringLiteral("Date: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date: ", 1); QString log; i++; //next line! while (i < commits.count() && !commits[i].contains(rx_com)) log += commits[i++]; --i; //while took commit line item.setLog(log.trimmed()); // qCDebug(PLUGIN_GIT) << "log is: " << log; //mask is used in CommitViewDelegate to understand what we should draw for each branch QList mask; //set mask (properties for each graph column in row) for(int i = 0; i < branchesShas.count(); ++i) { qCDebug(PLUGIN_GIT)<<"commit: " << item.getCommit(); if (branchesShas[i].contains(item.getCommit())) { mask.append(item.getType()); //we set type in setParents //check if parent from the same branch, if not then we have found a root of the branch //and will use empty column for all futher (from top to bottom) revisions //FIXME: we should set CROSS between parent and child (and do it when find merge point) additionalFlags[i] = false; foreach(const QString &sha, item.getParents()) { if (branchesShas[i].contains(sha)) additionalFlags[i] = true; } if (additionalFlags[i] == false) item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing } else { if (additionalFlags[i] == false) mask.append(DVcsEvent::EMPTY); else mask.append(DVcsEvent::CROSS); } qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i]; } item.setProperties(mask); commitList.append(item); } } //find and set merges, HEADS, require refactoring! for(QList::iterator iter = commitList.begin(); iter != commitList.end(); ++iter) { QStringList parents = iter->getParents(); //we need only only child branches if (parents.count() != 1) break; QString parent = parents[0]; QString commit = iter->getCommit(); bool parent_checked = false; int heads_checked = 0; for(int i = 0; i < branchesShas.count(); ++i) { //check parent if (branchesShas[i].contains(commit)) { if (!branchesShas[i].contains(parent)) { //parent and child are not in same branch //since it is list, than parent has i+1 index //set CROSS and HCROSS for(QList::iterator f_iter = iter; f_iter != commitList.end(); ++f_iter) { if (parent == f_iter->getCommit()) { for(int j = 0; j < i; ++j) { if(branchesShas[j].contains(parent)) f_iter->setPropetry(j, DVcsEvent::MERGE); else f_iter->setPropetry(j, DVcsEvent::HCROSS); } f_iter->setType(DVcsEvent::MERGE); f_iter->setPropetry(i, DVcsEvent::MERGE_RIGHT); qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit; qCDebug(PLUGIN_GIT) << f_iter->getCommit() << " is merge"; parent_checked = true; break; } else f_iter->setPropetry(i, DVcsEvent::CROSS); } } } //mark HEADs if (!branchesShas[i].empty() && commit == branchesShas[i][0]) { iter->setType(DVcsEvent::HEAD); iter->setPropetry(i, DVcsEvent::HEAD); heads_checked++; qCDebug(PLUGIN_GIT) << "HEAD found"; } //some optimization if (heads_checked == branchesShas.count() && parent_checked) break; } } return commitList; } void GitPlugin::initBranchHash(const QString &repo) { const QUrl repoUrl = QUrl::fromLocalFile(repo); QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList(); qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches; //Now root branch is the current branch. In future it should be the longest branch //other commitLists are got with git-rev-lits branch ^br1 ^ br2 QString root = runSynchronously(currentBranch(repoUrl)).toString(); QScopedPointer job(gitRevList(repo, QStringList(root))); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); foreach(const QString &branch, gitBranches) { if (branch == root) continue; QStringList args(branch); foreach(const QString &branch_arg, gitBranches) { if (branch_arg != branch) //man gitRevList for '^' args<<'^' + branch_arg; } QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); } } //Actually we can just copy the output without parsing. So it's a kind of draft for future void GitPlugin::parseLogOutput(const DVcsJob * job, QList& commits) const { // static QRegExp rx_sep( "[-=]+" ); // static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" ); static QRegularExpression rx_com( "commit \\w{1,40}" ); const auto output = job->output(); const auto lines = output.splitRef('\n', QString::SkipEmptyParts); DVcsEvent item; QString commitLog; for (int i=0; i= 0x050500 if (rx_com.match(lines[i]).hasMatch()) { #else if (rx_com.match(lines[i].toString()).hasMatch()) { #endif // qCDebug(PLUGIN_GIT) << "MATCH COMMIT"; item.setCommit(lines[++i].toString()); item.setAuthor(lines[++i].toString()); item.setDate(lines[++i].toString()); item.setLog(commitLog); commits.append(item); } else { //FIXME: add this in a loop to the if, like in getAllCommits() commitLog += lines[i].toString() +'\n'; } } } VcsItemEvent::Actions actionsFromString(char c) { switch(c) { case 'A': return VcsItemEvent::Added; case 'D': return VcsItemEvent::Deleted; case 'R': return VcsItemEvent::Replaced; case 'M': return VcsItemEvent::Modified; } return VcsItemEvent::Modified; } void GitPlugin::parseGitLogOutput(DVcsJob * job) { static QRegExp commitRegex( "^commit (\\w{8})\\w{32}" ); static QRegExp infoRegex( "^(\\w+):(.*)" ); static QRegExp modificationsRegex("^([A-Z])[0-9]*\t([^\t]+)\t?(.*)", Qt::CaseSensitive, QRegExp::RegExp2); //R099 plugins/git/kdevgit.desktop plugins/git/kdevgit.desktop.cmake //M plugins/grepview/CMakeLists.txt QList commits; QString contents = job->output(); // check if git-log returned anything if (contents.isEmpty()) { job->setResults(commits); // empty list return; } // start parsing the output QTextStream s(&contents); VcsEvent item; QString message; bool pushCommit = false; while (!s.atEnd()) { QString line = s.readLine(); if (commitRegex.exactMatch(line)) { if (pushCommit) { item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); item.setItems(QList()); } else { pushCommit = true; } VcsRevision rev; rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber); item.setRevision(rev); message.clear(); } else if (infoRegex.exactMatch(line)) { QString cap1 = infoRegex.cap(1); if (cap1 == QLatin1String("Author")) { item.setAuthor(infoRegex.cap(2).trimmed()); } else if (cap1 == QLatin1String("Date")) { item.setDate(QDateTime::fromTime_t(infoRegex.cap(2).trimmed().split(' ')[0].toUInt())); } } else if (modificationsRegex.exactMatch(line)) { VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1()); QString filenameA = modificationsRegex.cap(2); VcsItemEvent itemEvent; itemEvent.setActions(a); itemEvent.setRepositoryLocation(filenameA); if(a==VcsItemEvent::Replaced) { QString filenameB = modificationsRegex.cap(3); itemEvent.setRepositoryCopySourceLocation(filenameB); } item.addItem(itemEvent); } else if (line.startsWith(QLatin1String(" "))) { message += line.remove(0, 4); message += '\n'; } } item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); job->setResults(commits); } void GitPlugin::parseGitDiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath()))); diff.setDepth(usePrefix()? 1 : 0); job->setResults(qVariantFromValue(diff)); } static VcsStatusInfo::State lsfilesToState(char id) { switch(id) { case 'H': return VcsStatusInfo::ItemUpToDate; //Cached case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted case 'C': return VcsStatusInfo::ItemModified; //modified/changed case 'K': return VcsStatusInfo::ItemDeleted; //to be killed case '?': return VcsStatusInfo::ItemUnknown; //other } Q_ASSERT(false); return VcsStatusInfo::ItemUnknown; } void GitPlugin::parseGitStatusOutput_old(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QDir dir = job->directory(); QMap allStatus; foreach(const QString& line, outputLines) { VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1()); QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.right(line.size()-2))); allStatus[url] = status; } QVariantList statuses; QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd(); for(; it!=itEnd; ++it) { VcsStatusInfo status; status.setUrl(it.key()); status.setState(it.value()); statuses.append(qVariantFromValue(status)); } job->setResults(statuses); } void GitPlugin::parseGitStatusOutput(DVcsJob* job) { const auto output = job->output(); const auto outputLines = output.splitRef('\n', QString::SkipEmptyParts); QDir workingDir = job->directory(); QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); QVariantList statuses; QList processedFiles; foreach(const QStringRef& line, outputLines) { //every line is 2 chars for the status, 1 space then the file desc QStringRef curr=line.right(line.size()-3); QStringRef state = line.left(2); int arrow = curr.indexOf(QStringLiteral(" -> ")); if(arrow>=0) { VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow)))); status.setState(VcsStatusInfo::ItemDeleted); statuses.append(qVariantFromValue(status)); processedFiles += status.url(); curr = curr.mid(arrow+4); } if(curr.startsWith('\"') && curr.endsWith('\"')) { //if the path is quoted, unquote curr = curr.mid(1, curr.size()-2); } VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString()))); status.setState(messageToState(state)); processedFiles.append(status.url()); qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state(); statuses.append(qVariantFromValue(status)); } QStringList paths; QStringList oldcmd=job->dvcsCommand(); QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd(); for(; it!=itEnd; ++it) paths += *it; //here we add the already up to date files QStringList files = getLsFiles(job->directory(), QStringList() << QStringLiteral("-c") << QStringLiteral("--") << paths, OutputJob::Silent); foreach(const QString& file, files) { QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file)); if(!processedFiles.contains(fileUrl)) { VcsStatusInfo status; status.setUrl(fileUrl); status.setState(VcsStatusInfo::ItemUpToDate); statuses.append(qVariantFromValue(status)); } } job->setResults(statuses); } void GitPlugin::parseGitVersionOutput(DVcsJob* job) { const auto output = job->output().trimmed(); auto versionString = output.midRef(output.lastIndexOf(' ')).split('.'); static const QList minimumVersion = QList() << 1 << 7; qCDebug(PLUGIN_GIT) << "checking git version" << versionString << "against" << minimumVersion; m_oldVersion = false; if (versionString.size() < minimumVersion.size()) { m_oldVersion = true; qCWarning(PLUGIN_GIT) << "invalid git version string:" << job->output().trimmed(); return; } foreach(int num, minimumVersion) { QStringRef curr = versionString.takeFirst(); int valcurr = curr.toInt(); if (valcurr < num) { m_oldVersion = true; break; } if (valcurr > num) { m_oldVersion = false; break; } } qCDebug(PLUGIN_GIT) << "the current git version is old: " << m_oldVersion; } QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(lsFiles(directory, args, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) return job->output().split('\n', QString::SkipEmptyParts); return QStringList(); } DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(QDir(repository), this, verbosity); *job << "git" << "rev-parse" << args; return job; } DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args) { DVcsJob* job = new DVcsJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent); { *job << "git" << "rev-list" << args; return job; } } VcsStatusInfo::State GitPlugin::messageToState(const QStringRef& msg) { Q_ASSERT(msg.size()==1 || msg.size()==2); VcsStatusInfo::State ret = VcsStatusInfo::ItemUnknown; if(msg.contains('U') || msg == QLatin1String("AA") || msg == QLatin1String("DD")) ret = VcsStatusInfo::ItemHasConflicts; else switch(msg.at(0).toLatin1()) { case 'M': ret = VcsStatusInfo::ItemModified; break; case 'A': ret = VcsStatusInfo::ItemAdded; break; case 'R': ret = VcsStatusInfo::ItemModified; break; case 'C': ret = VcsStatusInfo::ItemHasConflicts; break; case ' ': ret = msg.at(1) == 'M' ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemDeleted; break; case 'D': ret = VcsStatusInfo::ItemDeleted; break; case '?': ret = VcsStatusInfo::ItemUnknown; break; default: qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg; break; } return ret; } StandardJob::StandardJob(IPlugin* parent, KJob* job, OutputJob::OutputJobVerbosity verbosity) : VcsJob(parent, verbosity) , m_job(job) , m_plugin(parent) , m_status(JobNotStarted) {} void StandardJob::start() { connect(m_job, &KJob::result, this, &StandardJob::result); m_job->start(); m_status=JobRunning; } void StandardJob::result(KJob* job) { m_status=job->error() == 0? JobSucceeded : JobFailed; emitResult(); } VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { //TODO: Probably we should "git add" after return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent); } VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination) { QDir dir = urlDir(source); QFileInfo fileInfo(source.toLocalFile()); if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(source.toLocalFile()))) { //move empty folder, git doesn't do that qCDebug(PLUGIN_GIT) << "empty folder" << source; return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << source.toLocalFile(), KDevelop::OutputJob::Silent); if(otherStr.isEmpty()) { DVcsJob* job = new DVcsJob(dir, this, KDevelop::OutputJob::Verbose); *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile(); return job; } else { return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job) { job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output()))); } VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(urlDir(localLocation), this); //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "config" << "remote.origin.url"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput); return job; } VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "pull"; if(!localOrRepoLocationSrc.localUrl().isEmpty()) *job << localOrRepoLocationSrc.localUrl().url(); return job; } VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "push"; if(!localOrRepoLocationDst.localUrl().isEmpty()) *job << localOrRepoLocationDst.localUrl().url(); return job; } VcsJob* GitPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); } VcsJob* GitPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Head) { return pull(VcsLocation(), localLocations.first()); } else { DVcsJob* job = new DVcsJob(urlDir(localLocations.first()), this); { //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "checkout" << rev.revisionValue().toString() << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } } } void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const { new GitMessageHighlighter(editor); QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG"))); // Some limit on the file size should be set since whole content is going to be read into // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit // message. static const qint64 maxMergeMsgFileSize = 1024*1024; if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly)) return; QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize)); editor->setPlainText(mergeMsg); } class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget { Q_OBJECT public: GitVcsLocationWidget(QWidget* parent = nullptr, Qt::WindowFlags f = nullptr) : StandardVcsLocationWidget(parent, f) {} bool isCorrect() const override { return !url().isEmpty(); } }; KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const { return new GitVcsLocationWidget(parent); } void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository) { QDir dir = dotGitDirectory(repository); QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD")); m_watcher->addFile(headFile); } void GitPlugin::fileChanged(const QString& file) { Q_ASSERT(file.endsWith(QStringLiteral("HEAD"))); //SMTH/.git/HEAD -> SMTH/ const QUrl fileUrl = Path(file).parent().parent().toUrl(); //We need to delay the emitted signal, otherwise the branch hasn't change yet //and the repository is not functional m_branchesChange.append(fileUrl); QTimer::singleShot(1000, this, SLOT(delayedBranchChanged())); } void GitPlugin::delayedBranchChanged() { emit repositoryBranchChanged(m_branchesChange.takeFirst()); } CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document) { CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path()); job->start(); return job; } DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) { auto job = new DVcsJob(urlDir(repository), this); QStringList args; args << "git" << "config"; if(global) args << "--global"; args << key << value; *job << args; return job; } QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key) { QProcess exec; exec.setWorkingDirectory(urlDir(repository).absolutePath()); exec.start("git", QStringList() << "config" << "--get" << key); exec.waitForFinished(); return exec.readAllStandardOutput().trimmed(); } #include "gitplugin.moc" diff --git a/plugins/openwith/openwithplugin.cpp b/plugins/openwith/openwithplugin.cpp index b117b66df6..867698b3f2 100644 --- a/plugins/openwith/openwithplugin.cpp +++ b/plugins/openwith/openwithplugin.cpp @@ -1,291 +1,290 @@ /* * This file is part of KDevelop * Copyright 2009 Andreas Pakulat * * 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 "openwithplugin.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 using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(KDevOpenWithFactory, "kdevopenwith.json", registerPlugin();) namespace { bool sortActions(QAction* left, QAction* right) { return left->text() < right->text(); } bool isTextEditor(const KService::Ptr& service) { return service->serviceTypes().contains( QStringLiteral("KTextEditor/Document") ); } QString defaultForMimeType(const QString& mimeType) { KConfigGroup config = KSharedConfig::openConfig()->group("Open With Defaults"); if (config.hasKey(mimeType)) { QString storageId = config.readEntry(mimeType, QString()); if (!storageId.isEmpty() && KService::serviceByStorageId(storageId)) { return storageId; } } return QString(); } bool canOpenDefault(const QString& mimeType) { if (defaultForMimeType(mimeType).isEmpty() && mimeType == QLatin1String("inode/directory")) { // potentially happens in non-kde environments apparently, see https://git.reviewboard.kde.org/r/122373 return KMimeTypeTrader::self()->preferredService(mimeType); } else { return true; } } } OpenWithPlugin::OpenWithPlugin ( QObject* parent, const QVariantList& ) : IPlugin ( QStringLiteral("kdevopenwith"), parent ), m_actionMap( nullptr ) { - KDEV_USE_EXTENSION_INTERFACE( IOpenWith ) } OpenWithPlugin::~OpenWithPlugin() { } KDevelop::ContextMenuExtension OpenWithPlugin::contextMenuExtension( KDevelop::Context* context ) { // do not recurse if (context->hasType(KDevelop::Context::OpenWithContext)) { return ContextMenuExtension(); } m_urls.clear(); m_actionMap.reset(); m_services.clear(); FileContext* filectx = dynamic_cast( context ); ProjectItemContext* projctx = dynamic_cast( context ); if ( filectx && filectx->urls().count() > 0 ) { m_urls = filectx->urls(); } else if ( projctx && projctx->items().count() > 0 ) { // For now, let's handle *either* files only *or* directories only const int wantedType = projctx->items().at(0)->type(); foreach( ProjectBaseItem* item, projctx->items() ) { if (wantedType == ProjectBaseItem::File && item->file()) { m_urls << item->file()->path().toUrl(); } else if ((wantedType == ProjectBaseItem::Folder || wantedType == ProjectBaseItem::BuildFolder) && item->folder()) { m_urls << item->folder()->path().toUrl(); } } } if (m_urls.isEmpty()) { return KDevelop::ContextMenuExtension(); } m_actionMap.reset(new QSignalMapper( this )); connect( m_actionMap.data(), static_cast(&QSignalMapper::mapped), this, &OpenWithPlugin::open ); // Ok, lets fetch the mimetype for the !!first!! url and the relevant services // TODO: Think about possible alternatives to using the mimetype of the first url. QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(m_urls.first()); m_mimeType = mimetype.name(); QList partActions = actionsForServiceType(QStringLiteral("KParts/ReadOnlyPart")); QList appActions = actionsForServiceType(QStringLiteral("Application")); OpenWithContext subContext(m_urls, mimetype); QList extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &subContext ); foreach( const ContextMenuExtension& ext, extensions ) { appActions += ext.actions(ContextMenuExtension::OpenExternalGroup); partActions += ext.actions(ContextMenuExtension::OpenEmbeddedGroup); } { auto other = new QAction(i18n("Other..."), this); connect(other, &QAction::triggered, this, [this] { auto dialog = new KOpenWithDialog(m_urls, ICore::self()->uiController()->activeMainWindow()); if (dialog->exec() == QDialog::Accepted && dialog->service()) { openService(dialog->service()); } }); appActions << other; } // Now setup a menu with actions for each part and app QMenu* menu = new QMenu( i18n("Open With" ) ); auto documentOpenIcon = QIcon::fromTheme( QStringLiteral("document-open") ); menu->setIcon( documentOpenIcon ); if (!partActions.isEmpty()) { menu->addSection(i18n("Embedded Editors")); menu->addActions( partActions ); } if (!appActions.isEmpty()) { menu->addSection(i18n("External Applications")); menu->addActions( appActions ); } KDevelop::ContextMenuExtension ext; if (canOpenDefault(m_mimeType)) { QAction* openAction = new QAction( i18n( "Open" ), this ); openAction->setIcon( documentOpenIcon ); connect( openAction, SIGNAL(triggered()), SLOT(openDefault()) ); ext.addAction( KDevelop::ContextMenuExtension::FileGroup, openAction ); } ext.addAction(KDevelop::ContextMenuExtension::FileGroup, menu->menuAction()); return ext; } QList OpenWithPlugin::actionsForServiceType( const QString& serviceType ) { KService::List list = KMimeTypeTrader::self()->query( m_mimeType, serviceType ); KService::Ptr pref = KMimeTypeTrader::self()->preferredService( m_mimeType, serviceType ); m_services += list; QList actions; QAction* standardAction = nullptr; const QString defaultId = defaultForMimeType(m_mimeType); foreach( KService::Ptr svc, list ) { QAction* act = new QAction( isTextEditor(svc) ? i18n("Default Editor") : svc->name(), this ); act->setIcon( QIcon::fromTheme( svc->icon() ) ); if (svc->storageId() == defaultId || (defaultId.isEmpty() && isTextEditor(svc))) { QFont font = act->font(); font.setBold(true); act->setFont(font); } connect(act, &QAction::triggered, m_actionMap.data(), static_cast(&QSignalMapper::map)); m_actionMap->setMapping( act, svc->storageId() ); actions << act; if ( isTextEditor(svc) ) { standardAction = act; } else if ( svc->storageId() == pref->storageId() ) { standardAction = act; } } std::sort(actions.begin(), actions.end(), sortActions); if (standardAction) { actions.removeOne(standardAction); actions.prepend(standardAction); } return actions; } void OpenWithPlugin::openDefault() { // check preferred handler const QString defaultId = defaultForMimeType(m_mimeType); if (!defaultId.isEmpty()) { open(defaultId); return; } // default handlers if (m_mimeType == QLatin1String("inode/directory")) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(m_mimeType); KRun::runService(*service, m_urls, ICore::self()->uiController()->activeMainWindow()); } else { foreach( const QUrl& u, m_urls ) { ICore::self()->documentController()->openDocument( u ); } } } void OpenWithPlugin::open( const QString& storageid ) { openService(KService::serviceByStorageId( storageid )); } void OpenWithPlugin::openService(const KService::Ptr& service) { if (service->isApplication()) { KRun::runService( *service, m_urls, ICore::self()->uiController()->activeMainWindow() ); } else { QString prefName = service->desktopEntryName(); if (isTextEditor(service)) { // If the user chose a KTE part, lets make sure we're creating a TextDocument instead of // a PartDocument by passing no preferredpart to the documentcontroller // TODO: Solve this rather inside DocumentController prefName.clear(); } foreach( const QUrl& u, m_urls ) { ICore::self()->documentController()->openDocument( u, prefName ); } } KConfigGroup config = KSharedConfig::openConfig()->group("Open With Defaults"); if (service->storageId() != config.readEntry(m_mimeType, QString())) { int setDefault = KMessageBox::questionYesNo( qApp->activeWindow(), i18nc("%1: mime type name, %2: app/part name", "Do you want to open all '%1' files by default with %2?", m_mimeType, service->name() ), i18n("Set as default?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("OpenWith-%1").arg(m_mimeType) ); if (setDefault == KMessageBox::Yes) { config.writeEntry(m_mimeType, service->storageId()); } } } void OpenWithPlugin::openFilesInternal( const QList& files ) { if (files.isEmpty()) { return; } m_urls = files; m_mimeType = QMimeDatabase().mimeTypeForUrl(m_urls.first()).name(); openDefault(); } #include "openwithplugin.moc" diff --git a/plugins/patchreview/patchreview.cpp b/plugins/patchreview/patchreview.cpp index 3b0981d5bc..699720c698 100644 --- a/plugins/patchreview/patchreview.cpp +++ b/plugins/patchreview/patchreview.cpp @@ -1,628 +1,627 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * 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 "patchreview.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 ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught #define CATCHLIBDIFF /* Exclude this file from doublequote_chars check as krazy doesn't understand std::string*/ //krazy:excludeall=doublequote_chars #include #include #include #include #include #include "patchhighlighter.h" #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_PATCHREVIEW, "kdevplatform.plugins.patchreview") using namespace KDevelop; namespace { // Maximum number of files to open directly within a tab when the review is started const int maximumFilesToOpenDirectly = 15; } Q_DECLARE_METATYPE( const Diff2::DiffModel* ) void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) { try { qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty(); if ( !m_modelList ) throw "no model"; for ( int a = 0; a < m_modelList->modelCount(); ++a ) { const Diff2::DiffModel* model = m_modelList->modelAt( a ); if ( !model || !model->differences() ) continue; QUrl file = urlForFileModel( model ); if ( !fileName.isEmpty() && fileName != file ) continue; IDocument* doc = ICore::self()->documentController()->documentForUrl( file ); if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) { if ( doc->textDocument() ) { const QList ranges = m_highlighters[doc->url()]->ranges(); KTextEditor::View * v = doc->activeTextView(); int bestLine = -1; if ( v ) { KTextEditor::Cursor c = v->cursorPosition(); for ( QList::const_iterator it = ranges.begin(); it != ranges.end(); ++it ) { int line = ( *it )->start().line(); if ( forwards ) { if ( line > c.line() && ( bestLine == -1 || line < bestLine ) ) bestLine = line; } else { if ( line < c.line() && ( bestLine == -1 || line > bestLine ) ) bestLine = line; } } if ( bestLine != -1 ) { v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) ); return; } else if(fileName.isEmpty()) { int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1); if (next < maximumFilesToOpenDirectly) { ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next))); } } } } } } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found"; } void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); if ( file != highlightFile ) continue; qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString(); IDocument* doc = document; if( !doc ) doc = ICore::self()->documentController()->documentForUrl( file ); qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc; if ( !doc || !doc->textDocument() ) continue; removeHighlighting( file ); m_highlighters[file] = new PatchHighlighter( model, doc, this, dynamic_cast(m_patch.data()) == nullptr ); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::highlightPatch() { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { const Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); addHighlighting( file ); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::removeHighlighting( const QUrl& file ) { if ( file.isEmpty() ) { ///Remove all highlighting qDeleteAll( m_highlighters ); m_highlighters.clear(); } else { HighlightMap::iterator it = m_highlighters.find( file ); if ( it != m_highlighters.end() ) { delete *it; m_highlighters.erase( it ); } } } void PatchReviewPlugin::notifyPatchChanged() { if (m_patch) { qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file(); m_updateKompareTimer->start( 500 ); } else { m_updateKompareTimer->stop(); } } void PatchReviewPlugin::forceUpdate() { if( m_patch ) { m_patch->update(); notifyPatchChanged(); } } void PatchReviewPlugin::updateKompareModel() { if ( !m_patch ) { ///TODO: this method should be cleaned up, it can be called by the timer and /// e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could /// lead to asserts before... return; } qCDebug(PLUGIN_PATCHREVIEW) << "updating model"; removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; delete m_diffSettings; { IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() ); if( patchDoc ) patchDoc->reload(); } QString patchFile; if( m_patch->file().isLocalFile() ) patchFile = m_patch->file().toLocalFile(); else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) { patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation); bool ret = KIO::copy( m_patch->file(), QUrl::fromLocalFile(patchFile) )->exec(); if( !ret ) { qWarning() << "Problem while downloading: " << m_patch->file() << "to" << patchFile; patchFile.clear(); } } if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load try { m_diffSettings = new DiffSettings( nullptr ); m_kompareInfo.reset( new Kompare::Info() ); m_kompareInfo->localDestination = patchFile; m_kompareInfo->localSource = m_patch->baseDir().toLocalFile(); m_kompareInfo->depth = m_patch->depth(); m_kompareInfo->applied = m_patch->isAlreadyApplied(); m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) ); m_modelList->slotKompareInfo( m_kompareInfo.data() ); try { m_modelList->openDirAndDiff(); } catch ( const QString & str ) { throw; } catch ( ... ) { throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." ); } for (m_depth = 0; m_depth < 10; ++m_depth) { bool allFound = true; for( int i = 0; i < m_modelList->modelCount(); i++ ) { if (!QFile::exists(urlForFileModel(m_modelList->modelAt(i)).toLocalFile())) { allFound = false; } } if (allFound) { break; // found depth } } emit patchChanged(); for( int i = 0; i < m_modelList->modelCount(); i++ ) { const Diff2::DiffModel* model = m_modelList->modelAt( i ); for( int j = 0; j < model->differences()->count(); j++ ) { model->differences()->at( j )->apply( m_patch->isAlreadyApplied() ); } } highlightPatch(); return; } catch ( const QString & str ) { KMessageBox::error( nullptr, str, i18n( "Kompare Model Update" ) ); } catch ( const char * str ) { KMessageBox::error( nullptr, str, i18n( "Kompare Model Update" ) ); } removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; m_kompareInfo.reset( nullptr ); delete m_diffSettings; emit patchChanged(); } K_PLUGIN_FACTORY_WITH_JSON(KDevProblemReporterFactory, "kdevpatchreview.json", registerPlugin();) class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory { public: PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new PatchReviewToolView( parent, m_plugin ); } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.PatchReview"); } private: PatchReviewPlugin *m_plugin; }; PatchReviewPlugin::~PatchReviewPlugin() { removeHighlighting(); // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829 // and http://qt-project.org/forums/viewthread/38406/#162801 // modified tweak: use setPatch() and deleteLater in that method. setPatch(nullptr); } void PatchReviewPlugin::clearPatch( QObject* _patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "clearing patch" << _patch << "current:" << ( QObject* )m_patch; IPatchSource::Ptr patch( ( IPatchSource* )_patch ); if( patch == m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "is current patch"; setPatch( IPatchSource::Ptr( new LocalPatchSource ) ); } } void PatchReviewPlugin::closeReview() { if( m_patch ) { IDocument* patchDocument = ICore::self()->documentController()->documentForUrl( m_patch->file() ); if (patchDocument) { // Revert modifications to the text document which we've done in updateReview patchDocument->setPrettyName( QString() ); patchDocument->textDocument()->setReadWrite( true ); KTextEditor::ModificationInterface* modif = dynamic_cast( patchDocument->textDocument() ); modif->setModifiedOnDiskWarning( true ); } removeHighlighting(); m_modelList.reset( nullptr ); m_depth = 0; if( !dynamic_cast( m_patch.data() ) ) { // make sure "show" button still openes the file dialog to open a custom patch file setPatch( new LocalPatchSource ); } else emit patchChanged(); Sublime::Area* area = ICore::self()->uiController()->activeArea(); if( area->objectName() == QLatin1String("review") ) { if( ICore::self()->documentController()->saveAllDocuments() ) ICore::self()->uiController()->switchToArea( QStringLiteral("code"), KDevelop::IUiController::ThisWindow ); } } } void PatchReviewPlugin::cancelReview() { if( m_patch ) { m_patch->cancelReview(); closeReview(); } } void PatchReviewPlugin::finishReview( QList selection ) { if( m_patch && m_patch->finishReview( selection ) ) { closeReview(); } } void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) { Q_UNUSED( mode ); emit startingNewReview(); setPatch( patch ); QMetaObject::invokeMethod( this, "updateReview", Qt::QueuedConnection ); } void PatchReviewPlugin::switchToEmptyReviewArea() { foreach(Sublime::Area* area, ICore::self()->uiController()->allAreas()) { if (area->objectName() == QLatin1String("review")) { area->clearDocuments(); } } if ( ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("review") ) ICore::self()->uiController()->switchToArea( QStringLiteral("review"), KDevelop::IUiController::ThisWindow ); } QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model ) { KDevelop::Path path(QDir::cleanPath(m_patch->baseDir().toLocalFile())); QVector destPath = KDevelop::Path("/"+model->destinationPath()).segments(); if (destPath.size() >= (int)m_depth) { destPath = destPath.mid(m_depth); } foreach(QString segment, destPath) { path.addPath(segment); } path.addPath(model->destinationFile()); return path.toUrl(); } void PatchReviewPlugin::updateReview() { if( !m_patch ) return; m_updateKompareTimer->stop(); switchToEmptyReviewArea(); KDevelop::IDocumentController *docController = ICore::self()->documentController(); // don't add documents opened automatically to the Files/Open Recent list IDocument* futureActiveDoc = docController->openDocument( m_patch->file(), KTextEditor::Range::invalid(), IDocumentController::DoNotAddToRecentOpen ); updateKompareModel(); if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) { // might happen if e.g. openDocument dialog was cancelled by user // or under the theoretic possibility of a non-text document getting opened return; } futureActiveDoc->textDocument()->setReadWrite( false ); futureActiveDoc->setPrettyName( i18n( "Overview" ) ); KTextEditor::ModificationInterface* modif = dynamic_cast( futureActiveDoc->textDocument() ); modif->setModifiedOnDiskWarning( false ); docController->activateDocument( futureActiveDoc ); PatchReviewToolView* toolView = qobject_cast(ICore::self()->uiController()->findToolView( i18n( "Patch Review" ), m_factory )); Q_ASSERT( toolView ); //Open all relates files for( int a = 0; a < m_modelList->modelCount() && a < maximumFilesToOpenDirectly; ++a ) { QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) ); if (absoluteUrl.isRelative()) { KMessageBox::error( nullptr, i18n("The base directory of the patch must be an absolute directory"), i18n( "Patch Review" ) ); break; } if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") ) { toolView->open( absoluteUrl, false ); }else{ // Maybe the file was deleted qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist"; } } } void PatchReviewPlugin::setPatch( IPatchSource* patch ) { if ( patch == m_patch ) { return; } if( m_patch ) { disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); if ( qobject_cast( m_patch ) ) { // make sure we don't leak this // TODO: what about other patch sources? m_patch->deleteLater(); } } m_patch = patch; if( m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir(); connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); } QString finishText = i18n( "Finish Review" ); if( m_patch && !m_patch->finishReviewCustomText().isEmpty() ) finishText = m_patch->finishReviewCustomText(); m_finishReview->setText( finishText ); m_finishReview->setEnabled( patch ); notifyPatchChanged(); } PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & ) : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ), - m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) ) { - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IPatchReview ) - KDEV_USE_EXTENSION_INTERFACE( KDevelop::ILanguageSupport ) + m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) ) +{ qRegisterMetaType( "const Diff2::DiffModel*" ); setXMLFile( QStringLiteral("kdevpatchreview.rc") ); connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed ); connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated ); connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved ); m_updateKompareTimer = new QTimer( this ); m_updateKompareTimer->setSingleShot( true ); connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel ); m_finishReview = new QAction(i18n("Finish Review"), this); m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) ); actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return ); actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview); foreach(Sublime::Area* area, ICore::self()->uiController()->allAreas()) { if (area->objectName() == QLatin1String("review")) area->addAction(m_finishReview); } core()->uiController()->addToolView( i18n( "Patch Review" ), m_factory, IUiController::None ); areaChanged(ICore::self()->uiController()->activeArea()); } void PatchReviewPlugin::documentClosed( IDocument* doc ) { removeHighlighting( doc->url() ); } void PatchReviewPlugin::documentSaved( IDocument* doc ) { // Only update if the url is not the patch-file, because our call to // the reload() KTextEditor function also causes this signal, // which would lead to an endless update loop. // Also, don't automatically update local patch sources, because // they may correspond to static files which don't match any more // after an edit was done. if( m_patch && doc->url() != m_patch->file() && !dynamic_cast(m_patch.data()) ) forceUpdate(); } void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) { if (m_patch) { addHighlighting( doc->url(), doc ); } } void PatchReviewPlugin::unload() { core()->uiController()->removeToolView( m_factory ); KDevelop::IPlugin::unload(); } void PatchReviewPlugin::areaChanged(Sublime::Area* area) { bool reviewing = area->objectName() == QLatin1String("review"); m_finishReview->setEnabled(reviewing); if(!reviewing) { closeReview(); } } KDevelop::ContextMenuExtension PatchReviewPlugin::contextMenuExtension( KDevelop::Context* context ) { QList urls; if ( context->type() == KDevelop::Context::FileContext ) { KDevelop::FileContext* filectx = dynamic_cast( context ); urls = filectx->urls(); } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { KDevelop::ProjectItemContext* projctx = dynamic_cast( context ); foreach( KDevelop::ProjectBaseItem* item, projctx->items() ) { if ( item->file() ) { urls << item->file()->path().toUrl(); } } } else if ( context->type() == KDevelop::Context::EditorContext ) { KDevelop::EditorContext *econtext = dynamic_cast( context ); urls << econtext->url(); } if (urls.size() == 1) { QString mimetype = QMimeDatabase().mimeTypeForUrl(urls.first()).name(); QAction* reviewAction = new QAction( i18n( "Review Patch" ), this ); reviewAction->setData(QVariant(urls[0])); connect( reviewAction, &QAction::triggered, this, &PatchReviewPlugin::executeFileReviewAction ); ContextMenuExtension cm; cm.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, reviewAction ); return cm; } return KDevelop::IPlugin::contextMenuExtension( context ); } void PatchReviewPlugin::executeFileReviewAction() { QAction* reviewAction = qobject_cast(sender()); KDevelop::Path path(reviewAction->data().toUrl()); LocalPatchSource* ps = new LocalPatchSource(); ps->setFilename(path.toUrl()); ps->setBaseDir(path.parent().toUrl()); ps->setAlreadyApplied(true); ps->createWidget(); startReview(ps, OpenAndRaise); } #include "patchreview.moc" // kate: space-indent on; indent-width 4; tab-width 4; replace-tabs on diff --git a/plugins/perforce/perforceplugin.cpp b/plugins/perforce/perforceplugin.cpp index e130817f13..2b96de43ae 100644 --- a/plugins/perforce/perforceplugin.cpp +++ b/plugins/perforce/perforceplugin.cpp @@ -1,696 +1,692 @@ /*************************************************************************** * Copyright 2010 Morten Danielsen Volden * * * * 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. * * * * 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 "perforceplugin.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString()) { bool ok; int previous = currentRevision.toInt(&ok); previous--; QString tmp; switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("#head"); case VcsRevision::Base: return QStringLiteral("#have"); case VcsRevision::Working: return QStringLiteral("#have"); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); tmp.setNum(previous); tmp.prepend("#"); return tmp; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: tmp.append("#"); tmp.append(rev.revisionValue().toString()); return tmp; case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } VcsItemEvent::Actions actionsFromString(QString const& changeDescription) { if(changeDescription == "add") return VcsItemEvent::Added; if(changeDescription == "delete") return VcsItemEvent::Deleted; return VcsItemEvent::Modified; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } } Q_LOGGING_CATEGORY(PLUGIN_PERFORCE, "kdevplatform.plugins.perforce") PerforcePlugin::PerforcePlugin(QObject* parent, const QVariantList&): KDevelop::IPlugin("kdevperforce", parent) , m_common(new KDevelop::VcsPluginHelper(this, this)) , m_perforcemenu(nullptr) , m_perforceConfigName("p4config.txt") , m_perforceExecutable("p4") , m_edit_action(nullptr) { QProcessEnvironment currentEviron(QProcessEnvironment::systemEnvironment()); QString tmp(currentEviron.value("P4CONFIG")); if (tmp.isEmpty()) { // We require the P4CONFIG variable to be set because the perforce command line client will need it setErrorDescription(i18n("The variable P4CONFIG is not set. Is perforce installed on the system?")); return; } else { m_perforceConfigName = tmp; } qCDebug(PLUGIN_PERFORCE) << "The value of P4CONFIG is : " << tmp; - - KDEV_USE_EXTENSION_INTERFACE(KDevelop::IBasicVersionControl) - KDEV_USE_EXTENSION_INTERFACE(KDevelop::ICentralizedVersionControl) - } PerforcePlugin::~PerforcePlugin() { } QString PerforcePlugin::name() const { return i18n("Perforce"); } KDevelop::VcsImportMetadataWidget* PerforcePlugin::createImportMetadataWidget(QWidget* /*parent*/) { return nullptr; } bool PerforcePlugin::isValidDirectory(const QUrl & dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(dirPath.toLocalFile()) : finfo.absoluteDir(); do { if (dir.exists(m_perforceConfigName)) { return true; } } while (dir.cdUp()); return false; } bool PerforcePlugin::isVersionControlled(const QUrl& localLocation) { QFileInfo fsObject(localLocation.toLocalFile()); if (fsObject.isDir()) { return isValidDirectory(localLocation); } return parseP4fstat(fsObject, KDevelop::OutputJob::Silent); } DVcsJob* PerforcePlugin::p4fstatJob(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(curFile.absolutePath(), this, verbosity); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); return job; } bool PerforcePlugin::parseP4fstat(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(p4fstatJob(curFile, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { qCDebug(PLUGIN_PERFORCE) << "Perforce returned: " << job->output(); if (!job->output().isEmpty()) return true; } return false; } QString PerforcePlugin::getRepositoryName(const QFileInfo& curFile) { static const QString DEPOT_FILE_STR("... depotFile "); QString ret; QScopedPointer job(p4fstatJob(curFile, KDevelop::OutputJob::Silent)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); foreach(const QString & line, outputLines) { int idx(line.indexOf(DEPOT_FILE_STR)); if (idx != -1) { ret = line.right(line.size() - DEPOT_FILE_STR.size()); return ret; } } } } return ret; } KDevelop::VcsJob* PerforcePlugin::repositoryLocation(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "add" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::remove(const QList& /*localLocations*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::copy(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDstn*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::move(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDst*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4StatusOutput); return job; } KDevelop::VcsJob* PerforcePlugin::revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "revert" << curFile.fileName(); return job; } KDevelop::VcsJob* PerforcePlugin::update(const QList& localLocations, const KDevelop::VcsRevision& /*rev*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); //*job << m_perforceExecutable << "-p" << "127.0.0.1:1666" << "info"; - Let's keep this for now it's very handy for debugging QString fileOrDirectory; if (curFile.isDir()) fileOrDirectory = curFile.absolutePath() + "/..."; else fileOrDirectory = curFile.fileName(); *job << m_perforceExecutable << "sync" << fileOrDirectory; return job; } KDevelop::VcsJob* PerforcePlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "submit" << "-d" << message << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type , KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(fileOrDirectory.toLocalFile()); QString depotSrcFileName = getRepositoryName(curFile); QString depotDstFileName = depotSrcFileName; depotSrcFileName.append(toRevisionName(srcRevision, dstRevision.prettyValue())); // dstRevision acutally contains the number that we want to take previous of DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); switch (dstRevision.revisionType()) { case VcsRevision::FileNumber: case VcsRevision::GlobalNumber: depotDstFileName.append("#"); depotDstFileName.append(dstRevision.prettyValue()); *job << m_perforceExecutable << "diff2" << "-u" << depotSrcFileName << depotDstFileName; break; case VcsRevision::Special: switch (dstRevision.revisionValue().value()) { case VcsRevision::Working: *job << m_perforceExecutable << "diff" << "-du" << depotSrcFileName; break; case VcsRevision::Start: case VcsRevision::UserSpecialType: default: break; } default: break; } connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4DiffOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, long unsigned int limit) { static QString lastSeenChangeList; QFileInfo curFile(localLocation.toLocalFile()); QString localLocationAndRevStr = localLocation.toLocalFile(); DVcsJob* job = new DVcsJob(urlDir(localLocation), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit"; if(limit > 0) *job << QStringLiteral("-m %1").arg(limit); if (curFile.isDir()) { localLocationAndRevStr.append("/..."); } QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) { // This is not too nice, but perforce argument for restricting output from filelog does not Work :-( // So putting this in so we do not end up in infinite loop calling log, if(revStr == lastSeenChangeList) { localLocationAndRevStr.append("#none"); lastSeenChangeList.clear(); } else { localLocationAndRevStr.append(revStr); lastSeenChangeList = revStr; } } *job << localLocationAndRevStr; qCDebug(PLUGIN_PERFORCE) << "Issuing the following command to p4: " << job->dvcsCommand(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/, const KDevelop::VcsRevision& /*limit*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::annotate(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "annotate" << "-qi" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4AnnotateOutput); return job; } KDevelop::VcsJob* PerforcePlugin::resolve(const QList& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::createWorkingCopy(const KDevelop::VcsLocation& /*sourceRepository*/, const QUrl& /*destinationDirectory*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsLocationWidget* PerforcePlugin::vcsLocation(QWidget* parent) const { return new StandardVcsLocationWidget(parent); } KDevelop::VcsJob* PerforcePlugin::edit(const QList& localLocations) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "edit" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::edit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::unedit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::localRevision(const QUrl& /*localLocation*/, KDevelop::VcsRevision::RevisionType) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::import(const QString& /*commitMessage*/, const QUrl& /*sourceDirectory*/, const KDevelop::VcsLocation& /*destinationRepository*/) { return nullptr; } KDevelop::ContextMenuExtension PerforcePlugin::contextMenuExtension(KDevelop::Context* context) { m_common->setupFromContext(context); const QList & ctxUrlList = m_common->contextUrlList(); bool hasVersionControlledEntries = false; for( const QUrl& url : ctxUrlList) { if (isValidDirectory(url)) { hasVersionControlledEntries = true; break; } } if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu * perforceMenu = m_common->commonActions(); perforceMenu->addSeparator(); perforceMenu->addSeparator(); if (!m_edit_action) { m_edit_action = new QAction(i18n("Edit"), this); connect(m_edit_action, &QAction::triggered, this, & PerforcePlugin::ctxEdit); } perforceMenu->addAction(m_edit_action); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, perforceMenu->menuAction()); return menuExt; } void PerforcePlugin::ctxEdit() { QList const & ctxUrlList = m_common->contextUrlList(); KDevelop::ICore::self()->runController()->registerJob(edit(ctxUrlList)); } void PerforcePlugin::setEnvironmentForJob(DVcsJob* job, const QFileInfo& curFile) { KProcess* jobproc = job->process(); jobproc->setEnv("P4CONFIG", m_perforceConfigName); if (curFile.isDir()) { jobproc->setEnv("PWD", curFile.filePath()); } else { jobproc->setEnv("PWD", curFile.absolutePath()); } } QList PerforcePlugin::getQvariantFromLogOutput(QStringList const& outputLines) { static const QString LOGENTRY_START("... #"); static const QString DEPOTMESSAGE_START("... ."); QMap changes; QList commits; QString currentFileName; QString changeNumberStr, author,changeDescription, commitMessage; VcsEvent currentVcsEvent; VcsItemEvent currentRepoFile; VcsRevision rev; int indexofAt; int changeNumber = 0; foreach(const QString & line, outputLines) { if (!line.startsWith(LOGENTRY_START) && !line.startsWith(DEPOTMESSAGE_START) && !line.startsWith('\t')) { currentFileName = line; } if(line.indexOf(LOGENTRY_START) != -1) { // expecting the Logentry line to be of the form: //... #5 change 10 edit on 2010/12/06 12:07:31 by mvo@testbed (text) changeNumberStr = line.section(' ', 3, 3 ); // We use global change number changeNumber = changeNumberStr.toInt(); author = line.section(' ', 9, 9); changeDescription = line.section(' ' , 4, 4 ); indexofAt = author.indexOf('@'); author.remove(indexofAt, author.size()); // Only keep the username itself rev.setRevisionValue(changeNumberStr, KDevelop::VcsRevision::GlobalNumber); changes[changeNumber].setRevision(rev); changes[changeNumber].setAuthor(author); changes[changeNumber].setDate(QDateTime::fromString(line.section(' ', 6, 7), "yyyy/MM/dd hh:mm:ss")); currentRepoFile.setRepositoryLocation(currentFileName); currentRepoFile.setActions( actionsFromString(changeDescription) ); changes[changeNumber].addItem(currentRepoFile); commitMessage.clear(); // We have a new entry, clear message } if (line.startsWith('\t') || line.startsWith(DEPOTMESSAGE_START)) { commitMessage += line.trimmed() + '\n'; changes[changeNumber].setMessage(commitMessage); } } for(auto item : changes) { commits.prepend(QVariant::fromValue(item)); } return commits; } void PerforcePlugin::parseP4StatusOutput(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QVariantList statuses; QList processedFiles; static const QString ACTION_STR("... action "); static const QString CLIENT_FILE_STR("... clientFile "); VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUserState); foreach(const QString & line, outputLines) { int idx(line.indexOf(ACTION_STR)); if (idx != -1) { QString curr = line.right(line.size() - ACTION_STR.size()); if (curr == "edit") { status.setState(VcsStatusInfo::ItemModified); } else if (curr == "add") { status.setState(VcsStatusInfo::ItemAdded); } else { status.setState(VcsStatusInfo::ItemUserState); } continue; } idx = line.indexOf(CLIENT_FILE_STR); if (idx != -1) { QUrl fileUrl = QUrl::fromLocalFile(line.right(line.size() - CLIENT_FILE_STR.size())); status.setUrl(fileUrl); } } statuses.append(qVariantFromValue(status)); job->setResults(statuses); } void PerforcePlugin::parseP4LogOutput(KDevelop::DVcsJob* job) { QList commits(getQvariantFromLogOutput(job->output().split('\n', QString::SkipEmptyParts))); job->setResults(commits); } void PerforcePlugin::parseP4DiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); QDir dir(job->directory()); do { if (dir.exists(m_perforceConfigName)) { break; } } while (dir.cdUp()); diff.setBaseDiff(QUrl::fromLocalFile(dir.absolutePath())); job->setResults(qVariantFromValue(diff)); } void PerforcePlugin::parseP4AnnotateOutput(DVcsJob *job) { QVariantList results; /// First get the changelists for this file QStringList strList(job->dvcsCommand()); QString localLocation(strList.last()); /// ASSUMPTION WARNING - localLocation is the last in the annotate command KDevelop::VcsRevision dummyRev; QScopedPointer logJob(new DVcsJob(job->directory(), this, OutputJob::Silent)); QFileInfo curFile(localLocation); setEnvironmentForJob(logJob.data(), curFile); *logJob << m_perforceExecutable << "filelog" << "-lit" << localLocation; QList commits; if (logJob->exec() && logJob->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { commits = getQvariantFromLogOutput(logJob->output().split('\n', QString::SkipEmptyParts)); } } VcsEvent item; QMap globalCommits; /// Move the VcsEvents to a more suitable data strucure for (QList::const_iterator commitsIt = commits.constBegin(), commitsEnd = commits.constEnd(); commitsIt != commitsEnd; ++commitsIt) { if(commitsIt->canConvert()) { item = commitsIt->value(); } globalCommits.insert(item.revision().revisionValue().toLongLong(), item); } VcsAnnotationLine* annotation; QStringList lines = job->output().split('\n'); size_t lineNumber(0); QMap definedRevisions; QMap::iterator currentEvent; bool convertToIntOk(false); int globalRevisionInt(0); QString globalRevision; for (QStringList::const_iterator it = lines.constBegin(), itEnd = lines.constEnd(); it != itEnd; ++it) { if (it->isEmpty()) { continue; } globalRevision = it->left(it->indexOf(':')); annotation = new VcsAnnotationLine; annotation->setLineNumber(lineNumber); VcsRevision rev; rev.setRevisionValue(globalRevision, KDevelop::VcsRevision::GlobalNumber); annotation->setRevision(rev); // Find the other info in the commits list globalRevisionInt = globalRevision.toLongLong(&convertToIntOk); if(convertToIntOk) { currentEvent = globalCommits.find(globalRevisionInt); annotation->setAuthor(currentEvent->author()); annotation->setCommitMessage(currentEvent->message()); annotation->setDate(currentEvent->date()); } results += qVariantFromValue(*annotation); ++lineNumber; } job->setResults(results); } KDevelop::VcsJob* PerforcePlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } diff --git a/plugins/projectfilter/projectfilterprovider.cpp b/plugins/projectfilter/projectfilterprovider.cpp index 048e8662d5..4fc7923ad6 100644 --- a/plugins/projectfilter/projectfilterprovider.cpp +++ b/plugins/projectfilter/projectfilterprovider.cpp @@ -1,168 +1,166 @@ /* This file is part of KDevelop Copyright 2013 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectfilterprovider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectfilterdebug.h" #include "projectfilterconfigpage.h" #include using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ProjectFilterProviderFactory, "kdevprojectfilter.json", registerPlugin();) ProjectFilterProvider::ProjectFilterProvider( QObject* parent, const QVariantList& /*args*/ ) : IPlugin( QStringLiteral( "kdevprojectfilter" ), parent ) { - KDEV_USE_EXTENSION_INTERFACE( IProjectFilterProvider ) - connect(core()->projectController(), &IProjectController::projectClosing, this, &ProjectFilterProvider::projectClosing); connect(core()->projectController(), &IProjectController::projectAboutToBeOpened, this, &ProjectFilterProvider::projectAboutToBeOpened); // initialize the filters for each project foreach(IProject* project, core()->projectController()->projects()) { updateProjectFilters(project); } } QSharedPointer ProjectFilterProvider::createFilter(IProject* project) const { return QSharedPointer(new ProjectFilter(project, m_filters[project])); } ContextMenuExtension ProjectFilterProvider::contextMenuExtension(Context* context) { ContextMenuExtension ret; if (!context->hasType(Context::ProjectItemContext)) { return ret; } ProjectItemContext* ctx = static_cast( context ); QList items = ctx->items(); // filter out project roots and items in targets QList< ProjectBaseItem* >::iterator it = items.begin(); while (it != items.end()) { if ((*it)->isProjectRoot() || !(*it)->parent()->folder()) { it = items.erase(it); } else { ++it; } } if (items.isEmpty()) { return ret; } QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18np("Exclude Item From Project", "Exclude Items From Project", items.size()), this); action->setData(QVariant::fromValue(items)); connect(action, &QAction::triggered, this, &ProjectFilterProvider::addFilterFromContextMenu); ret.addAction(ContextMenuExtension::FileGroup, action); return ret; } void ProjectFilterProvider::addFilterFromContextMenu() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); QList items = action->data().value >(); QHash changedProjectFilters; foreach(ProjectBaseItem* item, items) { if (!changedProjectFilters.contains(item->project())) { changedProjectFilters[item->project()] = readFilters(item->project()->projectConfiguration()); } SerializedFilters& filters = changedProjectFilters[item->project()]; Path path; if (item->target()) { path = Path(item->parent()->path(), item->text()); } else { path = item->path(); } filters << SerializedFilter('/' + item->project()->path().relativePath(path), item->folder() ? Filter::Folders : Filter::Files); } QHash< IProject*, SerializedFilters >::const_iterator it = changedProjectFilters.constBegin(); while (it != changedProjectFilters.constEnd()) { writeFilters(it.value(), it.key()->projectConfiguration()); m_filters[it.key()] = deserialize(it.value()); emit filterChanged(this, it.key()); ++it; } KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18np("A filter for the item was added. To undo, use the project filter settings.", "A filter for the items was added. To undo, use the project filter settings.", items.size()), i18n("Project Filter Added"), QStringLiteral("projectfilter-addfromctxmenu")); } void ProjectFilterProvider::updateProjectFilters(IProject* project) { Filters newFilters = deserialize(readFilters(project->projectConfiguration())); Filters& filters = m_filters[project]; if (filters != newFilters) { projectFilterDebug() << "project filter changed:" << project->name(); filters = newFilters; emit filterChanged(this, project); } } void ProjectFilterProvider::projectAboutToBeOpened(IProject* project) { m_filters[project] = deserialize(readFilters(project->projectConfiguration())); } void ProjectFilterProvider::projectClosing(IProject* project) { m_filters.remove(project); } int ProjectFilterProvider::perProjectConfigPages() const { return 1; } ConfigPage* ProjectFilterProvider::perProjectConfigPage(int i, const ProjectConfigOptions& options, QWidget* parent) { return i == 0 ? new ProjectFilterConfigPage(this, options, parent) : nullptr; } #include "projectfilterprovider.moc" diff --git a/plugins/quickopen/quickopenplugin.cpp b/plugins/quickopen/quickopenplugin.cpp index ed7e4f2354..a9cb9ab4ea 100644 --- a/plugins/quickopen/quickopenplugin.cpp +++ b/plugins/quickopen/quickopenplugin.cpp @@ -1,1108 +1,1107 @@ /* * This file is part of KDevelop * * Copyright 2007 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 "quickopenplugin.h" #include "quickopenwidget.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 #include "quickopenmodel.h" #include "projectfilequickopen.h" #include "projectitemquickopen.h" #include "declarationlistquickopen.h" #include "documentationquickopenprovider.h" #include "actionsquickopenprovider.h" #include "debug.h" #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(PLUGIN_QUICKOPEN, "kdevplatform.plugins.quickopen") using namespace KDevelop; const bool noHtmlDestriptionInOutline = true; class QuickOpenWidgetCreator { public: virtual ~QuickOpenWidgetCreator() { } virtual QuickOpenWidget* createWidget() = 0; virtual QString objectNameForLine() = 0; virtual void widgetShown() { } }; class StandardQuickOpenWidgetCreator : public QuickOpenWidgetCreator { public: StandardQuickOpenWidgetCreator(const QStringList& items, const QStringList& scopes) : m_items(items) , m_scopes(scopes) { } QString objectNameForLine() override { return QStringLiteral("Quickopen"); } void setItems(const QStringList& scopes, const QStringList& items) { m_scopes = scopes; m_items = items; } QuickOpenWidget* createWidget() override { QStringList useItems = m_items; if(useItems.isEmpty()) useItems = QuickOpenPlugin::self()->lastUsedItems; QStringList useScopes = m_scopes; if(useScopes.isEmpty()) useScopes = QuickOpenPlugin::self()->lastUsedScopes; return new QuickOpenWidget( i18n("Quick Open"), QuickOpenPlugin::self()->m_model, QuickOpenPlugin::self()->lastUsedItems, useScopes, false, true ); } QStringList m_items; QStringList m_scopes; }; class OutlineFilter : public DUChainUtils::DUChainItemFilter { public: enum OutlineMode { Functions, FunctionsAndClasses }; OutlineFilter(QList& _items, OutlineMode _mode = FunctionsAndClasses) : items(_items), mode(_mode) { } bool accept(Declaration* decl) override { if(decl->range().isEmpty()) return false; bool collectable = mode == Functions ? decl->isFunctionDeclaration() : (decl->isFunctionDeclaration() || (decl->internalContext() && decl->internalContext()->type() == DUContext::Class)); if (collectable) { DUChainItem item; item.m_item = IndexedDeclaration(decl); item.m_text = decl->toString(); items << item; return true; } else return false; } bool accept(DUContext* ctx) override { if(ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper ) return true; else return false; } QList& items; OutlineMode mode; }; K_PLUGIN_FACTORY_WITH_JSON(KDevQuickOpenFactory, "kdevquickopen.json", registerPlugin();) Declaration* cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return nullptr; KDevelop::DUChainReadLocker lock( DUChain::lock() ); return DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor( view->document()->url(), KTextEditor::Cursor(view->cursorPosition()) ).declaration ); } ///The first definition that belongs to a context that surrounds the current cursor Declaration* cursorContextDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return nullptr; KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* ctx = DUChainUtils::standardContextForUrl(view->document()->url()); if(!ctx) return nullptr; KTextEditor::Cursor cursor(view->cursorPosition()); DUContext* subCtx = ctx->findContext(ctx->transformToLocalRevision(cursor)); while(subCtx && !subCtx->owner()) subCtx = subCtx->parentContext(); Declaration* definition = nullptr; if(!subCtx || !subCtx->owner()) definition = DUChainUtils::declarationInLine(cursor, ctx); else definition = subCtx->owner(); if(!definition) return nullptr; return definition; } //Returns only the name, no template-parameters or scope QString cursorItemText() { KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) return QString(); IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) return QString(); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return QString(); } AbstractType::Ptr t = decl->abstractType(); IdentifiedType* idType = dynamic_cast(t.data()); if( idType && idType->declaration(context) ) decl = idType->declaration(context); if(!decl->qualifiedIdentifier().isEmpty()) return decl->qualifiedIdentifier().last().identifier().str(); return QString(); } QuickOpenLineEdit* QuickOpenPlugin::createQuickOpenLineWidget() { return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(QStringList(), QStringList())); } QuickOpenLineEdit* QuickOpenPlugin::quickOpenLine(QString name) { QList< QuickOpenLineEdit* > lines = ICore::self()->uiController()->activeMainWindow()->findChildren(name); foreach(QuickOpenLineEdit* line, lines) { if(line->isVisible()) { return line; } } return nullptr; } static QuickOpenPlugin* staticQuickOpenPlugin = nullptr; QuickOpenPlugin* QuickOpenPlugin::self() { return staticQuickOpenPlugin; } void QuickOpenPlugin::createActionsForMainWindow(Sublime::MainWindow* /*window*/, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevquickopen.rc"); QAction* quickOpen = actions.addAction(QStringLiteral("quick_open")); quickOpen->setText( i18n("&Quick Open") ); quickOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen")) ); actions.setDefaultShortcut( quickOpen, Qt::CTRL | Qt::ALT | Qt::Key_Q ); connect(quickOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpen); QAction* quickOpenFile = actions.addAction(QStringLiteral("quick_open_file")); quickOpenFile->setText( i18n("Quick Open &File") ); quickOpenFile->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); actions.setDefaultShortcut( quickOpenFile, Qt::CTRL | Qt::ALT | Qt::Key_O ); connect(quickOpenFile, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFile); QAction* quickOpenClass = actions.addAction(QStringLiteral("quick_open_class")); quickOpenClass->setText( i18n("Quick Open &Class") ); quickOpenClass->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-class")) ); actions.setDefaultShortcut( quickOpenClass, Qt::CTRL | Qt::ALT | Qt::Key_C ); connect(quickOpenClass, &QAction::triggered, this, &QuickOpenPlugin::quickOpenClass); QAction* quickOpenFunction = actions.addAction(QStringLiteral("quick_open_function")); quickOpenFunction->setText( i18n("Quick Open &Function") ); quickOpenFunction->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-function")) ); actions.setDefaultShortcut( quickOpenFunction, Qt::CTRL | Qt::ALT | Qt::Key_M ); connect(quickOpenFunction, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFunction); QAction* quickOpenAlreadyOpen = actions.addAction(QStringLiteral("quick_open_already_open")); quickOpenAlreadyOpen->setText( i18n("Quick Open &Already Open File") ); quickOpenAlreadyOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); connect(quickOpenAlreadyOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpenOpenFile); QAction* quickOpenDocumentation = actions.addAction(QStringLiteral("quick_open_documentation")); quickOpenDocumentation->setText( i18n("Quick Open &Documentation") ); quickOpenDocumentation->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-documentation")) ); actions.setDefaultShortcut( quickOpenDocumentation, Qt::CTRL | Qt::ALT | Qt::Key_D ); connect(quickOpenDocumentation, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDocumentation); QAction* quickOpenActions = actions.addAction(QStringLiteral("quick_open_actions")); quickOpenActions->setText( i18n("Quick Open &Actions") ); actions.setDefaultShortcut( quickOpenActions, Qt::CTRL | Qt::ALT | Qt::Key_A); connect(quickOpenActions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenActions); m_quickOpenDeclaration = actions.addAction(QStringLiteral("quick_open_jump_declaration")); m_quickOpenDeclaration->setText( i18n("Jump to Declaration") ); m_quickOpenDeclaration->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-declaration") ) ); actions.setDefaultShortcut( m_quickOpenDeclaration, Qt::CTRL | Qt::Key_Period ); connect(m_quickOpenDeclaration, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDeclaration, Qt::QueuedConnection); m_quickOpenDefinition = actions.addAction(QStringLiteral("quick_open_jump_definition")); m_quickOpenDefinition->setText( i18n("Jump to Definition") ); m_quickOpenDefinition->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-definition") ) ); actions.setDefaultShortcut( m_quickOpenDefinition, Qt::CTRL | Qt::Key_Comma ); connect(m_quickOpenDefinition, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDefinition, Qt::QueuedConnection); QWidgetAction* quickOpenLine = new QWidgetAction(this); quickOpenLine->setText( i18n("Embedded Quick Open") ); // actions.setDefaultShortcut( quickOpenLine, Qt::CTRL | Qt::ALT | Qt::Key_E ); // connect(quickOpenLine, SIGNAL(triggered(bool)), this, SLOT(quickOpenLine(bool))); quickOpenLine->setDefaultWidget(createQuickOpenLineWidget()); actions.addAction(QStringLiteral("quick_open_line"), quickOpenLine); QAction* quickOpenNextFunction = actions.addAction(QStringLiteral("quick_open_next_function")); quickOpenNextFunction->setText( i18n("Next Function") ); actions.setDefaultShortcut( quickOpenNextFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageDown ); connect(quickOpenNextFunction, &QAction::triggered, this, &QuickOpenPlugin::nextFunction); QAction* quickOpenPrevFunction = actions.addAction(QStringLiteral("quick_open_prev_function")); quickOpenPrevFunction->setText( i18n("Previous Function") ); actions.setDefaultShortcut( quickOpenPrevFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageUp ); connect(quickOpenPrevFunction, &QAction::triggered, this, &QuickOpenPlugin::previousFunction); QAction* quickOpenNavigateFunctions = actions.addAction(QStringLiteral("quick_open_outline")); quickOpenNavigateFunctions->setText( i18n("Outline") ); actions.setDefaultShortcut( quickOpenNavigateFunctions, Qt::CTRL| Qt::ALT | Qt::Key_N ); connect(quickOpenNavigateFunctions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenNavigateFunctions); } QuickOpenPlugin::QuickOpenPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevquickopen"), parent) { staticQuickOpenPlugin = this; - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IQuickOpen ) m_model = new QuickOpenModel( nullptr ); KConfigGroup quickopengrp = KSharedConfig::openConfig()->group("QuickOpen"); lastUsedScopes = quickopengrp.readEntry("SelectedScopes", QStringList() << i18n("Project") << i18n("Includes") << i18n("Includers") << i18n("Currently Open") ); lastUsedItems = quickopengrp.readEntry("SelectedItems", QStringList() ); { m_openFilesData = new OpenFilesDataProvider(); QStringList scopes, items; scopes << i18n("Currently Open"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_openFilesData ); } { m_projectFileData = new ProjectFileDataProvider(); QStringList scopes, items; scopes << i18n("Project"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_projectFileData ); } { m_projectItemData = new ProjectItemDataProvider(this); QStringList scopes, items; scopes << i18n("Project"); items << ProjectItemDataProvider::supportedItemTypes(); m_model->registerProvider( scopes, items, m_projectItemData ); } { m_documentationItemData = new DocumentationQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Documentation"); m_model->registerProvider( scopes, items, m_documentationItemData ); } { m_actionsItemData = new ActionsQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Actions"); m_model->registerProvider( scopes, items, m_actionsItemData ); } } QuickOpenPlugin::~QuickOpenPlugin() { freeModel(); delete m_model; delete m_projectFileData; delete m_projectItemData; delete m_openFilesData; delete m_documentationItemData; delete m_actionsItemData; } void QuickOpenPlugin::unload() { } ContextMenuExtension QuickOpenPlugin::contextMenuExtension(Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker readLock; Declaration* decl(codeContext->declaration().data()); if (decl) { const bool isDef = FunctionDefinition::definition(decl); if (codeContext->use().isValid() || !isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDeclaration); } if(isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDefinition); } } return menuExt; } void QuickOpenPlugin::showQuickOpen(const QStringList& items) { if(!freeModel()) return; QStringList initialItems = items; QStringList useScopes = lastUsedScopes; if (!useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); showQuickOpenWidget(initialItems, useScopes, false); } void QuickOpenPlugin::showQuickOpen( ModelTypes modes ) { if(!freeModel()) return; QStringList initialItems; if( modes & Files || modes & OpenFiles ) initialItems << i18n("Files"); if( modes & Functions ) initialItems << i18n("Functions"); if( modes & Classes ) initialItems << i18n("Classes"); QStringList useScopes; if ( modes != OpenFiles ) useScopes = lastUsedScopes; if((modes & OpenFiles) && !useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); bool preselectText = (!(modes & Files) || modes == QuickOpenPlugin::All); showQuickOpenWidget(initialItems, useScopes, preselectText); } void QuickOpenPlugin::showQuickOpenWidget(const QStringList& items, const QStringList& scopes, bool preselectText) { QuickOpenWidgetDialog* dialog = new QuickOpenWidgetDialog( i18n("Quick Open"), m_model, items, scopes ); m_currentWidgetHandler = dialog; if (preselectText) { KDevelop::IDocument *currentDoc = core()->documentController()->activeDocument(); if (currentDoc && currentDoc->isTextDocument()) { QString preselected = currentDoc->textSelection().isEmpty() ? currentDoc->textWord() : currentDoc->textDocument()->text(currentDoc->textSelection()); dialog->widget()->setPreselectedText(preselected); } } connect( dialog->widget(), &QuickOpenWidget::scopesChanged, this, &QuickOpenPlugin::storeScopes ); //Not connecting itemsChanged to storeItems, as showQuickOpen doesn't use lastUsedItems and so shouldn't store item changes //connect( dialog->widget(), SIGNAL(itemsChanged(QStringList)), this, SLOT(storeItems(QStringList)) ); dialog->widget()->ui.itemsButton->setEnabled(false); if(quickOpenLine()) { quickOpenLine()->showWithWidget(dialog->widget()); dialog->deleteLater(); }else{ dialog->run(); } } void QuickOpenPlugin::storeScopes( const QStringList& scopes ) { lastUsedScopes = scopes; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedScopes", scopes ); } void QuickOpenPlugin::storeItems( const QStringList& items ) { lastUsedItems = items; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedItems", items ); } void QuickOpenPlugin::quickOpen() { if(quickOpenLine()) //Same as clicking on Quick Open quickOpenLine()->setFocus(); else showQuickOpen( All ); } void QuickOpenPlugin::quickOpenFile() { showQuickOpen( (ModelTypes)(Files | OpenFiles) ); } void QuickOpenPlugin::quickOpenFunction() { showQuickOpen( Functions ); } void QuickOpenPlugin::quickOpenClass() { showQuickOpen( Classes ); } void QuickOpenPlugin::quickOpenOpenFile() { showQuickOpen( OpenFiles ); } void QuickOpenPlugin::quickOpenDocumentation() { showQuickOpenWidget(QStringList(i18n("Documentation")), QStringList(i18n("Includes")), true); } void QuickOpenPlugin::quickOpenActions() { showQuickOpenWidget(QStringList(i18n("Actions")), QStringList(i18n("Includes")), true); } QSet QuickOpenPlugin::fileSet() const { return m_model->fileSet(); } void QuickOpenPlugin::registerProvider( const QStringList& scope, const QStringList& type, KDevelop::QuickOpenDataProviderBase* provider ) { m_model->registerProvider( scope, type, provider ); } bool QuickOpenPlugin::removeProvider( KDevelop::QuickOpenDataProviderBase* provider ) { m_model->removeProvider( provider ); return true; } void QuickOpenPlugin::quickOpenDeclaration() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } decl->activateSpecialization(); IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } QWidget* QuickOpenPlugin::specialObjectNavigationWidget() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return nullptr; QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QWidget* w = language->specialLanguageObjectNavigationWidget(url, KTextEditor::Cursor(view->cursorPosition()) ); if(w) return w; } return nullptr; } QPair QuickOpenPlugin::specialObjectJumpPosition() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return qMakePair(QUrl(), KTextEditor::Cursor()); QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QPair pos = language->specialLanguageObjectJumpCursor(url, KTextEditor::Cursor(view->cursorPosition()) ); if(pos.second.isValid()) { return pos; } } return qMakePair(QUrl(), KTextEditor::Cursor::invalid()); } bool QuickOpenPlugin::jumpToSpecialObject() { QPair pos = specialObjectJumpPosition(); if(pos.second.isValid()) { if(pos.first.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for special language object"; return false; } ICore::self()->documentController()->openDocument(pos.first, pos.second); return true; } return false; } void QuickOpenPlugin::quickOpenDefinition() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(FunctionDefinition* def = FunctionDefinition::definition(decl)) { def->activateSpecialization(); u = def->url(); c = def->rangeInCurrentRevision().start(); }else{ qCDebug(PLUGIN_QUICKOPEN) << "Found no definition for declaration"; decl->activateSpecialization(); } if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } bool QuickOpenPlugin::freeModel() { if(m_currentWidgetHandler) delete m_currentWidgetHandler; m_currentWidgetHandler = nullptr; return true; } void QuickOpenPlugin::nextFunction() { jumpToNearestFunction(NextFunction); } void QuickOpenPlugin::previousFunction() { jumpToNearestFunction(PreviousFunction); } void QuickOpenPlugin::jumpToNearestFunction(QuickOpenPlugin::FunctionJumpDirection direction) { IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } QList items; OutlineFilter filter(items, OutlineFilter::Functions); DUChainUtils::collectItems( context, filter ); CursorInRevision cursor = context->transformToLocalRevision(KTextEditor::Cursor(doc->cursorPosition())); if (!cursor.isValid()) return; Declaration *nearestDeclBefore = nullptr; int distanceBefore = INT_MIN; Declaration *nearestDeclAfter = nullptr; int distanceAfter = INT_MAX; for (int i = 0; i < items.count(); ++i) { Declaration *decl = items[i].m_item.data(); int distance = decl->range().start.line - cursor.line; if (distance < 0 && distance >= distanceBefore) { distanceBefore = distance; nearestDeclBefore = decl; } else if (distance > 0 && distance <= distanceAfter) { distanceAfter = distance; nearestDeclAfter = decl; } } CursorInRevision c = CursorInRevision::invalid(); if (direction == QuickOpenPlugin::NextFunction && nearestDeclAfter) c = nearestDeclAfter->range().start; else if (direction == QuickOpenPlugin::PreviousFunction && nearestDeclBefore) c = nearestDeclBefore->range().start; KTextEditor::Cursor textCursor = KTextEditor::Cursor::invalid(); if (c.isValid()) textCursor = context->transformFromLocalRevision(c); lock.unlock(); if (textCursor.isValid()) core()->documentController()->openDocument(doc->url(), textCursor); else qCDebug(PLUGIN_QUICKOPEN) << "No declaration to jump to"; } struct CreateOutlineDialog { CreateOutlineDialog() : dialog(nullptr), cursorDecl(nullptr), model(nullptr) { } void start() { if(!QuickOpenPlugin::self()->freeModel()) return; IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } model = new QuickOpenModel(nullptr); OutlineFilter filter(items); DUChainUtils::collectItems( context, filter ); if(noHtmlDestriptionInOutline) { for(int a = 0; a < items.size(); ++a) items[a].m_noHtmlDestription = true; } cursorDecl = cursorContextDeclaration(); model->registerProvider( QStringList(), QStringList(), new DeclarationListDataProvider(QuickOpenPlugin::self(), items, true) ); dialog = new QuickOpenWidgetDialog( i18n("Outline"), model, QStringList(), QStringList(), true ); model->setParent(dialog->widget()); } void finish() { //Select the declaration that contains the cursor if(cursorDecl && dialog) { int num = 0; foreach(const DUChainItem& item, items) { if(item.m_item.data() == cursorDecl) { QModelIndex index(model->index(num,0,QModelIndex())); // Need to invoke the scrolling later. If we did it now, then it wouldn't have any effect, // apparently because the widget internals aren't initialized yet properly (although we've // already called 'widget->show()'. auto list = dialog->widget()->ui.list; QMetaObject::invokeMethod(list, "setCurrentIndex", Qt::QueuedConnection, Q_ARG(QModelIndex, index)); QMetaObject::invokeMethod(list, "scrollTo", Qt::QueuedConnection, Q_ARG(QModelIndex, index), Q_ARG(QAbstractItemView::ScrollHint, QAbstractItemView::PositionAtCenter)); } ++num; } } } QPointer dialog; Declaration* cursorDecl; QList items; QuickOpenModel* model; }; class OutlineQuickopenWidgetCreator : public QuickOpenWidgetCreator { public: OutlineQuickopenWidgetCreator(QStringList /*scopes*/, QStringList /*items*/) : m_creator(nullptr) { } ~OutlineQuickopenWidgetCreator() override { delete m_creator; } QuickOpenWidget* createWidget() override { delete m_creator; m_creator = new CreateOutlineDialog; m_creator->start(); if(!m_creator->dialog) return nullptr; m_creator->dialog->deleteLater(); return m_creator->dialog->widget(); } void widgetShown() override { if(m_creator) { m_creator->finish(); delete m_creator; m_creator = nullptr; } } QString objectNameForLine() override { return QStringLiteral("Outline"); } CreateOutlineDialog* m_creator; }; void QuickOpenPlugin::quickOpenNavigateFunctions() { CreateOutlineDialog create; create.start(); if(!create.dialog) return; m_currentWidgetHandler = create.dialog; QuickOpenLineEdit* line = quickOpenLine(QStringLiteral("Outline")); if(!line) line = quickOpenLine(); if(line) { line->showWithWidget(create.dialog->widget()); create.dialog->deleteLater(); }else create.dialog->run(); create.finish(); } QuickOpenLineEdit::QuickOpenLineEdit(QuickOpenWidgetCreator* creator) : m_widget(nullptr), m_forceUpdate(false), m_widgetCreator(creator) { setMinimumWidth(200); setMaximumWidth(400); deactivate(); setDefaultText(i18n("Quick Open...")); setToolTip(i18n("Search for files, classes, functions and more," " allowing you to quickly navigate in your source code.")); setObjectName(m_widgetCreator->objectNameForLine()); setFocusPolicy(Qt::ClickFocus); } QuickOpenLineEdit::~QuickOpenLineEdit() { delete m_widget; delete m_widgetCreator; } bool QuickOpenLineEdit::insideThis(QObject* object) { while (object) { qCDebug(PLUGIN_QUICKOPEN) << object; if (object == this || object == m_widget) { return true; } object = object->parent(); } return false; } void QuickOpenLineEdit::widgetDestroyed(QObject* obj) { Q_UNUSED(obj); // need to use a queued connection here, because this function is called in ~QWidget! // => QuickOpenWidget instance is half-destructed => connections are not yet cleared // => clear() will trigger signals which will operate on the invalid QuickOpenWidget // So, just wait until properly destructed QMetaObject::invokeMethod(this, "deactivate", Qt::QueuedConnection); } void QuickOpenLineEdit::showWithWidget(QuickOpenWidget* widget) { connect(widget, &QuickOpenWidget::destroyed, this, &QuickOpenLineEdit::widgetDestroyed); qCDebug(PLUGIN_QUICKOPEN) << "storing widget" << widget; deactivate(); if(m_widget) { qCDebug(PLUGIN_QUICKOPEN) << "deleting" << m_widget; delete m_widget; } m_widget = widget; m_forceUpdate = true; setFocus(); } void QuickOpenLineEdit::focusInEvent(QFocusEvent* ev) { QLineEdit::focusInEvent(ev); // delete m_widget; qCDebug(PLUGIN_QUICKOPEN) << "got focus"; qCDebug(PLUGIN_QUICKOPEN) << "old widget" << m_widget << "force update:" << m_forceUpdate; if (m_widget && !m_forceUpdate) return; if (!m_forceUpdate && !QuickOpenPlugin::self()->freeModel()) { deactivate(); return; } m_forceUpdate = false; if(!m_widget) { m_widget = m_widgetCreator->createWidget(); if(!m_widget) { deactivate(); return; } } activate(); m_widget->showStandardButtons(false); m_widget->showSearchField(false); m_widget->setParent(nullptr, Qt::ToolTip); m_widget->setFocusPolicy(Qt::NoFocus); m_widget->setAlternativeSearchField(this); QuickOpenPlugin::self()->m_currentWidgetHandler = m_widget; connect(m_widget.data(), &QuickOpenWidget::ready, this, &QuickOpenLineEdit::deactivate); connect( m_widget.data(), &QuickOpenWidget::scopesChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeScopes ); connect( m_widget.data(), &QuickOpenWidget::itemsChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeItems ); Q_ASSERT(m_widget->ui.searchLine == this); m_widget->prepareShow(); QRect widgetGeometry = QRect(mapToGlobal(QPoint(0, height())), mapToGlobal(QPoint(width(), height() + 400))); widgetGeometry.setWidth(700); ///@todo Waste less space QRect screenGeom = QApplication::desktop()->screenGeometry(this); if (widgetGeometry.right() > screenGeom.right()) { widgetGeometry.moveRight(screenGeom.right()); } if (widgetGeometry.bottom() > screenGeom.bottom()) { widgetGeometry.moveBottom(mapToGlobal(QPoint(0, 0)).y()); } m_widget->setGeometry(widgetGeometry); m_widget->show(); m_widgetCreator->widgetShown(); } void QuickOpenLineEdit::hideEvent(QHideEvent* ev) { QWidget::hideEvent(ev); if(m_widget) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); // deactivate(); } bool QuickOpenLineEdit::eventFilter(QObject* obj, QEvent* e) { if (!m_widget) return false; switch (e->type()) { case QEvent::KeyPress: case QEvent::ShortcutOverride: if (static_cast(e)->key() == Qt::Key_Escape) { deactivate(); e->accept(); return true; } break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); break; // handle bug 260657 - "Outline menu doesn't follow main window on its move" case QEvent::Move: { if (QWidget* widget = qobject_cast(obj)) { // close the outline menu in case a parent widget moved if (widget->isAncestorOf(this)) { qCDebug(PLUGIN_QUICKOPEN) << "closing because of parent widget move"; deactivate(); } break; } } case QEvent::FocusIn: if (dynamic_cast(obj)) { QFocusEvent* focusEvent = dynamic_cast(e); Q_ASSERT(focusEvent); //Eat the focus event, keep the focus qCDebug(PLUGIN_QUICKOPEN) << "focus change" << "inside this: " << insideThis(obj) << "this" << this << "obj" << obj; if(obj == this) return false; qCDebug(PLUGIN_QUICKOPEN) << "reason" << focusEvent->reason(); if (focusEvent->reason() != Qt::MouseFocusReason && focusEvent->reason() != Qt::ActiveWindowFocusReason) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); return false; } if (!insideThis(obj)) deactivate(); } else if(obj != this) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); } break; default: break; } return false; } void QuickOpenLineEdit::activate() { qCDebug(PLUGIN_QUICKOPEN) << "activating"; setText(QString()); setStyleSheet(QString()); qApp->installEventFilter(this); } void QuickOpenLineEdit::deactivate() { qCDebug(PLUGIN_QUICKOPEN) << "deactivating"; clear(); if(m_widget || hasFocus()) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); if (m_widget) m_widget->deleteLater(); m_widget = nullptr; qApp->removeEventFilter(this); } void QuickOpenLineEdit::checkFocus() { qCDebug(PLUGIN_QUICKOPEN) << "checking focus" << m_widget; if(m_widget) { QWidget* focusWidget = QApplication::focusWidget(); bool focusWidgetInsideThis = focusWidget ? insideThis(focusWidget) : false; if(QApplication::focusWindow() && isVisible() && !isHidden() && (!focusWidget || (focusWidget && focusWidgetInsideThis))) { qCDebug(PLUGIN_QUICKOPEN) << "setting focus to line edit"; activateWindow(); setFocus(); } else { qCDebug(PLUGIN_QUICKOPEN) << "deactivating because check failed, focusWidget" << focusWidget << "insideThis" << focusWidgetInsideThis; deactivate(); } }else{ if (ICore::self()->documentController()->activeDocument()) ICore::self()->documentController()->activateDocument(ICore::self()->documentController()->activeDocument()); //Make sure the focus is somewehre else, even if there is no active document setEnabled(false); setEnabled(true); } } IQuickOpenLine* QuickOpenPlugin::createQuickOpenLine(const QStringList& scopes, const QStringList& type, IQuickOpen::QuickOpenType kind) { if(kind == Outline) return new QuickOpenLineEdit(new OutlineQuickopenWidgetCreator(scopes, type)); else return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(scopes, type)); } #include "quickopenplugin.moc" diff --git a/plugins/standardoutputview/standardoutputview.cpp b/plugins/standardoutputview/standardoutputview.cpp index a54d0cc1c8..31bae9ab1e 100644 --- a/plugins/standardoutputview/standardoutputview.cpp +++ b/plugins/standardoutputview/standardoutputview.cpp @@ -1,315 +1,313 @@ /* KDevelop Standard OutputView * * Copyright 2006-2007 Andreas Pakulat * Copyright 2007 Dukju Ahn * * 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. * * 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 "standardoutputview.h" #include "outputwidget.h" #include "toolviewdata.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include class OutputViewFactory : public KDevelop::IToolViewFactory{ public: OutputViewFactory(const ToolViewData* data): m_data(data) {} QWidget* create(QWidget *parent = nullptr) override { return new OutputWidget( parent, m_data ); } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } void viewCreated( Sublime::View* view ) override { m_data->views << view; } QString id() const override { //NOTE: id must be unique, see e.g. https://bugs.kde.org/show_bug.cgi?id=287093 return "org.kdevelop.OutputView." + QString::number(m_data->toolViewId); } private: const ToolViewData *m_data; }; StandardOutputView::StandardOutputView(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevstandardoutputview"), parent) { - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IOutputView ) - setXMLFile(QStringLiteral("kdevstandardoutputview.rc")); connect(KDevelop::ICore::self()->uiController()->controller(), &Sublime::Controller::aboutToRemoveView, this, &StandardOutputView::removeSublimeView); } void StandardOutputView::removeSublimeView( Sublime::View* v ) { foreach( ToolViewData* d, toolviews ) { if( d->views.contains(v) ) { if( d->views.count() == 1 ) { toolviews.remove( d->toolViewId ); ids.removeAll( d->toolViewId ); delete d; } else { d->views.removeAll(v); } } } } StandardOutputView::~StandardOutputView() { } int StandardOutputView::standardToolView( KDevelop::IOutputView::StandardToolView view ) { if( standardViews.contains( view ) ) { return standardViews.value( view ); } int ret = -1; switch( view ) { case KDevelop::IOutputView::BuildView: { ret = registerToolView( i18nc("@title:window", "Build"), KDevelop::IOutputView::HistoryView, QIcon::fromTheme(QStringLiteral("run-build")), KDevelop::IOutputView::AddFilterAction ); break; } case KDevelop::IOutputView::RunView: { ret = registerToolView( i18nc("@title:window", "Run"), KDevelop::IOutputView::MultipleView, QIcon::fromTheme(QStringLiteral("system-run")), KDevelop::IOutputView::AddFilterAction ); break; } case KDevelop::IOutputView::DebugView: { ret = registerToolView( i18nc("@title:window", "Debug"), KDevelop::IOutputView::MultipleView, QIcon::fromTheme(QStringLiteral("debug-step-into")), KDevelop::IOutputView::AddFilterAction ); break; } case KDevelop::IOutputView::TestView: { ret = registerToolView( i18nc("@title:window", "Test"), KDevelop::IOutputView::HistoryView, QIcon::fromTheme(QStringLiteral("system-run"))); break; } case KDevelop::IOutputView::VcsView: { ret = registerToolView( i18nc("@title:window", "Version Control"), KDevelop::IOutputView::HistoryView, QIcon::fromTheme(QStringLiteral("system-run"))); break; } } Q_ASSERT(ret != -1); standardViews[view] = ret; return ret; } int StandardOutputView::registerToolView( const QString& title, KDevelop::IOutputView::ViewType type, const QIcon& icon, Options option, const QList& actionList ) { // try to reuse existing toolview foreach( ToolViewData* d, toolviews ) { if ( d->type == type && d->title == title ) { return d->toolViewId; } } // register new tool view const int newid = ids.isEmpty() ? 0 : (ids.last() + 1); qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "Registering view" << title << "with type:" << type << "id:" << newid; ToolViewData* tvdata = new ToolViewData( this ); tvdata->toolViewId = newid; tvdata->type = type; tvdata->title = title; tvdata->icon = icon; tvdata->plugin = this; tvdata->option = option; tvdata->actionList = actionList; core()->uiController()->addToolView( title, new OutputViewFactory( tvdata ) ); ids << newid; toolviews[newid] = tvdata; return newid; } int StandardOutputView::registerOutputInToolView( int toolViewId, const QString& title, KDevelop::IOutputView::Behaviours behaviour ) { if( !toolviews.contains( toolViewId ) ) return -1; int newid; if( ids.isEmpty() ) { newid = 0; } else { newid = ids.last()+1; } ids << newid; toolviews.value( toolViewId )->addOutput( newid, title, behaviour ); return newid; } void StandardOutputView::raiseOutput(int outputId) { foreach( int _id, toolviews.keys() ) { if( toolviews.value( _id )->outputdata.contains( outputId ) ) { foreach( Sublime::View* v, toolviews.value( _id )->views ) { if( v->hasWidget() ) { OutputWidget* w = qobject_cast( v->widget() ); w->raiseOutput( outputId ); v->requestRaise(); } } } } } void StandardOutputView::setModel( int outputId, QAbstractItemModel* model ) { int tvid = -1; foreach( int _id, toolviews.keys() ) { if( toolviews.value( _id )->outputdata.contains( outputId ) ) { tvid = _id; break; } } if( tvid == -1 ) qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "Trying to set model on unknown view-id:" << outputId; else { toolviews.value( tvid )->outputdata.value( outputId )->setModel( model ); } } void StandardOutputView::setDelegate( int outputId, QAbstractItemDelegate* delegate ) { int tvid = -1; foreach( int _id, toolviews.keys() ) { if( toolviews.value( _id )->outputdata.contains( outputId ) ) { tvid = _id; break; } } if( tvid == -1 ) qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "Trying to set model on unknown view-id:" << outputId; else { toolviews.value( tvid )->outputdata.value( outputId )->setDelegate( delegate ); } } void StandardOutputView::removeToolView( int toolviewId ) { if( toolviews.contains(toolviewId) ) { ToolViewData* td = toolviews.value(toolviewId); foreach( Sublime::View* view, td->views ) { if( view->hasWidget() ) { OutputWidget* outputWidget = qobject_cast( view->widget() ); foreach( int outid, td->outputdata.keys() ) { outputWidget->removeOutput( outid ); } } foreach( Sublime::Area* area, KDevelop::ICore::self()->uiController()->controller()->allAreas() ) { area->removeToolView( view ); } } delete td; toolviews.remove(toolviewId); emit toolViewRemoved(toolviewId); } } OutputWidget* StandardOutputView::outputWidgetForId( int outputId ) const { foreach( ToolViewData* td, toolviews ) { if( td->outputdata.contains( outputId ) ) { foreach( Sublime::View* view, td->views ) { if( view->hasWidget() ) return qobject_cast( view->widget() ); } } } return nullptr; } void StandardOutputView::scrollOutputTo( int outputId, const QModelIndex& idx ) { OutputWidget* widget = outputWidgetForId( outputId ); if( widget ) widget->scrollToIndex( idx ); } void StandardOutputView::removeOutput( int outputId ) { foreach( ToolViewData* td, toolviews ) { if( td->outputdata.contains( outputId ) ) { foreach( Sublime::View* view, td->views ) { if( view->hasWidget() ) qobject_cast( view->widget() )->removeOutput( outputId ); } td->outputdata.remove( outputId ); } } } void StandardOutputView::setTitle(int outputId, const QString& title) { outputWidgetForId(outputId)->setTitle(outputId, title); } diff --git a/plugins/subversion/kdevsvnplugin.cpp b/plugins/subversion/kdevsvnplugin.cpp index 00d201d24b..71fb3fef44 100644 --- a/plugins/subversion/kdevsvnplugin.cpp +++ b/plugins/subversion/kdevsvnplugin.cpp @@ -1,531 +1,528 @@ /*************************************************************************** * Copyright 2007 Dukju Ahn * * Copyright 2008 Andreas Pakulat * * * * 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 "kdevsvnplugin.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 "kdevsvncpp/apr.hpp" #include "svncommitjob.h" #include "svnstatusjob.h" #include "svnaddjob.h" #include "svnrevertjob.h" #include "svnremovejob.h" #include "svnupdatejob.h" #include "svninfojob.h" #include "svndiffjob.h" #include "svncopyjob.h" #include "svnmovejob.h" #include "svnlogjob.h" #include "svnblamejob.h" #include "svnimportjob.h" #include "svncheckoutjob.h" #include "svnimportmetadatawidget.h" #include "svncheckoutmetadatawidget.h" #include #include #include "svnlocationwidget.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_SVN, "kdevplatform.plugins.svn") K_PLUGIN_FACTORY_WITH_JSON(KDevSvnFactory, "kdevsubversion.json", registerPlugin();) KDevSvnPlugin::KDevSvnPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevsubversion"), parent) , m_common(new KDevelop::VcsPluginHelper(this, this)) , copy_action( nullptr ) , move_action( nullptr ) , m_jobQueue(new ThreadWeaver::Queue(this)) { - KDEV_USE_EXTENSION_INTERFACE(KDevelop::IBasicVersionControl) - KDEV_USE_EXTENSION_INTERFACE(KDevelop::ICentralizedVersionControl) - qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); } KDevSvnPlugin::~KDevSvnPlugin() { } bool KDevSvnPlugin::isVersionControlled(const QUrl &localLocation) { ///TODO: also check this in the other functions? if (!localLocation.isValid()) { return false; } SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); if (job->exec()) { QVariant result = job->fetchResults(); if (result.isValid()) { SvnInfoHolder h = result.value(); return !h.name.isEmpty(); } } else { qCDebug(PLUGIN_SVN) << "Couldn't execute job"; } return false; } KDevelop::VcsJob* KDevSvnPlugin::repositoryLocation(const QUrl &localLocation) { SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); job->setProvideInformation(SvnInfoJob::RepoUrlOnly); return job; } KDevelop::VcsJob* KDevSvnPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode mode) { SvnStatusJob* job = new SvnStatusJob(this); job->setLocations(localLocations); job->setRecursive((mode == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnAddJob* job = new SvnAddJob(this); job->setLocations(localLocations); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::remove(const QList& localLocations) { SvnRemoveJob* job = new SvnRemoveJob(this); job->setLocations(localLocations); return job; } KDevelop::VcsJob* KDevSvnPlugin::edit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::unedit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::localRevision(const QUrl &localLocation, KDevelop::VcsRevision::RevisionType type) { SvnInfoJob* job = new SvnInfoJob(this); job->setLocation(localLocation); job->setProvideInformation(SvnInfoJob::RevisionOnly); job->setProvideRevisionType(type); return job; } KDevelop::VcsJob* KDevSvnPlugin::copy(const QUrl &localLocationSrc, const QUrl& localLocationDstn) { SvnCopyJob* job = new SvnCopyJob(this); job->setSourceLocation(localLocationSrc); job->setDestinationLocation(localLocationDstn); return job; } KDevelop::VcsJob* KDevSvnPlugin::move(const QUrl &localLocationSrc, const QUrl& localLocationDst) { SvnMoveJob* job = new SvnMoveJob(this); job->setSourceLocation(localLocationSrc); job->setDestinationLocation(localLocationDst); return job; } KDevelop::VcsJob* KDevSvnPlugin::revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnRevertJob* job = new SvnRevertJob(this); job->setLocations(localLocations); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnUpdateJob* job = new SvnUpdateJob(this); job->setLocations(localLocations); job->setRevision(rev); job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnCommitJob* job = new SvnCommitJob(this); qCDebug(PLUGIN_SVN) << "Committing locations:" << localLocations << endl; job->setUrls(localLocations); job->setCommitMessage(message) ; job->setRecursive((recursion == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::diff(const QUrl &fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type diffType, KDevelop::IBasicVersionControl::RecursionMode recurse) { KDevelop::VcsLocation loc(fileOrDirectory); return diff2(loc, loc, srcRevision, dstRevision, diffType, recurse); } KDevelop::VcsJob* KDevSvnPlugin::diff2(const KDevelop::VcsLocation& src, const KDevelop::VcsLocation& dst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type diffType, KDevelop::IBasicVersionControl::RecursionMode recurse) { SvnDiffJob* job = new SvnDiffJob(this); job->setSource(src); job->setDestination(dst); job->setSrcRevision(srcRevision); job->setDstRevision(dstRevision); job->setDiffType(diffType); job->setRecursive((recurse == KDevelop::IBasicVersionControl::Recursive)); return job; } KDevelop::VcsJob* KDevSvnPlugin::log(const QUrl &localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) { SvnLogJob* job = new SvnLogJob(this); job->setLocation(localLocation); job->setStartRevision(rev); job->setLimit(limit); return job; } KDevelop::VcsJob* KDevSvnPlugin::log(const QUrl &localLocation, const KDevelop::VcsRevision& startRev, const KDevelop::VcsRevision& endRev) { SvnLogJob* job = new SvnLogJob(this); job->setLocation(localLocation); job->setStartRevision(startRev); job->setEndRevision(endRev); return job; } KDevelop::VcsJob* KDevSvnPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision& rev) { SvnBlameJob* job = new SvnBlameJob(this); job->setLocation(localLocation); job->setEndRevision(rev); return job; } KDevelop::VcsJob* KDevSvnPlugin::merge(const KDevelop::VcsLocation& localOrRepoLocationSrc, const KDevelop::VcsLocation& localOrRepoLocationDst, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, const QUrl &localLocation) { // TODO implement merge Q_UNUSED(localOrRepoLocationSrc) Q_UNUSED(localOrRepoLocationDst) Q_UNUSED(srcRevision) Q_UNUSED(dstRevision) Q_UNUSED(localLocation) return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::resolve(const QList& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsJob* KDevSvnPlugin::import(const QString & commitMessage, const QUrl &sourceDirectory, const KDevelop::VcsLocation & destinationRepository) { SvnImportJob* job = new SvnImportJob(this); job->setMapping(sourceDirectory, destinationRepository); job->setMessage(commitMessage); return job; } KDevelop::VcsJob* KDevSvnPlugin::createWorkingCopy(const KDevelop::VcsLocation & sourceRepository, const QUrl &destinationDirectory, KDevelop::IBasicVersionControl::RecursionMode recursion) { SvnCheckoutJob* job = new SvnCheckoutJob(this); job->setMapping(sourceRepository, destinationDirectory, recursion); return job; } KDevelop::ContextMenuExtension KDevSvnPlugin::contextMenuExtension(KDevelop::Context* context) { m_common->setupFromContext(context); const QList & ctxUrlList = m_common->contextUrlList(); bool hasVersionControlledEntries = false; foreach(const QUrl &url, ctxUrlList) { if (isVersionControlled(url) || isVersionControlled(KIO::upUrl(url))) { hasVersionControlledEntries = true; break; } } qCDebug(PLUGIN_SVN) << "version controlled?" << hasVersionControlledEntries; if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context); QMenu* svnmenu= m_common->commonActions(); svnmenu->addSeparator(); if( !copy_action ) { copy_action = new QAction(i18n("Copy..."), this); connect(copy_action, SIGNAL(triggered()), this, SLOT(ctxCopy())); } svnmenu->addAction(copy_action); if( !move_action ) { move_action = new QAction(i18n("Move..."), this); connect(move_action, SIGNAL(triggered()), this, SLOT(ctxMove())); } svnmenu->addAction(move_action); KDevelop::ContextMenuExtension menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::VcsGroup, svnmenu->menuAction()); return menuExt; } void KDevSvnPlugin::ctxInfo() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } void KDevSvnPlugin::ctxStatus() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() > 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } void KDevSvnPlugin::ctxCopy() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() > 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QUrl source = ctxUrlList.first(); if (source.isLocalFile()) { QUrl dir = source; bool isFile = QFileInfo(source.toLocalFile()).isFile(); if (isFile) { dir = dir.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash); } KUrlRequesterDialog dlg(dir, i18n("Destination file/directory"), nullptr); if (isFile) { dlg.urlRequester()->setMode(KFile::File | KFile::Directory | KFile::LocalOnly); } else { dlg.urlRequester()->setMode(KFile::Directory | KFile::LocalOnly); } if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(copy(source, dlg.selectedUrl())); } } else { KMessageBox::error(nullptr, i18n("Copying only works on local files")); return; } } void KDevSvnPlugin::ctxMove() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QUrl source = ctxUrlList.first(); if (source.isLocalFile()) { QUrl dir = source; bool isFile = QFileInfo(source.toLocalFile()).isFile(); if (isFile) { dir = source.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash); } KUrlRequesterDialog dlg(dir, i18n("Destination file/directory"), nullptr); if (isFile) { dlg.urlRequester()->setMode(KFile::File | KFile::Directory | KFile::LocalOnly); } else { dlg.urlRequester()->setMode(KFile::Directory | KFile::LocalOnly); } if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(move(source, dlg.selectedUrl())); } } else { KMessageBox::error(nullptr, i18n("Moving only works on local files/dirs")); return; } } void KDevSvnPlugin::ctxCat() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } } QString KDevSvnPlugin::name() const { return i18n("Subversion"); } KDevelop::VcsImportMetadataWidget* KDevSvnPlugin::createImportMetadataWidget(QWidget* parent) { return new SvnImportMetadataWidget(parent); } void KDevSvnPlugin::ctxImport() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QDialog dlg; dlg.setWindowTitle(i18n("Import into Subversion repository")); SvnImportMetadataWidget* widget = new SvnImportMetadataWidget(&dlg); widget->setSourceLocation(KDevelop::VcsLocation(ctxUrlList.first())); widget->setSourceLocationEditable(false); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto layout = new QVBoxLayout(); dlg.setLayout(layout); layout->addWidget(widget); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(import(widget->message(), widget->source(), widget->destination())); } } void KDevSvnPlugin::ctxCheckout() { QList const & ctxUrlList = m_common->contextUrlList(); if (ctxUrlList.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return; } QDialog dlg; dlg.setWindowTitle(i18n("Checkout from Subversion repository")); SvnCheckoutMetadataWidget* widget = new SvnCheckoutMetadataWidget(&dlg); QUrl tmp = KIO::upUrl(ctxUrlList.first()); widget->setDestinationLocation(tmp); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto layout = new QVBoxLayout(); dlg.setLayout(layout); layout->addWidget(widget); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted) { KDevelop::ICore::self()->runController()->registerJob(createWorkingCopy(widget->source(), widget->destination(), widget->recursionMode())); } } KDevelop::VcsLocationWidget* KDevSvnPlugin::vcsLocation(QWidget* parent) const { return new SvnLocationWidget(parent); } ThreadWeaver::Queue* KDevSvnPlugin::jobQueue() const { return m_jobQueue; } #include "kdevsvnplugin.moc" diff --git a/project/abstractfilemanagerplugin.cpp b/project/abstractfilemanagerplugin.cpp index 9eaf1fed05..1f31692799 100644 --- a/project/abstractfilemanagerplugin.cpp +++ b/project/abstractfilemanagerplugin.cpp @@ -1,659 +1,657 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2010-2012 Milian Wolff * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "abstractfilemanagerplugin.h" #include "filemanagerlistjob.h" #include "projectmodel.h" #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include "projectfiltermanager.h" #include "debug.h" #define ifDebug(x) using namespace KDevelop; //BEGIN Helper namespace { /** * Returns the parent folder item for a given item or the project root item if there is no parent. */ ProjectFolderItem* getParentFolder(ProjectBaseItem* item) { if ( item->parent() ) { return static_cast(item->parent()); } else { return item->project()->projectItem(); } } } //END Helper //BEGIN Private struct AbstractFileManagerPlugin::Private { explicit Private(AbstractFileManagerPlugin* qq) : q(qq) { } AbstractFileManagerPlugin* q; /** * The just returned must be started in one way or another for this method * to have any affect. The job will then auto-delete itself upon completion. */ KIO::Job* eventuallyReadFolder( ProjectFolderItem* item ) Q_REQUIRED_RESULT; void addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries); void deleted(const QString &path); void created(const QString &path); void projectClosing(IProject* project); void jobFinished(KJob* job); /// Stops watching the given folder for changes, only useful for local files. void stopWatcher(ProjectFolderItem* folder); /// Continues watching the given folder for changes. void continueWatcher(ProjectFolderItem* folder); /// Common renaming function. bool rename(ProjectBaseItem* item, const Path& newPath); void removeFolder(ProjectFolderItem* folder); QHash m_watchers; QHash > m_projectJobs; QVector m_stoppedFolders; ProjectFilterManager m_filters; }; void AbstractFileManagerPlugin::Private::projectClosing(IProject* project) { if ( m_projectJobs.contains(project) ) { // make sure the import job does not live longer than the project // see also addLotsOfFiles test foreach( FileManagerListJob* job, m_projectJobs[project] ) { qCDebug(FILEMANAGER) << "killing project job:" << job; job->abort(); } m_projectJobs.remove(project); } delete m_watchers.take(project); m_filters.remove(project); } KIO::Job* AbstractFileManagerPlugin::Private::eventuallyReadFolder( ProjectFolderItem* item ) { FileManagerListJob* listJob = new FileManagerListJob( item ); m_projectJobs[ item->project() ] << listJob; qCDebug(FILEMANAGER) << "adding job" << listJob << item << item->path() << "for project" << item->project(); q->connect( listJob, &FileManagerListJob::finished, q, [&] (KJob* job) { jobFinished(job); } ); q->connect( listJob, &FileManagerListJob::entries, q, [&] (FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { addJobItems(job, baseItem, entries); } ); return listJob; } void AbstractFileManagerPlugin::Private::jobFinished(KJob* job) { FileManagerListJob* gmlJob = qobject_cast(job); if (gmlJob) { ifDebug(qCDebug(FILEMANAGER) << job << gmlJob << gmlJob->item();) m_projectJobs[ gmlJob->item()->project() ].removeOne( gmlJob ); } else { // job emitted its finished signal from its destructor // ensure we don't keep a dangling point in our list foreach (auto jobs, m_projectJobs) { if (jobs.removeOne(reinterpret_cast(job))) { break; } } } } void AbstractFileManagerPlugin::Private::addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { if ( entries.empty() ) { return; } qCDebug(FILEMANAGER) << "reading entries of" << baseItem->path(); // build lists of valid files and folders with paths relative to the project folder Path::List files; Path::List folders; foreach ( const KIO::UDSEntry& entry, entries ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if (name == QLatin1String(".") || name == QLatin1String("..")) { continue; } Path path(baseItem->path(), name); if ( !q->isValid( path, entry.isDir(), baseItem->project() ) ) { continue; } else { if ( entry.isDir() ) { if( entry.isLink() ) { const Path linkedPath = baseItem->path().cd(entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST )); // make sure we don't end in an infinite loop if( linkedPath.isParentOf( baseItem->project()->path() ) || baseItem->project()->path().isParentOf( linkedPath ) || linkedPath == baseItem->project()->path() ) { continue; } } folders << path; } else { files << path; } } } ifDebug(qCDebug(FILEMANAGER) << "valid folders:" << folders;) ifDebug(qCDebug(FILEMANAGER) << "valid files:" << files;) // remove obsolete rows for ( int j = 0; j < baseItem->rowCount(); ++j ) { if ( ProjectFolderItem* f = baseItem->child(j)->folder() ) { // check if this is still a valid folder int index = folders.indexOf( f->path() ); if ( index == -1 ) { // folder got removed or is now invalid removeFolder(f); --j; } else { // this folder already exists in the view folders.remove( index ); // no need to add this item, but we still want to recurse into it job->addSubDir( f ); emit q->reloadedFolderItem( f ); } } else if ( ProjectFileItem* f = baseItem->child(j)->file() ) { // check if this is still a valid file int index = files.indexOf( f->path() ); if ( index == -1 ) { // file got removed or is now invalid ifDebug(qCDebug(FILEMANAGER) << "removing file:" << f << f->path();) baseItem->removeRow( j ); --j; } else { // this file already exists in the view files.remove( index ); emit q->reloadedFileItem( f ); } } } // add new rows foreach ( const Path& path, files ) { ProjectFileItem* file = q->createFileItem( baseItem->project(), path, baseItem ); if (file) { emit q->fileAdded( file ); } } foreach ( const Path& path, folders ) { ProjectFolderItem* folder = q->createFolderItem( baseItem->project(), path, baseItem ); if (folder) { emit q->folderAdded( folder ); job->addSubDir( folder ); } } } void AbstractFileManagerPlugin::Private::created(const QString &path_) { qCDebug(FILEMANAGER) << "created:" << path_; QFileInfo info(path_); ///FIXME: share memory with parent const Path path(path_); const IndexedString indexedPath(path.pathOrUrl()); const IndexedString indexedParent(path.parent().pathOrUrl()); foreach ( IProject* p, m_watchers.keys() ) { if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } if ( !q->isValid(path, info.isDir(), p) ) { continue; } if ( info.isDir() ) { bool found = false; foreach ( ProjectFolderItem* folder, p->foldersForPath(indexedPath) ) { // exists already in this project, happens e.g. when we restart the dirwatcher // or if we delete and remove folders consecutively https://bugs.kde.org/show_bug.cgi?id=260741 qCDebug(FILEMANAGER) << "force reload of" << path << folder; auto job = eventuallyReadFolder( folder ); job->start(); found = true; } if ( found ) { continue; } } else if (!p->filesForPath(indexedPath).isEmpty()) { // also gets triggered for kate's backup files continue; } foreach ( ProjectFolderItem* parentItem, p->foldersForPath(indexedParent) ) { if ( info.isDir() ) { ProjectFolderItem* folder = q->createFolderItem( p, path, parentItem ); if (folder) { emit q->folderAdded( folder ); auto job = eventuallyReadFolder( folder ); job->start(); } } else { ProjectFileItem* file = q->createFileItem( p, path, parentItem ); if (file) { emit q->fileAdded( file ); } } } } } void AbstractFileManagerPlugin::Private::deleted(const QString &path_) { if ( QFile::exists(path_) ) { // stopDirScan... return; } // ensure that the path is not inside a stopped folder foreach(const QString& folder, m_stoppedFolders) { if (path_.startsWith(folder)) { return; } } qCDebug(FILEMANAGER) << "deleted:" << path_; const Path path(path_); const IndexedString indexed(path.pathOrUrl()); foreach ( IProject* p, m_watchers.keys() ) { if (path == p->path()) { KMessageBox::error(qApp->activeWindow(), i18n("The base folder of project %1" " got deleted or moved outside of KDevelop.\n" "The project has to be closed.", p->name()), i18n("Project Folder Deleted") ); ICore::self()->projectController()->closeProject(p); continue; } if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } foreach ( ProjectFolderItem* item, p->foldersForPath(indexed) ) { removeFolder(item); } foreach ( ProjectFileItem* item, p->filesForPath(indexed) ) { emit q->fileRemoved(item); ifDebug(qCDebug(FILEMANAGER) << "removing file" << item;) item->parent()->removeRow(item->row()); } } } bool AbstractFileManagerPlugin::Private::rename(ProjectBaseItem* item, const Path& newPath) { if ( !q->isValid(newPath, true, item->project()) ) { int cancel = KMessageBox::warningContinueCancel( qApp->activeWindow(), i18n("You tried to rename '%1' to '%2', but the latter is filtered and will be hidden.\n" "Do you want to continue?", item->text(), newPath.lastPathSegment()), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("GenericManagerRenameToFiltered") ); if ( cancel == KMessageBox::Cancel ) { return false; } } foreach ( ProjectFolderItem* parent, item->project()->foldersForPath(IndexedString(newPath.parent().pathOrUrl())) ) { if ( parent->folder() ) { stopWatcher(parent); const Path source = item->path(); bool success = renameUrl( item->project(), source.toUrl(), newPath.toUrl() ); if ( success ) { item->setPath( newPath ); item->parent()->takeRow( item->row() ); parent->appendRow( item ); if (item->file()) { emit q->fileRenamed(source, item->file()); } else { Q_ASSERT(item->folder()); emit q->folderRenamed(source, item->folder()); } } continueWatcher(parent); return success; } } return false; } void AbstractFileManagerPlugin::Private::stopWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->stopDirScan(path); m_stoppedFolders.append(path); } void AbstractFileManagerPlugin::Private::continueWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->restartDirScan(path); const int idx = m_stoppedFolders.indexOf(path); if (idx != -1) { m_stoppedFolders.remove(idx); } } bool isChildItem(ProjectBaseItem* parent, ProjectBaseItem* child) { do { if (child == parent) { return true; } child = child->parent(); } while(child); return false; } void AbstractFileManagerPlugin::Private::removeFolder(ProjectFolderItem* folder) { ifDebug(qCDebug(FILEMANAGER) << "removing folder:" << folder << folder->path();) foreach(FileManagerListJob* job, m_projectJobs[folder->project()]) { if (isChildItem(folder, job->item())) { qCDebug(FILEMANAGER) << "killing list job for removed folder" << job << folder->path(); job->abort(); Q_ASSERT(!m_projectJobs.value(folder->project()).contains(job)); } else { job->removeSubDir(folder); } } folder->parent()->removeRow( folder->row() ); } //END Private //BEGIN Plugin AbstractFileManagerPlugin::AbstractFileManagerPlugin( const QString& componentName, QObject *parent, const QVariantList & /*args*/ ) : IProjectFileManager(), IPlugin( componentName, parent ), d(new Private(this)) { - KDEV_USE_EXTENSION_INTERFACE( IProjectFileManager ) - connect(core()->projectController(), &IProjectController::projectClosing, this, [&] (IProject* project) { d->projectClosing(project); }); } AbstractFileManagerPlugin::~AbstractFileManagerPlugin() { delete d; } IProjectFileManager::Features AbstractFileManagerPlugin::features() const { return Features( Folders | Files ); } QList AbstractFileManagerPlugin::parse( ProjectFolderItem *item ) { // we are async, can't return anything here qCDebug(FILEMANAGER) << "note: parse will always return an empty list"; Q_UNUSED(item); return QList(); } ProjectFolderItem *AbstractFileManagerPlugin::import( IProject *project ) { ProjectFolderItem *projectRoot = createFolderItem( project, project->path(), nullptr ); emit folderAdded( projectRoot ); qCDebug(FILEMANAGER) << "imported new project" << project->name() << "at" << projectRoot->path(); ///TODO: check if this works for remote files when something gets changed through another KDE app if ( project->path().isLocalFile() ) { d->m_watchers[project] = new KDirWatch( project ); connect(d->m_watchers[project], &KDirWatch::created, this, [&] (const QString& path_) { d->created(path_); }); connect(d->m_watchers[project], &KDirWatch::deleted, this, [&] (const QString& path_) { d->deleted(path_); }); d->m_watchers[project]->addDir(project->path().toLocalFile(), KDirWatch::WatchSubDirs | KDirWatch:: WatchFiles ); } d->m_filters.add(project); return projectRoot; } KJob* AbstractFileManagerPlugin::createImportJob(ProjectFolderItem* item) { return d->eventuallyReadFolder(item); } bool AbstractFileManagerPlugin::reload( ProjectFolderItem* item ) { qCDebug(FILEMANAGER) << "reloading item" << item->path(); auto job = d->eventuallyReadFolder( item->folder() ); job->start(); return true; } ProjectFolderItem* AbstractFileManagerPlugin::addFolder( const Path& folder, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding folder" << folder << "to" << parent->path(); ProjectFolderItem* created = nullptr; d->stopWatcher(parent); if ( createFolder(folder.toUrl()) ) { created = createFolderItem( parent->project(), folder, parent ); if (created) { emit folderAdded(created); } } d->continueWatcher(parent); return created; } ProjectFileItem* AbstractFileManagerPlugin::addFile( const Path& file, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding file" << file << "to" << parent->path(); ProjectFileItem* created = nullptr; d->stopWatcher(parent); if ( createFile(file.toUrl()) ) { created = createFileItem( parent->project(), file, parent ); if (created) { emit fileAdded(created); } } d->continueWatcher(parent); return created; } bool AbstractFileManagerPlugin::renameFolder(ProjectFolderItem* folder, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a folder:" << folder->path() << newPath; return d->rename(folder, newPath); } bool AbstractFileManagerPlugin::renameFile(ProjectFileItem* file, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a file:" << file->path() << newPath; return d->rename(file, newPath); } bool AbstractFileManagerPlugin::removeFilesAndFolders(const QList &items) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* parent = getParentFolder(item); d->stopWatcher(parent); success &= removeUrl(parent->project(), item->path().toUrl(), true); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { Q_ASSERT(item->folder()); emit folderRemoved(item->folder()); } item->parent()->removeRow( item->row() ); } d->continueWatcher(parent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::moveFilesAndFolders(const QList< ProjectBaseItem* >& items, ProjectFolderItem* newParent) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* oldParent = getParentFolder(item); d->stopWatcher(oldParent); d->stopWatcher(newParent); const Path oldPath = item->path(); const Path newPath(newParent->path(), item->baseName()); success &= renameUrl(oldParent->project(), oldPath.toUrl(), newPath. toUrl()); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { emit folderRemoved(item->folder()); } oldParent->removeRow( item->row() ); KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(oldParent); d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::copyFilesAndFolders(const Path::List& items, ProjectFolderItem* newParent) { bool success = true; foreach(const Path& item, items) { d->stopWatcher(newParent); success &= copyUrl(newParent->project(), item.toUrl(), newParent->path().toUrl()); if ( success ) { KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::isValid( const Path& path, const bool isFolder, IProject* project ) const { return d->m_filters.isValid( path, isFolder, project ); } ProjectFileItem* AbstractFileManagerPlugin::createFileItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFileItem( project, path, parent ); } ProjectFolderItem* AbstractFileManagerPlugin::createFolderItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFolderItem( project, path, parent ); } KDirWatch* AbstractFileManagerPlugin::projectWatcher( IProject* project ) const { return d->m_watchers.value( project, nullptr ); } //END Plugin #include "moc_abstractfilemanagerplugin.cpp" diff --git a/shell/tests/nonguiinterfaceplugin.cpp b/shell/tests/nonguiinterfaceplugin.cpp index 727d2b7fb0..c94553e72d 100644 --- a/shell/tests/nonguiinterfaceplugin.cpp +++ b/shell/tests/nonguiinterfaceplugin.cpp @@ -1,35 +1,34 @@ /* * This file is part of KDevelop * * Copyright 2007 Hamish Rodda * * 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 "nonguiinterfaceplugin.h" #include K_PLUGIN_FACTORY_WITH_JSON(KDevProblemReporterFactory, "kdevnonguiinterfaceplugin.json", registerPlugin();) NonGuiInterfacePlugin::NonGuiInterfacePlugin( QObject* parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevnonguiinterfaceplugin"), parent ) { - KDEV_USE_EXTENSION_INTERFACE( ITestNonGuiInterface ) } #include "nonguiinterfaceplugin.moc" diff --git a/shell/tests/test_plugincontroller.cpp b/shell/tests/test_plugincontroller.cpp index eead965cf8..ffedc86cdd 100644 --- a/shell/tests/test_plugincontroller.cpp +++ b/shell/tests/test_plugincontroller.cpp @@ -1,108 +1,110 @@ /*************************************************************************** * Copyright 2008 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "test_plugincontroller.h" +#include "nonguiinterfaceplugin.h" +#include "testfilepaths.h" + #include #include #include #include #include #include #include "../core.h" #include "../plugincontroller.h" -#include "testfilepaths.h" - using namespace KDevelop; void TestPluginController::initTestCase() { qApp->addLibraryPath(QStringLiteral(TEST_PLUGIN_DIR)); AutoTestShell::init({QStringLiteral("kdevnonguiinterface")}); TestCore::initialize( Core::NoUi ); m_pluginCtrl = Core::self()->pluginControllerInternal(); } void TestPluginController::cleanupTestCase() { TestCore::shutdown(); } void TestPluginController::init() { } void TestPluginController::cleanup() { } void TestPluginController::pluginInfo() { IPlugin* plugin = m_pluginCtrl->loadPlugin( QStringLiteral("kdevnonguiinterface") ); QVERIFY(plugin); KPluginMetaData pluginInfo = m_pluginCtrl->pluginInfo(plugin); QCOMPARE(pluginInfo.pluginId(), QStringLiteral("kdevnonguiinterface")); } void TestPluginController::loadUnloadPlugin() { QSignalSpy spy(m_pluginCtrl, SIGNAL(pluginLoaded(KDevelop::IPlugin*))); QSignalSpy spyloading(m_pluginCtrl, SIGNAL(loadingPlugin(QString))); QVERIFY(spy.isValid()); QVERIFY(spyloading.isValid()); m_pluginCtrl->loadPlugin( QStringLiteral( "kdevnonguiinterface" ) ); QVERIFY( m_pluginCtrl->plugin( QStringLiteral( "kdevnonguiinterface" ) ) ); QCOMPARE(spy.size(), 1); QCOMPARE(spyloading.size(), 1); QList args = spyloading.takeFirst(); QCOMPARE( args.at(0).toString(), QStringLiteral( "kdevnonguiinterface" ) ); QSignalSpy spy2(m_pluginCtrl, SIGNAL(pluginUnloaded(KDevelop::IPlugin*)) ); QSignalSpy spy3(m_pluginCtrl, SIGNAL(unloadingPlugin(KDevelop::IPlugin*)) ); QVERIFY(spy2.isValid()); QVERIFY(spy3.isValid()); m_pluginCtrl->unloadPlugin( QStringLiteral("kdevnonguiinterface") ); QVERIFY( !m_pluginCtrl->plugin( QStringLiteral( "kdevnonguiinterface" ) ) ); QCOMPARE(spy2.size(), 1); QCOMPARE(spy3.size(), 1); } void TestPluginController::loadFromExtension() { IPlugin* plugin = m_pluginCtrl->pluginForExtension( QStringLiteral("org.kdevelop.ITestNonGuiInterface") ); QVERIFY( plugin ); - QCOMPARE( plugin->extensions(), QVector() << "org.kdevelop.ITestNonGuiInterface" ); + QVERIFY( plugin->inherits("org.kdevelop.ITestNonGuiInterface") ); + QVERIFY( plugin->extension()); } void TestPluginController::benchPluginForExtension() { QBENCHMARK { IPlugin* plugin = m_pluginCtrl->pluginForExtension( QStringLiteral("org.kdevelop.ITestNonGuiInterface") ); QVERIFY( plugin ); } } QTEST_MAIN( TestPluginController) diff --git a/shell/tests/test_projectcontroller.cpp b/shell/tests/test_projectcontroller.cpp index 92dc542447..a204040e71 100644 --- a/shell/tests/test_projectcontroller.cpp +++ b/shell/tests/test_projectcontroller.cpp @@ -1,575 +1,574 @@ /*************************************************************************** * Copyright 2008 Manuel Breugelmans * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "test_projectcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; Q_DECLARE_METATYPE(KDevelop::IProject*) namespace { class DialogProviderFake : public IProjectDialogProvider { Q_OBJECT public: DialogProviderFake() : m_reopen(true) {} ~DialogProviderFake() override {} bool m_reopen; public slots: QUrl askProjectConfigLocation(bool /*fetch*/, const QUrl& /*startUrl*/ = QUrl()) override { return QUrl(); } bool userWantsReopen() override { return m_reopen; } }; } /*! A Filemanager plugin that allows you to setup a file & directory structure */ class FakeFileManager : public IPlugin, public IProjectFileManager { Q_OBJECT Q_INTERFACES(KDevelop::IProjectFileManager) public: FakeFileManager(QObject*, const QVariantList&) : IPlugin(ICore::self()->aboutData().componentName(), Core::self()) { - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IProjectFileManager ) } FakeFileManager() : IPlugin(ICore::self()->aboutData().componentName(), Core::self()) { - KDEV_USE_EXTENSION_INTERFACE( KDevelop::IProjectFileManager ) } ~FakeFileManager() override {} Features features() const override { return IProjectFileManager::Files | IProjectFileManager::Folders; } QMap m_filesInFolder; // initialize QMap m_subFoldersInFolder; /*! Setup this manager such that @p folder contains @p file */ void addFileToFolder(const Path& folder, const Path& file) { if (!m_filesInFolder.contains(folder)) { m_filesInFolder[folder] = Path::List(); } m_filesInFolder[folder] << file; } /*! Setup this manager such that @p folder has @p subFolder */ void addSubFolderTo(const Path& folder, Path subFolder) { if (!m_subFoldersInFolder.contains(folder)) { m_subFoldersInFolder[folder] = Path::List(); } m_subFoldersInFolder[folder] << subFolder; } QList parse(ProjectFolderItem *dom) override { Path::List files = m_filesInFolder[dom->path()]; foreach (const Path& file, files) { new ProjectFileItem(dom->project(), file, dom); } Path::List folderPaths = m_subFoldersInFolder[dom->path()]; QList folders; foreach (const Path& folderPath, folderPaths) { folders << new ProjectFolderItem(dom->project(), folderPath, dom); } return folders; } ProjectFolderItem *import(IProject *project) override { ProjectFolderItem* it = new ProjectFolderItem(project, project->path()); return it; } ProjectFolderItem* addFolder(const Path& /*folder*/, ProjectFolderItem */*parent*/) override { return nullptr; } ProjectFileItem* addFile(const Path& /*file*/, ProjectFolderItem */*parent*/) override { return nullptr; } bool removeFilesAndFolders(const QList &/*items*/) override { return false; } bool moveFilesAndFolders(const QList< KDevelop::ProjectBaseItem* > &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; } bool copyFilesAndFolders(const Path::List &/*items*/, KDevelop::ProjectFolderItem* /*newParent*/) override { return false; } bool renameFile(ProjectFileItem* /*file*/, const Path& /*newPath*/) override { return false; } bool renameFolder(ProjectFolderItem* /*oldFolder*/, const Path& /*newPath*/ ) override { return false; } bool reload(ProjectFolderItem* /*item*/) override { return false; } }; class FakePluginController : public PluginController { Q_OBJECT public: FakePluginController(Core* core) : PluginController(core) , m_fakeFileManager(new FakeFileManager) { } IPlugin* pluginForExtension(const QString& extension, const QString& pluginName = {}, const QVariantMap& constraints = QVariantMap()) override { - if (extension == m_fakeFileManager->extensions().at(0)) + if (extension == qobject_interface_iid()) { return m_fakeFileManager; + } return PluginController::pluginForExtension(extension, pluginName, constraints); } private: FakeFileManager* m_fakeFileManager; }; ////////////////////// Fixture /////////////////////////////////////////////// void TestProjectController::initTestCase() { AutoTestShell::init(); TestCore* testCore = new TestCore; testCore->setPluginController( new FakePluginController(testCore) ); testCore->initialize(); qRegisterMetaType(); m_core = Core::self(); m_scratchDir = QDir(QDir::tempPath()); m_scratchDir.mkdir(QStringLiteral("prjctrltest")); m_scratchDir.cd(QStringLiteral("prjctrltest")); } void TestProjectController::cleanupTestCase() { TestCore::shutdown(); } void TestProjectController::init() { m_projName = QStringLiteral("foo"); m_projFilePath = writeProjectConfig(m_projName); m_projCtrl = m_core->projectControllerInternal(); m_tmpConfigs << m_projFilePath; m_projFolder = Path(m_scratchDir.absolutePath() + '/'); } void TestProjectController::cleanup() { // also close any opened projects as we do not get a clean fixture, // following tests should start off clean. foreach(IProject* p, m_projCtrl->projects()) { m_projCtrl->closeProject(p); } foreach(const Path &cfg, m_tmpConfigs) { QFile::remove(cfg.pathOrUrl()); } qDeleteAll(m_fileManagerGarbage); m_fileManagerGarbage.clear(); } ////////////////////// Commands ////////////////////////////////////////////// #define WAIT_FOR_OPEN_SIGNAL \ {\ QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*)));\ QVERIFY2(signal.wait(30000), "Timeout while waiting for opened signal");\ } void(0) void TestProjectController::openProject() { QSignalSpy* spy = createOpenedSpy(); QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName)); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 1); IProject* proj; assertProjectOpened(m_projName, proj);QVERIFY(proj); assertSpyCaughtProject(spy, proj); QCOMPARE(proj->projectFile(), m_projFilePath); QCOMPARE(proj->path(), Path(m_scratchDir.absolutePath()+'/')); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); } void TestProjectController::closeProject() { m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; IProject* proj = m_projCtrl->findProjectByName(m_projName); Q_ASSERT(proj); QSignalSpy* spy1 = createClosedSpy(); QSignalSpy* spy2 = createClosingSpy(); m_projCtrl->closeProject(proj); QVERIFY(!m_projCtrl->isProjectNameUsed(m_projName)); QCOMPARE(m_projCtrl->projectCount(), 0); assertProjectClosed(proj); assertSpyCaughtProject(spy1, proj); assertSpyCaughtProject(spy2, proj); } void TestProjectController::openCloseOpen() { m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; IProject* proj; assertProjectOpened(m_projName, proj); m_projCtrl->closeProject(proj); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); QCOMPARE(m_projCtrl->projectCount(), 1); assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::reopen() { m_projCtrl->setDialogProvider(new DialogProviderFake); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 1); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); IProject* proj; assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::reopenWhileLoading() { // Open the same project again while the first is still // loading. The second open request should be blocked. m_projCtrl->setDialogProvider(new DialogProviderFake); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); //m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; // wait a bit for a second signal, this should timeout QSignalSpy signal(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*))); QVERIFY2(!signal.wait(100), "Received 2 projectOpened signals."); QCOMPARE(m_projCtrl->projectCount(), 1); IProject* proj; assertProjectOpened(m_projName, proj); assertSpyCaughtProject(spy, proj); } void TestProjectController::openMultiple() { QString secondProj(QStringLiteral("bar")); Path secondCfgUrl = writeProjectConfig(secondProj); QSignalSpy* spy = createOpenedSpy(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; m_projCtrl->openProject(secondCfgUrl.toUrl()); WAIT_FOR_OPEN_SIGNAL; QCOMPARE(m_projCtrl->projectCount(), 2); IProject *proj1, *proj2; assertProjectOpened(m_projName, proj1); assertProjectOpened(secondProj, proj2); QVERIFY(m_projCtrl->isProjectNameUsed(m_projName)); QVERIFY(m_projCtrl->isProjectNameUsed(QStringLiteral("bar"))); QCOMPARE(spy->size(), 2); IProject* emittedProj1 = (*spy)[0][0].value(); IProject* emittedProj2 = (*spy)[1][0].value(); QCOMPARE(emittedProj1, proj1); QCOMPARE(emittedProj2, proj2); m_tmpConfigs << secondCfgUrl; } /*! Verify that the projectmodel contains a single project. Put this project's * ProjectFolderItem in the output parameter @p RootItem */ #define ASSERT_SINGLE_PROJECT_IN_MODEL(rootItem) \ {\ QCOMPARE(m_projCtrl->projectModel()->rowCount(), 1); \ QModelIndex projIndex = m_projCtrl->projectModel()->index(0,0); \ QVERIFY(projIndex.isValid()); \ ProjectBaseItem* i = m_projCtrl->projectModel()->itemFromIndex( projIndex ); \ QVERIFY(i); \ QVERIFY(i->folder()); \ rootItem = i->folder();\ } void(0) /*! Verify that the projectitem @p item has a single child item * named @p name with url @p url. @p subFolder is an output parameter * that contains the sub-folder projectitem. */ #define ASSERT_SINGLE_SUBFOLDER_IN(item, name, path__, subFolder) \ {\ QCOMPARE(item->rowCount(), 1);\ QCOMPARE(item->folderList().size(), 1);\ ProjectFolderItem* fo = item->folderList().at(0);\ QVERIFY(fo);\ QCOMPARE(fo->path(), path__);\ QCOMPARE(fo->folderName(), QStringLiteral(name));\ subFolder = fo;\ } void(0) #define ASSERT_SINGLE_FILE_IN(rootFolder, name, path__, fileItem)\ {\ QCOMPARE(rootFolder->rowCount(), 1);\ QCOMPARE(rootFolder->fileList().size(), 1);\ fileItem = rootFolder->fileList().at(0);\ QVERIFY(fileItem);\ QCOMPARE(fileItem->path(), path__);\ QCOMPARE(fileItem->fileName(), QStringLiteral(name));\ } void(0) // command void TestProjectController::emptyProject() { // verify that the project model contains a single top-level folder after loading // an empty project assertEmptyProjectModel(); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); Q_ASSERT(fileMng); proj->setManagerPlugin(fileMng); proj->reloadModel(); QTest::qWait(100); ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); // check that the project is empty QCOMPARE(rootFolder->rowCount(), 0); QCOMPARE(rootFolder->project()->name(), m_projName); QCOMPARE(rootFolder->path(), m_projFolder); } // command void TestProjectController::singleFile() { // verify that the project model contains a single file in the // top folder. First setup a FakeFileManager with this file m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); proj->setManagerPlugin(fileMng); Path filePath = Path(m_projFolder, QStringLiteral("foobar")); fileMng->addFileToFolder(m_projFolder, filePath); proj->reloadModel(); QTest::qWait(100); // NO signals for reload ... ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ProjectFileItem* fi; ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi); QCOMPARE(fi->rowCount(), 0); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_FILE_IN(rootFolder, "foobar", filePath, fi); } // command void TestProjectController::singleDirectory() { // verify that the project model contains a single folder in the // top folder. First setup a FakeFileManager with this folder m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); Path folderPath = Path(m_projFolder, QStringLiteral("foobar/")); FakeFileManager* fileMng = createFileManager(); fileMng->addSubFolderTo(m_projFolder, folderPath); proj->setManagerPlugin(fileMng); proj->reloadModel(); QTest::qWait(100); ProjectFolderItem* rootFolder; ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); // check that the project contains a single subfolder ProjectFolderItem* sub; ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); QCOMPARE(sub->rowCount(), 0); } // command void TestProjectController::fileInSubdirectory() { // verify that the project model contains a single file in a subfolder // First setup a FakeFileManager with this folder + file m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); Path folderPath = Path(m_projFolder, QStringLiteral("foobar/")); FakeFileManager* fileMng = createFileManager(); fileMng->addSubFolderTo(m_projFolder, folderPath); Path filePath = Path(folderPath, QStringLiteral("zoo")); fileMng->addFileToFolder(folderPath, filePath); proj->setManagerPlugin(fileMng); ProjectFolderItem* rootFolder = nullptr; ProjectFolderItem* sub = nullptr; ProjectFileItem* file = nullptr; proj->reloadModel(); QTest::qWait(100); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file); ASSERT_SINGLE_PROJECT_IN_MODEL(rootFolder); ASSERT_SINGLE_SUBFOLDER_IN(rootFolder, "foobar", folderPath, sub); ASSERT_SINGLE_FILE_IN(sub,"zoo",filePath,file); } void TestProjectController::prettyFileName_data() { QTest::addColumn("relativeFilePath"); QTest::newRow("basic") << "foobar.txt"; QTest::newRow("subfolder") << "sub/foobar.txt"; } void TestProjectController::prettyFileName() { QFETCH(QString, relativeFilePath); m_projCtrl->openProject(m_projFilePath.toUrl()); WAIT_FOR_OPEN_SIGNAL; Project* proj; assertProjectOpened(m_projName, (KDevelop::IProject*&)proj); FakeFileManager* fileMng = createFileManager(); proj->setManagerPlugin(fileMng); Path filePath = Path(m_projFolder, relativeFilePath); fileMng->addFileToFolder(m_projFolder, filePath); QCOMPARE(m_projCtrl->prettyFileName(filePath.toUrl(), ProjectController::FormattingOptions::FormatPlain), QString(m_projName + ':' + relativeFilePath)); } ////////////////////// Helpers /////////////////////////////////////////////// Path TestProjectController::writeProjectConfig(const QString& name) { Path configPath = Path(m_scratchDir.absolutePath() + '/' + name + ".kdev4"); QFile f(configPath.pathOrUrl()); f.open(QIODevice::WriteOnly); QTextStream str(&f); str << "[Project]\n" << "Name=" << name << "\n"; f.close(); return configPath; } ////////////////// Custom assertions ///////////////////////////////////////// void TestProjectController::assertProjectOpened(const QString& name, IProject*& proj) { QVERIFY(proj = m_projCtrl->findProjectByName(name)); QVERIFY(m_projCtrl->projects().contains(proj)); } void TestProjectController::assertSpyCaughtProject(QSignalSpy* spy, IProject* proj) { QCOMPARE(spy->size(), 1); IProject* emittedProj = (*spy)[0][0].value(); QCOMPARE(proj, emittedProj); } void TestProjectController::assertProjectClosed(IProject* proj) { IProject* p = m_projCtrl->findProjectByName(proj->name()); QVERIFY(p == nullptr); QVERIFY(!m_projCtrl->projects().contains(proj)); } void TestProjectController::assertEmptyProjectModel() { ProjectModel* m = m_projCtrl->projectModel(); Q_ASSERT(m); QCOMPARE(m->rowCount(), 0); } ///////////////////// Creation stuff ///////////////////////////////////////// QSignalSpy* TestProjectController::createOpenedSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectOpened(KDevelop::IProject*))); } QSignalSpy* TestProjectController::createClosedSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectClosed(KDevelop::IProject*))); } QSignalSpy* TestProjectController::createClosingSpy() { return new QSignalSpy(m_projCtrl, SIGNAL(projectClosing(KDevelop::IProject*))); } FakeFileManager* TestProjectController::createFileManager() { FakeFileManager* fileMng = new FakeFileManager; m_fileManagerGarbage << fileMng; return fileMng; } QTEST_MAIN(TestProjectController) #include "moc_test_projectcontroller.cpp" #include "test_projectcontroller.moc"