diff --git a/app/main.cpp b/app/main.cpp index 9255a300e6..df75fcd30f 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,826 +1,827 @@ /*************************************************************************** * Copyright 2003-2009 Alexander Dymo * * Copyright 2007 Ralf Habacker * * Copyright 2006-2007 Matt Rogers * * Copyright 2006-2007 Hamish Rodda * * Copyright 2005-2007 Adam Treat * * Copyright 2003-2007 Jens Dagerbo * * Copyright 2001-2002 Bernd Gehrmann * * Copyright 2001-2002 Matthias Hoelzer-Kluepfel * * Copyright 2003 Roberto Raggi * * Copyright 2010 Niko Sams * * Copyright 2015 Kevin Funk * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU 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 "config-kdevelop.h" #include "kdevelop_version.h" #include "urlinfo.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 "kdevideextension.h" #if KDEVELOP_SINGLE_APP #include "qtsingleapplication.h" #endif #include #ifdef Q_OS_MAC #include #endif using namespace KDevelop; namespace { #if KDEVELOP_SINGLE_APP QString serializeOpenFilesMessage(const QVector &infos) { QByteArray message; QDataStream stream(&message, QIODevice::WriteOnly); stream << QByteArrayLiteral("open"); stream << infos; return QString::fromLatin1(message.toHex()); } #endif void openFiles(const QVector& infos) { for (const UrlInfo& info : infos) { if (!ICore::self()->documentController()->openDocument(info.url, info.cursor)) { qWarning(APP) << i18n("Could not open %1", info.url.toDisplayString(QUrl::PreferLocalFile)); } } } } class KDevelopApplication: #if KDEVELOP_SINGLE_APP public SharedTools::QtSingleApplication #else public QApplication #endif { public: explicit KDevelopApplication(int &argc, char **argv, bool GUIenabled = true) #if KDEVELOP_SINGLE_APP : SharedTools::QtSingleApplication(QStringLiteral("KDevelop"), argc, argv) #else : QApplication(argc, argv, GUIenabled) #endif { Q_UNUSED(GUIenabled); connect(this, &QGuiApplication::saveStateRequest, this, &KDevelopApplication::saveState); } #if KDEVELOP_SINGLE_APP public Q_SLOTS: void remoteArguments(const QString &message, QObject *socket) { Q_UNUSED(socket); QByteArray ba = QByteArray::fromHex(message.toLatin1()); QDataStream stream(ba); QByteArray command; stream >> command; qCDebug(APP) << "Received remote command: " << command; if (command == "open") { QVector infos; stream >> infos; QVector files, directories; for (const auto& info : infos) if (info.isDirectory()) directories << info; else files << info; openFiles(files); for(const auto &urlinfo : directories) ICore::self()->projectController()->openProjectForUrl(urlinfo.url); } else { qCWarning(APP) << "Unknown remote command: " << command; } } void fileOpenRequested(const QString &file) { openFiles({UrlInfo(file)}); } #endif private Q_SLOTS: void saveState( QSessionManager& sm ) { if (KDevelop::Core::self() && KDevelop::Core::self()->sessionController()) { const auto activeSession = KDevelop::Core::self()->sessionController()->activeSession(); if (!activeSession) { qWarning(APP) << "No active session, can't save state"; return; } const QString x11SessionId = sm.sessionId() + QLatin1Char('_') + sm.sessionKey(); QString kdevelopSessionId = activeSession->id().toString(); sm.setRestartCommand({QCoreApplication::applicationFilePath(), "-session", x11SessionId, "-s", kdevelopSessionId}); } } }; /// Tries to find a session identified by @p data in @p sessions. /// The @p data may be either a session's name or a string-representation of its UUID. /// @return pointer to the session or NULL if nothing appropriate has been found static const KDevelop::SessionInfo* findSessionInList( const SessionInfos& sessions, const QString& data ) { // We won't search a session without input data, since that could lead to false-positives // with unnamed sessions if( data.isEmpty() ) return nullptr; for( auto it = sessions.constBegin(); it != sessions.constEnd(); ++it ) { if ( ( it->name == data ) || ( it->uuid.toString() == data ) ) { const KDevelop::SessionInfo& sessionRef = *it; return &sessionRef; } } return nullptr; } /// Tries to find sessions containing project @p projectUrl in @p sessions. static const KDevelop::SessionInfos findSessionsWithProject(const SessionInfos& sessions, const QUrl& projectUrl) { if (!projectUrl.isValid()) return {}; KDevelop::SessionInfos infos; for (auto it = sessions.constBegin(); it != sessions.constEnd(); ++it) { if (it->projects.contains(projectUrl)) { infos << *it; } } return infos; } /// Performs a DBus call to open the given @p files in the running kdev instance identified by @p pid /// Returns the exit status static int openFilesInRunningInstance(const QVector& files, qint64 pid) { const QString service = QStringLiteral("org.kdevelop.kdevelop-%1").arg(pid); QDBusInterface iface(service, QStringLiteral("/org/kdevelop/DocumentController"), QStringLiteral("org.kdevelop.DocumentController")); QStringList urls; bool errors_occured = false; for (const UrlInfo& file : files) { QDBusReply result = iface.call(QStringLiteral("openDocumentSimple"), file.url.toString(), file.cursor.line(), file.cursor.column()); if ( ! result.value() ) { QTextStream err(stderr); err << i18n("Could not open file '%1'.", file.url.toDisplayString(QUrl::PreferLocalFile)) << "\n"; errors_occured = true; } } // make the window visible QDBusMessage makeVisible = QDBusMessage::createMethodCall( service, QStringLiteral("/kdevelop/MainWindow"), QStringLiteral("org.kdevelop.MainWindow"), QStringLiteral("ensureVisible") ); QDBusConnection::sessionBus().asyncCall( makeVisible ); return errors_occured; } /// Performs a DBus call to open the given @p files in the running kdev instance identified by @p pid /// Returns the exit status static int openProjectInRunningInstance(const QVector& paths, qint64 pid) { const QString service = QStringLiteral("org.kdevelop.kdevelop-%1").arg(pid); QDBusInterface iface(service, QStringLiteral("/org/kdevelop/ProjectController"), QStringLiteral("org.kdevelop.ProjectController")); int errors = 0; for (const UrlInfo& path : paths) { QDBusReply result = iface.call(QStringLiteral("openProjectForUrl"), path.url.toString()); if ( !result.isValid() ) { QTextStream err(stderr); err << i18n("Could not open project '%1': %2", path.url.toDisplayString(QUrl::PreferLocalFile), result.error().message()) << "\n"; ++errors; } } // make the window visible QDBusMessage makeVisible = QDBusMessage::createMethodCall( service, QStringLiteral("/kdevelop/MainWindow"), QStringLiteral("org.kdevelop.MainWindow"), QStringLiteral("ensureVisible") ); QDBusConnection::sessionBus().asyncCall( makeVisible ); return errors; } /// Gets the PID of a running KDevelop instance, eventually asking the user if there is more than one. /// Returns -1 in case there are no running sessions. static qint64 getRunningSessionPid() { SessionInfos candidates; foreach( const KDevelop::SessionInfo& si, KDevelop::SessionController::availableSessionInfos() ) { if( KDevelop::SessionController::isSessionRunning(si.uuid.toString()) ) { candidates << si; } } if ( candidates.isEmpty() ) { return -1; } QString sessionUuid; if ( candidates.size() == 1 ) { sessionUuid = candidates.first().uuid.toString(); } else { const QString title = i18n("Select the session to open the document in"); sessionUuid = KDevelop::SessionController::showSessionChooserDialog(title, true); } return KDevelop::SessionController::sessionRunInfo(sessionUuid).holderPid; } static QString findSessionId(const SessionInfos& availableSessionInfos, const QString& session) { //If there is a session and a project with the same name, always open the session //regardless of the order encountered QString projectAsSession; for (const KDevelop::SessionInfo& si : availableSessionInfos) { if ( session == si.name || session == si.uuid.toString() ) { return si.uuid.toString(); } else if (projectAsSession.isEmpty()) { foreach(const QUrl& k, si.projects) { QString fn(k.fileName()); fn = fn.left(fn.indexOf('.')); if ( session == fn ) { projectAsSession = si.uuid.toString(); } } } } if (projectAsSession.isEmpty()) { QTextStream qerr(stderr); qerr << endl << i18n("Cannot open unknown session %1. See `--list-sessions` switch for available sessions or use `-n` to create a new one.", session) << endl; } return projectAsSession; } static qint64 findSessionPid(const QString &sessionId) { KDevelop::SessionRunInfo sessionInfo = KDevelop::SessionController::sessionRunInfo( sessionId ); return sessionInfo.holderPid; } int main( int argc, char *argv[] ) { QElapsedTimer timer; timer.start(); #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) // If possible, use the Software backend for QQuickWidget (currently used in the // welcome page plugin). This means we don't need OpenGL at all, avoiding issues // like https://bugs.kde.org/show_bug.cgi?id=386527. QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); #endif // TODO: Maybe generalize, add KDEVELOP_STANDALONE build option #if defined(Q_OS_WIN) || defined(Q_OS_MAC) qputenv("KDE_FORK_SLAVES", "1"); // KIO slaves will be forked off instead of being started via DBus #endif // Useful for valgrind runs, just `export KDEV_DISABLE_JIT=1` if (qEnvironmentVariableIsSet("KDEV_DISABLE_JIT")) { qputenv("KDEV_DISABLE_WELCOMEPAGE", "1"); qputenv("QT_ENABLE_REGEXP_JIT", "0"); } QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); #ifdef Q_OS_MAC CFBundleRef mainBundle = CFBundleGetMainBundle(); if (mainBundle) { // get the application's Info Dictionary. For app bundles this would live in the bundle's Info.plist, // for regular executables it is obtained in another way. CFMutableDictionaryRef infoDict = (CFMutableDictionaryRef) CFBundleGetInfoDictionary(mainBundle); if (infoDict) { // Try to prevent App Nap on OS X. This can be tricky in practice, at least in 10.9 . CFDictionarySetValue(infoDict, CFSTR("NSAppSleepDisabled"), kCFBooleanTrue); CFDictionarySetValue(infoDict, CFSTR("NSSupportsAutomaticTermination"), kCFBooleanFalse); } } #endif //we can't use KCmdLineArgs as it doesn't allow arguments for the debugee //so lookup the --debug switch and eat everything behind by decrementing argc //debugArgs is filled with args after --debug QStringList debugArgs; QString debugeeName; { bool debugFound = false; int c = argc; for (int i=0; i < c; ++i) { if (debugFound) { debugArgs << argv[i]; } else if ((qstrcmp(argv[i], "--debug") == 0) || (qstrcmp(argv[i], "-d") == 0)) { if (argc > (i + 1)) { i++; } argc = i + 1; debugFound = true; } else if (QString(argv[i]).startsWith(QLatin1String("--debug="))) { argc = i + 1; debugFound = true; } } } KDevelopApplication app(argc, argv); KLocalizedString::setApplicationDomain("kdevelop"); KAboutData aboutData( QStringLiteral("kdevelop"), i18n( "KDevelop" ), QByteArray(KDEVELOP_VERSION_STRING), i18n("The KDevelop Integrated Development Environment"), KAboutLicense::GPL, i18n("Copyright 1999-2018, The KDevelop developers"), QString(), QStringLiteral("https://www.kdevelop.org/")); aboutData.setDesktopFileName(QStringLiteral("org.kde.kdevelop")); aboutData.addAuthor( i18n("Kevin Funk"), i18n( "Co-maintainer, C++/Clang, QA, Windows Support" ), QStringLiteral("kfunk@kde.org") ); aboutData.addAuthor( i18n("Sven Brauch"), i18n( "Co-maintainer, AppImage, Python Support, User Interface improvements" ), QStringLiteral("svenbrauch@gmail.com") ); aboutData.addAuthor( i18n("Aleix Pol Gonzalez"), i18n( "CMake Support, Run Support, Kross Support" ), QStringLiteral("aleixpol@gmail.com") ); aboutData.addAuthor( i18n("Milian Wolff"), i18n( "C++/Clang, Generic manager, Webdevelopment Plugins, Snippets, Performance" ), QStringLiteral("mail@milianw.de") ); aboutData.addAuthor( i18n("Olivier JG"), i18n( "C++/Clang, DUChain, Bug Fixes" ), QStringLiteral("olivier.jg@gmail.com") ); aboutData.addAuthor( i18n("Andreas Pakulat"), i18n( "Architecture, VCS Support, Project Management Support, QMake Projectmanager" ), QStringLiteral("apaku@gmx.de") ); aboutData.addAuthor( i18n("Alexander Dymo"), i18n( "Architecture, Sublime UI, Ruby support" ), QStringLiteral("adymo@kdevelop.org") ); aboutData.addAuthor( i18n("David Nolden"), i18n( "Definition-Use Chain, C++ Support, Code Navigation, Code Completion, Coding Assistance, Refactoring" ), QStringLiteral("david.nolden.kdevelop@art-master.de") ); aboutData.addAuthor( i18n("Vladimir Prus"), i18n( "GDB integration" ), QStringLiteral("ghost@cs.msu.su") ); aboutData.addAuthor( i18n("Hamish Rodda"), i18n( "Text editor integration, definition-use chain" ), QStringLiteral("rodda@kde.org") ); aboutData.addAuthor( i18n("Amilcar do Carmo Lucas"), i18n( "Website admin, API documentation, Doxygen and autoproject patches" ), QStringLiteral("amilcar@kdevelop.org") ); aboutData.addAuthor( i18n("Niko Sams"), i18n( "GDB integration, Webdevelopment Plugins" ), QStringLiteral("niko.sams@gmail.com") ); aboutData.addAuthor( i18n("Friedrich W. H. Kossebau"), QString(), QStringLiteral("kossebau@kde.org") ); aboutData.addCredit( i18n("Matt Rogers"), QString(), QStringLiteral("mattr@kde.org")); aboutData.addCredit( i18n("Cédric Pasteur"), i18n("astyle and indent support"), QStringLiteral("cedric.pasteur@free.fr") ); aboutData.addCredit( i18n("Evgeniy Ivanov"), i18n("Distributed VCS, Git, Mercurial"), QStringLiteral("powerfox@kde.ru") ); // QTest integration is separate in playground currently. //aboutData.addCredit( i18n("Manuel Breugelmanns"), i18n( "Veritas, QTest integration"), "mbr.nxi@gmail.com" ); aboutData.addCredit( i18n("Robert Gruber") , i18n( "SnippetPart, debugger and usability patches" ), QStringLiteral("rgruber@users.sourceforge.net") ); aboutData.addCredit( i18n("Dukju Ahn"), i18n( "Subversion plugin, Custom Make Manager, Overall improvements" ), QStringLiteral("dukjuahn@gmail.com") ); aboutData.addCredit( i18n("Harald Fernengel"), i18n( "Ported to Qt 3, patches, valgrind, diff and perforce support" ), QStringLiteral("harry@kdevelop.org") ); aboutData.addCredit( i18n("Roberto Raggi"), i18n( "C++ parser" ), QStringLiteral("roberto@kdevelop.org") ); aboutData.addCredit( i18n("The KWrite authors"), i18n( "Kate editor component" ), QStringLiteral("kwrite-devel@kde.org") ); aboutData.addCredit( i18n("Nokia Corporation/Qt Software"), i18n( "Designer code" ), QStringLiteral("qt-info@nokia.com") ); aboutData.addCredit( i18n("Contributors to older versions:"), QString(), QString() ); aboutData.addCredit( i18n("Bernd Gehrmann"), i18n( "Initial idea, basic architecture, much initial source code" ), QStringLiteral("bernd@kdevelop.org") ); aboutData.addCredit( i18n("Caleb Tennis"), i18n( "KTabBar, bugfixes" ), QStringLiteral("caleb@aei-tech.com") ); aboutData.addCredit( i18n("Richard Dale"), i18n( "Java & Objective C support" ), QStringLiteral("Richard_Dale@tipitina.demon.co.uk") ); aboutData.addCredit( i18n("John Birch"), i18n( "Debugger frontend" ), QStringLiteral("jbb@kdevelop.org") ); aboutData.addCredit( i18n("Sandy Meier"), i18n( "PHP support, context menu stuff" ), QStringLiteral("smeier@kdevelop.org") ); aboutData.addCredit( i18n("Kurt Granroth"), i18n( "KDE application templates" ), QStringLiteral("kurth@granroth.org") ); aboutData.addCredit( i18n("Ian Reinhart Geiser"), i18n( "Dist part, bash support, application templates" ), QStringLiteral("geiseri@yahoo.com") ); aboutData.addCredit( i18n("Matthias Hoelzer-Kluepfel"), i18n( "Several components, htdig indexing" ), QStringLiteral("hoelzer@kde.org") ); aboutData.addCredit( i18n("Victor Roeder"), i18n( "Help with Automake manager and persistent class store" ), QStringLiteral("victor_roeder@gmx.de") ); aboutData.addCredit( i18n("Simon Hausmann"), i18n( "Help with KParts infrastructure" ), QStringLiteral("hausmann@kde.org") ); aboutData.addCredit( i18n("Oliver Kellogg"), i18n( "Ada support" ), QStringLiteral("okellogg@users.sourceforge.net") ); aboutData.addCredit( i18n("Jakob Simon-Gaarde"), i18n( "QMake projectmanager" ), QStringLiteral("jsgaarde@tdcspace.dk") ); aboutData.addCredit( i18n("Falk Brettschneider"), i18n( "MDI modes, QEditor, bugfixes" ), QStringLiteral("falkbr@kdevelop.org") ); aboutData.addCredit( i18n("Mario Scalas"), i18n( "PartExplorer, redesign of CvsPart, patches, bugs(fixes)" ), QStringLiteral("mario.scalas@libero.it") ); aboutData.addCredit( i18n("Jens Dagerbo"), i18n( "Replace, Bookmarks, FileList and CTags2 plugins. Overall improvements and patches" ), QStringLiteral("jens.dagerbo@swipnet.se") ); aboutData.addCredit( i18n("Julian Rockey"), i18n( "Filecreate part and other bits and patches" ), QStringLiteral("linux@jrockey.com") ); aboutData.addCredit( i18n("Ajay Guleria"), i18n( "ClearCase support" ), QStringLiteral("ajay_guleria@yahoo.com") ); aboutData.addCredit( i18n("Marek Janukowicz"), i18n( "Ruby support" ), QStringLiteral("child@t17.ds.pwr.wroc.pl") ); aboutData.addCredit( i18n("Robert Moniot"), i18n( "Fortran documentation" ), QStringLiteral("moniot@fordham.edu") ); aboutData.addCredit( i18n("Ka-Ping Yee"), i18n( "Python documentation utility" ), QStringLiteral("ping@lfw.org") ); aboutData.addCredit( i18n("Dimitri van Heesch"), i18n( "Doxygen wizard" ), QStringLiteral("dimitri@stack.nl") ); aboutData.addCredit( i18n("Hugo Varotto"), i18n( "Fileselector component" ), QStringLiteral("hugo@varotto-usa.com") ); aboutData.addCredit( i18n("Matt Newell"), i18n( "Fileselector component" ), QStringLiteral("newellm@proaxis.com") ); aboutData.addCredit( i18n("Daniel Engelschalt"), i18n( "C++ code completion, persistent class store" ), QStringLiteral("daniel.engelschalt@gmx.net") ); aboutData.addCredit( i18n("Stephane Ancelot"), i18n( "Patches" ), QStringLiteral("sancelot@free.fr") ); aboutData.addCredit( i18n("Jens Zurheide"), i18n( "Patches" ), QStringLiteral("jens.zurheide@gmx.de") ); aboutData.addCredit( i18n("Luc Willems"), i18n( "Help with Perl support" ), QStringLiteral("Willems.luc@pandora.be") ); aboutData.addCredit( i18n("Marcel Turino"), i18n( "Documentation index view" ), QStringLiteral("M.Turino@gmx.de") ); aboutData.addCredit( i18n("Yann Hodique"), i18n( "Patches" ), QStringLiteral("Yann.Hodique@lifl.fr") ); aboutData.addCredit( i18n("Tobias Gl\303\244\303\237er") , i18n( "Documentation Finder, qmake projectmanager patches, usability improvements, bugfixes ... " ), QStringLiteral("tobi.web@gmx.de") ); aboutData.addCredit( i18n("Andreas Koepfle") , i18n( "QMake project manager patches" ), QStringLiteral("koepfle@ti.uni-mannheim.de") ); aboutData.addCredit( i18n("Sascha Cunz") , i18n( "Cleanup and bugfixes for qEditor, AutoMake and much other stuff" ), QStringLiteral("mail@sacu.de") ); aboutData.addCredit( i18n("Zoran Karavla"), i18n( "Artwork for the ruby language" ), QStringLiteral("webmaster@the-error.net"), QStringLiteral("http://the-error.net") ); KAboutData::setApplicationData(aboutData); // set icon for shells which do not use desktop file metadata // but without setting replacing an existing icon with an empty one! QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("kdevelop"), QApplication::windowIcon())); KCrash::initialize(); Kdelibs4ConfigMigrator migrator(QStringLiteral("kdevelop")); migrator.setConfigFiles({QStringLiteral("kdeveloprc")}); migrator.setUiFiles({QStringLiteral("kdevelopui.rc")}); migrator.migrate(); // High DPI support app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); qCDebug(APP) << "Startup"; QCommandLineParser parser; aboutData.setupCommandLine(&parser); parser.addOption(QCommandLineOption{QStringList{"n", "new-session"}, i18n("Open KDevelop with a new session using the given name."), QStringLiteral("name")}); parser.addOption(QCommandLineOption{QStringList{"s", "open-session"}, i18n("Open KDevelop with the given session.\n" "You can pass either hash or the name of the session." ), QStringLiteral("session")}); parser.addOption(QCommandLineOption{QStringList{"rm", "remove-session"}, i18n("Delete the given session.\n" "You can pass either hash or the name of the session." ), QStringLiteral("session")}); parser.addOption(QCommandLineOption{QStringList{"ps", "pick-session"}, i18n("Shows all available sessions and lets you select one to open.")}); parser.addOption(QCommandLineOption{QStringList{"pss", "pick-session-shell"}, i18n("List all available sessions on shell and lets you select one to open.")}); parser.addOption(QCommandLineOption{QStringList{"l", "list-sessions"}, i18n("List available sessions and quit.")}); parser.addOption(QCommandLineOption{QStringList{"f", "fetch"}, i18n("Open KDevelop and fetch the project from the given ."), QStringLiteral("repo url")}); parser.addOption(QCommandLineOption{QStringList{"p", "project"}, i18n("Open KDevelop and load the given project. can be either a .kdev4 file or a directory path."), QStringLiteral("project")}); parser.addOption(QCommandLineOption{QStringList{"d", "debug"}, i18n("Start debugging an application in KDevelop with the given debugger.\n" "The executable that should be debugged must follow - including arguments.\n" "Example: kdevelop --debug gdb myapp --foo bar"), QStringLiteral("debugger")}); // this is used by the 'kdevelop!' script to retrieve the pid of a KDEVELOP // instance. When this is called, then we should just print the PID on the // standard-output. If a session is specified through open-session, then // we should return the PID of that session. Otherwise, if only a single // session is running, then we should just return the PID of that session. // Otherwise, we should print a command-line session-chooser dialog ("--pss"), // which only shows the running sessions, and the user can pick one. parser.addOption(QCommandLineOption{QStringList{"pid"}}); parser.addPositionalArgument(QStringLiteral("files"), i18n( "Files to load, or directories to load as projects" ), QStringLiteral("[FILE[:line[:column]] | DIRECTORY]...")); // The session-controller needs to arguments to eventually pass them to newly opened sessions KDevelop::SessionController::setArguments(argc, argv); parser.process(app); aboutData.processCommandLine(&parser); if(parser.isSet(QStringLiteral("list-sessions"))) { QTextStream qout(stdout); qout << endl << ki18n("Available sessions (use '-s HASH' or '-s NAME' to open a specific one):").toString() << endl << endl; qout << QStringLiteral("%1").arg(ki18n("Hash").toString(), -38) << '\t' << ki18n("Name: Opened Projects").toString() << endl; foreach(const KDevelop::SessionInfo& si, KDevelop::SessionController::availableSessionInfos()) { if ( si.name.isEmpty() && si.projects.isEmpty() ) { continue; } qout << si.uuid.toString() << '\t' << si.description; if(KDevelop::SessionController::isSessionRunning(si.uuid.toString())) qout << " " << i18n("[running]"); qout << endl; } return 0; } // Handle extra arguments, which stand for files to open QVector initialFiles; QVector initialDirectories; foreach (const QString &file, parser.positionalArguments()) { const UrlInfo info(file); if (info.isDirectory()) { initialDirectories.append(info); } else { initialFiles.append(info); } } const auto availableSessionInfos = KDevelop::SessionController::availableSessionInfos(); if ((!initialFiles.isEmpty() || !initialDirectories.isEmpty()) && !parser.isSet(QStringLiteral("new-session"))) { #if KDEVELOP_SINGLE_APP if (app.isRunning()) { bool success = app.sendMessage(serializeOpenFilesMessage(initialFiles << initialDirectories)); if (success) { return 0; } } #else qint64 pid = -1; if (parser.isSet(QStringLiteral("open-session"))) { const QString session = findSessionId(availableSessionInfos, parser.value(QStringLiteral("open-session"))); if (session.isEmpty()) { return 1; } else if (KDevelop::SessionController::isSessionRunning(session)) { pid = findSessionPid(session); } } else { pid = getRunningSessionPid(); } if ( pid > 0 ) { return openFilesInRunningInstance(initialFiles, pid) + openProjectInRunningInstance(initialDirectories, pid); } // else there are no running sessions, and the generated list of files will be opened below. #endif } // if empty, restart kdevelop with last active session, see SessionController::defaultSessionId QString session; uint nRunningSessions = 0; for (const KDevelop::SessionInfo& si : availableSessionInfos) { if(KDevelop::SessionController::isSessionRunning(si.uuid.toString())) ++nRunningSessions; } // also show the picker dialog when a pid shall be retrieved and multiple // sessions are running. if(parser.isSet(QStringLiteral("pss")) || (parser.isSet(QStringLiteral("pid")) && !parser.isSet(QStringLiteral("open-session")) && !parser.isSet(QStringLiteral("ps")) && nRunningSessions > 1)) { QTextStream qerr(stderr); SessionInfos candidates; for (const KDevelop::SessionInfo& si : availableSessionInfos) { if( (!si.name.isEmpty() || !si.projects.isEmpty() || parser.isSet(QStringLiteral("pid"))) && (!parser.isSet(QStringLiteral("pid")) || KDevelop::SessionController::isSessionRunning(si.uuid.toString()))) candidates << si; } if(candidates.size() == 0) { qerr << "no session available" << endl; return 1; } if(candidates.size() == 1 && parser.isSet(QStringLiteral("pid"))) { session = candidates[0].uuid.toString(); }else{ for(int i = 0; i < candidates.size(); ++i) qerr << "[" << i << "]: " << candidates[i].description << endl; int chosen; std::cin >> chosen; if(std::cin.good() && (chosen >= 0 && chosen < candidates.size())) { session = candidates[chosen].uuid.toString(); }else{ qerr << "invalid selection" << endl; return 1; } } } if(parser.isSet(QStringLiteral("ps"))) { bool onlyRunning = parser.isSet(QStringLiteral("pid")); session = KDevelop::SessionController::showSessionChooserDialog(i18n("Select the session you would like to use"), onlyRunning); if(session.isEmpty()) return 1; } if ( parser.isSet(QStringLiteral("debug")) ) { if ( debugArgs.isEmpty() ) { QTextStream qerr(stderr); qerr << endl << i18nc("@info:shell", "Specify the executable you want to debug.") << endl; return 1; } QFileInfo executableFileInfo(debugArgs.first()); if (!executableFileInfo.exists()) { executableFileInfo = QStandardPaths::findExecutable(debugArgs.first()); if (!executableFileInfo.exists()) { QTextStream qerr(stderr); qerr << endl << i18nc("@info:shell", "Specified executable does not exist.") << endl; return 1; } } debugArgs.first() = executableFileInfo.absoluteFilePath(); debugeeName = i18n("Debug %1", executableFileInfo.fileName()); session = debugeeName; } else if ( parser.isSet(QStringLiteral("new-session")) ) { session = parser.value(QStringLiteral("new-session")); for (const KDevelop::SessionInfo& si : availableSessionInfos) { if ( session == si.name ) { QTextStream qerr(stderr); qerr << endl << i18n("A session with the name %1 exists already. Use the -s switch to open it.", session) << endl; return 1; } } // session doesn't exist, we can create it } else if ( parser.isSet(QStringLiteral("open-session")) ) { session = findSessionId(availableSessionInfos, parser.value(QStringLiteral("open-session"))); if (session.isEmpty()) { return 1; } } else if ( parser.isSet(QStringLiteral("remove-session")) ) { session = parser.value(QStringLiteral("remove-session")); auto si = findSessionInList(availableSessionInfos, session); if (!si) { QTextStream qerr(stderr); qerr << endl << i18n("No session with the name %1 exists.", session) << endl; return 1; } auto sessionLock = KDevelop::SessionController::tryLockSession(si->uuid.toString()); if (!sessionLock.lock) { QTextStream qerr(stderr); qerr << endl << i18n("Could not lock session %1 for deletion.", session) << endl; return 1; } KDevelop::SessionController::deleteSessionFromDisk(sessionLock.lock); QTextStream qout(stdout); qout << endl << i18n("Session with name %1 was successfully removed.", session) << endl; return 0; } if(parser.isSet(QStringLiteral("pid"))) { if (session.isEmpty()) { // just pick the first running session for (const KDevelop::SessionInfo& si : availableSessionInfos) { if(KDevelop::SessionController::isSessionRunning(si.uuid.toString())) session = si.uuid.toString(); } } const KDevelop::SessionInfo* sessionData = findSessionInList(availableSessionInfos, session); if( !sessionData ) { qCritical(APP) << "session not given or does not exist"; return 5; } const auto pid = findSessionPid(sessionData->uuid.toString()); if (pid > 0) { // Print the PID and we're ready std::cout << pid << std::endl; return 0; } else { qCritical(APP) << sessionData->uuid.toString() << sessionData->name << "is not running"; return 5; } } if (parser.isSet(QStringLiteral("project"))) { const auto project = parser.value(QStringLiteral("project")); QFileInfo info(project); QUrl projectUrl; if (info.suffix() == QLatin1String("kdev4")) { projectUrl = QUrl::fromLocalFile(info.absoluteFilePath()); } else if (info.isDir()) { QDir dir(info.absoluteFilePath()); const auto potentialProjectFiles = dir.entryList({QStringLiteral("*.kdev4")}, QDir::Files, QDir::Name); qDebug(APP) << "Found these potential project files:" << potentialProjectFiles; if (!potentialProjectFiles.isEmpty()) { projectUrl = QUrl::fromLocalFile(dir.absoluteFilePath(potentialProjectFiles.value(0))); } } else { QTextStream qerr(stderr); qerr << "Invalid project: " << project << " - should be either a path to a .kdev4 file or a directory containing a .kdev4 file"; return 1; } qDebug(APP) << "Attempting to find a suitable session for project" << projectUrl; const auto sessionInfos = findSessionsWithProject(availableSessionInfos, projectUrl); qDebug(APP) << "Found matching sessions:" << sessionInfos.size(); if (!sessionInfos.isEmpty()) { // TODO: If there's more than one match: Allow the user to select which session to open? qDebug(APP) << "Attempting to open session:" << sessionInfos.at(0).name; session = sessionInfos.at(0).uuid.toString(); } } KDevIDEExtension::init(); qDebug(APP) << "Attempting to initialize session:" << session; if(!Core::initialize(Core::Default, session)) return 5; // register a DBUS service for this process, so that we can open files in it from other invocations QDBusConnection::sessionBus().registerService(QStringLiteral("org.kdevelop.kdevelop-%1").arg(app.applicationPid())); Core* core = Core::self(); if (!QProcessEnvironment::systemEnvironment().contains(QStringLiteral("KDEV_DISABLE_WELCOMEPAGE"))) { core->pluginController()->loadPlugin(QStringLiteral("KDevWelcomePage")); } const auto fetchUrlStrings = parser.values(QStringLiteral("fetch")); for (const auto& fetchUrlString : fetchUrlStrings) { core->projectControllerInternal()->fetchProjectFromUrl(QUrl::fromUserInput(fetchUrlString)); } const QString debugStr = QStringLiteral("debug"); if ( parser.isSet(debugStr) ) { Q_ASSERT( !debugeeName.isEmpty() ); QString launchName = debugeeName; KDevelop::LaunchConfiguration* launch = nullptr; qCDebug(APP) << launchName; foreach (KDevelop::LaunchConfiguration *l, core->runControllerInternal()->launchConfigurationsInternal()) { qCDebug(APP) << l->name(); if (l->name() == launchName) { launch = l; } } KDevelop::LaunchConfigurationType *type = nullptr; foreach (KDevelop::LaunchConfigurationType *t, core->runController()->launchConfigurationTypes()) { qCDebug(APP) << t->id(); if (t->id() == QLatin1String("Native Application")) { type = t; break; } } if (!type) { QTextStream qerr(stderr); qerr << endl << i18n("Cannot find native launch configuration type") << endl; return 1; } if (launch && launch->type()->id() != QLatin1String("Native Application")) launch = nullptr; if (launch && launch->launcherForMode(debugStr) != parser.value(debugStr)) launch = nullptr; if (!launch) { qCDebug(APP) << launchName << "not found, creating a new one"; QPair launcher; launcher.first = debugStr; foreach (KDevelop::ILauncher *l, type->launchers()) { if (l->id() == parser.value(debugStr)) { if (l->supportedModes().contains(debugStr)) { launcher.second = l->id(); } } } if (launcher.second.isEmpty()) { QTextStream qerr(stderr); qerr << endl << i18n("Cannot find launcher %1", parser.value(debugStr)) << endl; return 1; } KDevelop::ILaunchConfiguration* ilaunch = core->runController()->createLaunchConfiguration(type, launcher, nullptr, launchName); launch = static_cast(ilaunch); } type->configureLaunchFromCmdLineArguments(launch->config(), debugArgs); launch->config().writeEntry("Break on Start", true); core->runControllerInternal()->setDefaultLaunch(launch); core->runControllerInternal()->execute(debugStr, launch); } else { openFiles(initialFiles); - for(const auto &urlinfo: initialDirectories) + for(const auto& urlinfo: qAsConst(initialDirectories)) core->projectController()->openProjectForUrl(urlinfo.url); } #if KDEVELOP_SINGLE_APP // Set up remote arguments. QObject::connect(&app, &SharedTools::QtSingleApplication::messageReceived, &app, &KDevelopApplication::remoteArguments); QObject::connect(&app, &SharedTools::QtSingleApplication::fileOpenRequest, &app, &KDevelopApplication::fileOpenRequested); #endif qCDebug(APP) << "Done startup" << "- took:" << timer.elapsed() << "ms"; timer.invalidate(); return app.exec(); } diff --git a/kdevplatform/interfaces/contextmenuextension.cpp b/kdevplatform/interfaces/contextmenuextension.cpp index a455bd2aff..dac72d8c86 100644 --- a/kdevplatform/interfaces/contextmenuextension.cpp +++ b/kdevplatform/interfaces/contextmenuextension.cpp @@ -1,149 +1,149 @@ /*************************************************************************** * This file is part of KDevelop * * 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 "contextmenuextension.h" #include "qtcompat_p.h" #include #include #include #include namespace KDevelop { const QString ContextMenuExtension::FileGroup = QStringLiteral("FileGroup"); const QString ContextMenuExtension::RefactorGroup = QStringLiteral("RefactorGroup"); const QString ContextMenuExtension::BuildGroup = QStringLiteral("BuildGroup"); const QString ContextMenuExtension::RunGroup = QStringLiteral("RunGroup"); const QString ContextMenuExtension::DebugGroup = QStringLiteral("DebugGroup"); const QString ContextMenuExtension::EditGroup = QStringLiteral("EditGroup"); const QString ContextMenuExtension::VcsGroup = QStringLiteral("VcsGroup"); const QString ContextMenuExtension::ProjectGroup = QStringLiteral("ProjectGroup"); const QString ContextMenuExtension::OpenEmbeddedGroup = QStringLiteral("OpenEmbeddedGroup"); const QString ContextMenuExtension::OpenExternalGroup = QStringLiteral("OpenExternalGroup"); const QString ContextMenuExtension::AnalyzeFileGroup = QStringLiteral("AnalyzeFileGroup"); const QString ContextMenuExtension::AnalyzeProjectGroup = QStringLiteral("AnalyzeProjectGroup"); const QString ContextMenuExtension::NavigationGroup = QStringLiteral("NavigationGroup"); const QString ContextMenuExtension::ExtensionGroup = QStringLiteral("ExtensionGroup"); class ContextMenuExtensionPrivate { public: QMap > extensions; }; ContextMenuExtension::ContextMenuExtension() : d(new ContextMenuExtensionPrivate) { } ContextMenuExtension::~ContextMenuExtension() = default; ContextMenuExtension::ContextMenuExtension( const ContextMenuExtension& rhs ) : d( new ContextMenuExtensionPrivate ) { d->extensions = rhs.d->extensions; } ContextMenuExtension& ContextMenuExtension::operator=( const ContextMenuExtension& rhs ) { if( this == &rhs ) return *this; d->extensions = rhs.d->extensions; return *this; } QList ContextMenuExtension::actions( const QString& group ) const { return d->extensions.value( group, QList() ); } void ContextMenuExtension::addAction( const QString& group, QAction* action ) { if( !d->extensions.contains( group ) ) { d->extensions.insert( group, QList() << action ); } else { d->extensions[group].append( action ); } } static void populateMenuWithGroup( QMenu* menu, const QList& extensions, const QString& groupName, const QString& groupDisplayName = QString(), const QString& groupIconName = QString(), bool forceAddMenu = false, bool addSeparator = true) { QList groupActions; for (const ContextMenuExtension& extension : extensions) { groupActions += extension.actions(groupName); } // remove NULL QActions, if any. Those can end up in groupActions if plugins // like the debugger plugins are not loaded. groupActions.removeAll(nullptr); if (groupActions.isEmpty()) { return; } QMenu* groupMenu = menu; if ((groupActions.count() > 1 && !groupDisplayName.isEmpty()) || (!groupDisplayName.isEmpty() && forceAddMenu)) { groupMenu = menu->addMenu(groupDisplayName); if (!groupIconName.isEmpty()) { groupMenu->setIcon(QIcon::fromTheme(groupIconName)); } } - for (QAction* action : groupActions) { + for (QAction* action : qAsConst(groupActions)) { groupMenu->addAction(action); } if (addSeparator) { menu->addSeparator(); } } void ContextMenuExtension::populateMenu(QMenu* menu, const QList& extensions) { populateMenuWithGroup(menu, extensions, BuildGroup); populateMenuWithGroup(menu, extensions, FileGroup); populateMenuWithGroup(menu, extensions, EditGroup); populateMenuWithGroup(menu, extensions, DebugGroup, i18n("Debug")); populateMenuWithGroup(menu, extensions, RefactorGroup, i18n("Refactor")); populateMenuWithGroup(menu, extensions, NavigationGroup); populateMenuWithGroup(menu, extensions, AnalyzeFileGroup, i18n("Analyze Current File With"), QStringLiteral("dialog-ok"), true, false); populateMenuWithGroup(menu, extensions, AnalyzeProjectGroup, i18n("Analyze Current Project With"), QStringLiteral("dialog-ok"), true); populateMenuWithGroup(menu, extensions, VcsGroup); populateMenuWithGroup(menu, extensions, ExtensionGroup); } } diff --git a/kdevplatform/language/codecompletion/codecompletiontesthelper.h b/kdevplatform/language/codecompletion/codecompletiontesthelper.h index b5ac341d9c..02a191a938 100644 --- a/kdevplatform/language/codecompletion/codecompletiontesthelper.h +++ b/kdevplatform/language/codecompletion/codecompletiontesthelper.h @@ -1,236 +1,236 @@ /* This file is part of KDevelop Copyright 2006 Hamish Rodda Copyright 2007-2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H #define KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H #include #include #include "../duchain/declaration.h" #include "../duchain/duchain.h" #include "codecompletionitem.h" #include #include #include #include using namespace KTextEditor; using namespace KDevelop; /** * Helper-class for testing completion-items * Just initialize it with the context and the text, and then use the members, for simple cases only "names" * the template parameter is your language specific CodeCompletionContext */ template struct CodeCompletionItemTester { using Element = QExplicitlySharedDataPointer; using Item = QExplicitlySharedDataPointer; using Context = QExplicitlySharedDataPointer; //Standard constructor - CodeCompletionItemTester(DUContext* context, const QString& text = "; ", const QString& followingText = QString(), + CodeCompletionItemTester(DUContext* context, const QString& text = QStringLiteral("; "), const QString& followingText = QString(), const CursorInRevision& position = CursorInRevision::invalid()) : completionContext(new T(DUContextPointer(context), text, followingText, position.isValid() ? position : context->range().end)) { init(); } //Can be used if you already have the completion context CodeCompletionItemTester(const Context& context) : completionContext(context) { init(); } //Creates a CodeCompletionItemTester for the parent context CodeCompletionItemTester parent() const { Context parent = Context(dynamic_cast(completionContext->parentContext())); Q_ASSERT(parent); return CodeCompletionItemTester(parent); } void addElements(const QList& elements) { for (auto& element : elements) { Item item(dynamic_cast(element.data())); if(item) items << item; CompletionTreeNode* node = dynamic_cast(element.data()); if(node) addElements(node->children); } } bool containsDeclaration(Declaration* dec) const { for (auto& item : items) { if (item->declaration().data() == dec) { return true; } } return false; } QList items; // All items retrieved QStringList names; // Names of all completion-items Context completionContext; //Convenience-function to retrieve data from completion-items by name QVariant itemData(const QString& itemName, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { return itemData(names.indexOf(itemName), column, role); } QVariant itemData(int itemNumber, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { if(itemNumber < 0 || itemNumber >= items.size()) return QVariant(); return itemData(items[itemNumber], column, role); } QVariant itemData(Item item, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { return item->data(fakeModel().index(0, column), role, nullptr); } Item findItem(const QString& itemName) const { const auto idx = names.indexOf(itemName); if (idx < 0) { return {}; } return items[idx]; } private: void init() { if ( !completionContext || !completionContext->isValid() ) { qWarning() << "invalid completion context"; return; } bool abort = false; items = completionContext->completionItems(abort); addElements(completionContext->ungroupedElements()); names.reserve(items.size()); foreach(Item i, items) { names << i->data(fakeModel().index(0, KTextEditor::CodeCompletionModel::Name), Qt::DisplayRole, nullptr).toString(); } } static QStandardItemModel& fakeModel() { static QStandardItemModel model; model.setColumnCount(10); model.setRowCount(10); return model; } }; /** * Helper class that inserts the given text into the duchain under the specified name, * allows parsing it with a simple call to parse(), and automatically releases the top-context * * The duchain must not be locked when this object is destroyed */ struct InsertIntoDUChain { ///Artificially inserts a file called @p name with the text @p text InsertIntoDUChain(const QString& name, const QString& text) : m_insertedCode(IndexedString(name), text), m_topContext(nullptr) { } ~InsertIntoDUChain() { get(); release(); } ///The duchain must not be locked when this is called void release() { if(m_topContext) { DUChainWriteLocker lock; m_topContext = nullptr; const QList chains = DUChain::self()->chainsForDocument(m_insertedCode.file()); for (TopDUContext* top : chains) { DUChain::self()->removeDocumentChain(top); } } } TopDUContext* operator->() { get(); return m_topContext.data(); } TopDUContext* tryGet() { DUChainReadLocker lock; return DUChain::self()->chainForDocument(m_insertedCode.file(), false); } void get() { if(!m_topContext) m_topContext = tryGet(); } ///Helper function: get a declaration based on its qualified identifier Declaration* getDeclaration(const QString& id) { get(); if(!topContext()) return nullptr; return DeclarationId(IndexedQualifiedIdentifier(QualifiedIdentifier(id))).getDeclaration(topContext()); } TopDUContext* topContext() { return m_topContext.data(); } /** * Parses this inserted code as a stand-alone top-context * The duchain must not be locked when this is called * * @param features The features that should be requested for the top-context * @param update Whether the top-context should be updated if it already exists. Else it will be deleted. */ void parse(uint features = TopDUContext::AllDeclarationsContextsAndUses, bool update = false) { if(!update) release(); m_topContext = DUChain::self()->waitForUpdate(m_insertedCode.file(), (TopDUContext::Features)features, false); Q_ASSERT(m_topContext); DUChainReadLocker lock; Q_ASSERT(!m_topContext->parsingEnvironmentFile()->isProxyContext()); } InsertArtificialCodeRepresentation m_insertedCode; ReferencedTopDUContext m_topContext; }; #endif // KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H diff --git a/kdevplatform/language/duchain/problem.cpp b/kdevplatform/language/duchain/problem.cpp index a42bf79fce..47e9e7541c 100644 --- a/kdevplatform/language/duchain/problem.cpp +++ b/kdevplatform/language/duchain/problem.cpp @@ -1,283 +1,285 @@ /* This file is part of KDevelop Copyright 2007 Hamish Rodda 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 "problem.h" #include "duchainregister.h" #include "topducontextdynamicdata.h" #include "topducontext.h" #include "topducontextdata.h" #include "duchain.h" #include "duchainlock.h" #include #include +#include + #include namespace KDevelop { REGISTER_DUCHAIN_ITEM(Problem); DEFINE_LIST_MEMBER_HASH(ProblemData, diagnostics, LocalIndexedProblem) } using namespace KDevelop; LocalIndexedProblem::LocalIndexedProblem(const ProblemPointer& problem, const TopDUContext* top) : m_index(problem->m_indexInTopContext) { ENSURE_CHAIN_READ_LOCKED // ensure child problems are properly serialized before we serialize the parent problem // see below, the diagnostic size is kept in sync by the mutable API of Problem // the const cast is ugly but we don't really "change" the state as observed from the outside auto& serialized = const_cast(problem.data())->d_func_dynamic()->diagnosticsList(); serialized.clear(); serialized.reserve(problem->m_diagnostics.size()); foreach(const ProblemPointer& child, problem->m_diagnostics) { serialized << LocalIndexedProblem(child, top); } if (!m_index) { m_index = top->m_dynamicData->allocateProblemIndex(problem); } } ProblemPointer LocalIndexedProblem::data(const TopDUContext* top) const { if (!m_index) { return {}; } return top->m_dynamicData->getProblemForIndex(m_index); } Problem::Problem() : DUChainBase(*new ProblemData) { d_func_dynamic()->setClassId(this); } Problem::Problem(ProblemData& data) : DUChainBase(data) { } Problem::~Problem() { } TopDUContext* Problem::topContext() const { return m_topContext.data(); } IndexedString Problem::url() const { return d_func()->url; } DocumentRange Problem::finalLocation() const { return DocumentRange(d_func()->url, d_func()->m_range.castToSimpleRange()); } void Problem::setFinalLocation(const DocumentRange& location) { setRange(RangeInRevision::castFromSimpleRange(location)); d_func_dynamic()->url = location.document; } IProblem::FinalLocationMode Problem::finalLocationMode() const { return d_func()->finalLocationMode; } void Problem::setFinalLocationMode(IProblem::FinalLocationMode mode) { d_func_dynamic()->finalLocationMode = mode; } void Problem::clearDiagnostics() { m_diagnostics.clear(); // keep serialization in sync, see also LocalIndexedProblem ctor above d_func_dynamic()->diagnosticsList().clear(); } QVector Problem::diagnostics() const { QVector vector; - for (ProblemPointer ptr : m_diagnostics) { + for (const auto& ptr : qAsConst(m_diagnostics)) { vector.push_back(ptr); } return vector; } void Problem::setDiagnostics(const QVector &diagnostics) { clearDiagnostics(); for (const IProblem::Ptr& problem : diagnostics) { addDiagnostic(problem); } } void Problem::addDiagnostic(const IProblem::Ptr &diagnostic) { Problem *problem = dynamic_cast(diagnostic.data()); Q_ASSERT(problem != nullptr); ProblemPointer ptr(problem); m_diagnostics << ptr; } QString Problem::description() const { return d_func()->description.str(); } void Problem::setDescription(const QString& description) { d_func_dynamic()->description = IndexedString(description); } QString Problem::explanation() const { return d_func()->explanation.str(); } void Problem::setExplanation(const QString& explanation) { d_func_dynamic()->explanation = IndexedString(explanation); } IProblem::Source Problem::source() const { return d_func()->source; } void Problem::setSource(IProblem::Source source) { d_func_dynamic()->source = source; } QExplicitlySharedDataPointer Problem::solutionAssistant() const { return {}; } IProblem::Severity Problem::severity() const { return d_func()->severity; } void Problem::setSeverity(Severity severity) { d_func_dynamic()->severity = severity; } QString Problem::severityString() const { switch(severity()) { case IProblem::NoSeverity: return {}; case IProblem::Error: return i18n("Error"); case IProblem::Warning: return i18n("Warning"); case IProblem::Hint: return i18n("Hint"); } return QString(); } QString Problem::sourceString() const { switch (source()) { case IProblem::Disk: return i18n("Disk"); case IProblem::Preprocessor: return i18n("Preprocessor"); case IProblem::Lexer: return i18n("Lexer"); case IProblem::Parser: return i18n("Parser"); case IProblem::DUChainBuilder: return i18n("Definition-Use Chain"); case IProblem::SemanticAnalysis: return i18n("Semantic analysis"); case IProblem::ToDo: return i18n("To-do"); case IProblem::Unknown: default: return i18n("Unknown"); } } QString Problem::toString() const { return i18nc(": in :[]: (found by )", "%1: %2 in %3:[(%4,%5),(%6,%7)]: %8 (found by %9)" , severityString() , description() , url().str() , range().start.line , range().start.column , range().end.line , range().end.column , (explanation().isEmpty() ? i18n("") : explanation()) , sourceString()); } void Problem::rebuildDynamicData(DUContext* parent, uint ownIndex) { auto top = dynamic_cast(parent); Q_ASSERT(top); m_topContext = top; m_indexInTopContext = ownIndex; // deserialize child diagnostics here, as the top-context might get unloaded // but we still want to keep the child-diagnostics in-tact, as one would assume // a shared-ptr works. const auto data = d_func(); m_diagnostics.reserve(data->diagnosticsSize()); for (uint i = 0; i < data->diagnosticsSize(); ++i) { m_diagnostics << ProblemPointer(data->diagnostics()[i].data(top)); } DUChainBase::rebuildDynamicData(parent, ownIndex); } QDebug operator<<(QDebug s, const Problem& problem) { s.nospace() << problem.toString(); return s.space(); } QDebug operator<<(QDebug s, const ProblemPointer& problem) { if (!problem) { s.nospace() << ""; } else { s.nospace() << problem->toString(); } return s.space(); } diff --git a/kdevplatform/project/tests/abstractfilemanagerpluginimportbenchmark.cpp b/kdevplatform/project/tests/abstractfilemanagerpluginimportbenchmark.cpp index 111c3e3f50..bae86c9d91 100644 --- a/kdevplatform/project/tests/abstractfilemanagerpluginimportbenchmark.cpp +++ b/kdevplatform/project/tests/abstractfilemanagerpluginimportbenchmark.cpp @@ -1,184 +1,185 @@ /* This file is part of KDevelop Copyright 2017 René J.V. Bertin 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 #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace KDevelop { // wrap the ProjectController to make its addProject() method public class ProjectControllerWrapper : public ProjectController { Q_OBJECT public: ProjectControllerWrapper(Core* core) : ProjectController(core) {} using ProjectController::addProject; }; class AbstractFileManagerPluginImportBenchmark : public QObject { Q_OBJECT public: AbstractFileManagerPluginImportBenchmark(AbstractFileManagerPlugin* manager, const QString& path, TestCore* core) : QObject(core) , m_out(stdout) , m_core(core) { m_manager = manager; m_project = new TestProject(Path(path)); } void start() { m_projectNumber = s_numBenchmarksRunning++; m_out << "Starting import of project " << m_project->path().toLocalFile() << endl; ProjectControllerWrapper *projectController = qobject_cast(m_core->projectController()); projectController->addProject(m_project); m_timer.start(); auto root = m_manager->import(m_project); int elapsed = m_timer.elapsed(); m_out << "\tcreating dirwatcher took " << elapsed / 1000.0 << " seconds" << endl; auto import = m_manager->createImportJob(root); connect(import, &KJob::finished, this, &AbstractFileManagerPluginImportBenchmark::projectImportDone); m_timer.restart(); import->start(); } AbstractFileManagerPlugin* m_manager; TestProject* m_project; QElapsedTimer m_timer; int m_projectNumber; QTextStream m_out; TestCore* m_core; static int s_numBenchmarksRunning; Q_SIGNALS: void finished(); private Q_SLOTS: void projectImportDone(KJob* job) { Q_UNUSED(job); int elapsed = m_timer.elapsed(); m_out << "importing " << m_project->fileSet().size() << " items into project #" << m_projectNumber << " took " << elapsed / 1000.0 << " seconds" << endl; s_numBenchmarksRunning -= 1; if (s_numBenchmarksRunning <= 0) { emit finished(); } } }; int AbstractFileManagerPluginImportBenchmark::s_numBenchmarksRunning = 0; } int main(int argc, char** argv) { if (argc < 2) { qWarning() << "Usage:" << argv[0] << "projectDir1 [...projectDirN]"; return 1; } QApplication app(argc, argv); QTextStream qout(stdout); // measure the total test time, this provides an indication // of overhead and how well multiple projects are imported in parallel // (= how different is the total time from the import time of the largest // project). When testing a single project the difference between this // value and total runtime will provide an estimate of the time required // to destroy the dirwatcher. QElapsedTimer runTimer; AutoTestShell::init({"no plugins"}); auto core = TestCore::initialize(); // load/activate the "Project Filter" plugin (it won't be available to us without this step): core->pluginController()->allPluginsForExtension(QStringLiteral("org.kdevelop.IProjectFilter")); auto projectController = new ProjectControllerWrapper(core); delete core->projectController(); core->setProjectController(projectController); auto manager = new AbstractFileManagerPlugin({}, core); const char *kdwMethod[] = {"FAM", "Inotify", "Stat", "QFSWatch"}; qout << "KDirWatch backend: " << kdwMethod[KDirWatch().internalMethod()] << endl; QList benchmarks; for (int i = 1 ; i < argc ; ++i) { const QString path = QString::fromUtf8(argv[i]); if (QFileInfo(path).isDir()) { const auto benchmark = new AbstractFileManagerPluginImportBenchmark(manager, path, core); benchmarks << benchmark; QObject::connect(benchmark, &AbstractFileManagerPluginImportBenchmark::finished, &app, [&runTimer, &qout] { qout << "Done in " << runTimer.elapsed() / 1000.0 << " seconds total\n"; QCoreApplication::instance()->quit(); }); } } if (benchmarks.isEmpty()) { qWarning() << "no projects to import (arguments must be directories)"; return 1; } runTimer.start(); - for (auto benchmark : benchmarks) { + for (auto benchmark : qAsConst(benchmarks)) { benchmark->start(); } return app.exec(); } #include "abstractfilemanagerpluginimportbenchmark.moc" diff --git a/kdevplatform/shell/ktexteditorpluginintegration.cpp b/kdevplatform/shell/ktexteditorpluginintegration.cpp index b72a358141..d24024d837 100644 --- a/kdevplatform/shell/ktexteditorpluginintegration.cpp +++ b/kdevplatform/shell/ktexteditorpluginintegration.cpp @@ -1,459 +1,459 @@ /* Copyright 2015 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "ktexteditorpluginintegration.h" #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "uicontroller.h" #include "documentcontroller.h" #include "plugincontroller.h" #include "mainwindow.h" #include "textdocument.h" #include using namespace KDevelop; namespace { KTextEditor::MainWindow *toKteWrapper(KParts::MainWindow *window) { if (auto mainWindow = dynamic_cast(window)) { return mainWindow->kateWrapper() ? mainWindow->kateWrapper()->interface() : nullptr; } else { return nullptr; } } KTextEditor::View *toKteView(Sublime::View *view) { if (auto textView = dynamic_cast(view)) { return textView->textView(); } else { return nullptr; } } class ToolViewFactory; /** * This HACK is required to massage the KTextEditor plugin API into the * GUI concepts we apply in KDevelop. Kate does not allow the user to * delete tool views and then readd them. We do. To support our use case * we prevent the widget we return to KTextEditor plugins from * MainWindow::createToolView from getting destroyed. This widget class * unsets the parent of the so called container in its dtor. The * ToolViewFactory handles the ownership and destroys the kate widget * as needed. */ class KeepAliveWidget : public QWidget { Q_OBJECT public: explicit KeepAliveWidget(ToolViewFactory *factory, QWidget *parent = nullptr) : QWidget(parent) , m_factory(factory) { } ~KeepAliveWidget() override; private: ToolViewFactory *m_factory; }; class ToolViewFactory : public QObject, public KDevelop::IToolViewFactory { Q_OBJECT public: ToolViewFactory(const QString &text, const QIcon &icon, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos) : m_text(text) , m_icon(icon) , m_identifier(identifier) , m_container(new QWidget) , m_pos(pos) { m_container->setLayout(new QVBoxLayout); } ~ToolViewFactory() override { delete m_container; } QWidget *create(QWidget *parent = nullptr) override { auto widget = new KeepAliveWidget(this, parent); widget->setWindowTitle(m_text); widget->setWindowIcon(m_icon); widget->setLayout(new QVBoxLayout); widget->layout()->addWidget(m_container); widget->addActions(m_container->actions()); return widget; } Qt::DockWidgetArea defaultPosition() override { switch (m_pos) { case KTextEditor::MainWindow::Left: return Qt::LeftDockWidgetArea; case KTextEditor::MainWindow::Right: return Qt::RightDockWidgetArea; case KTextEditor::MainWindow::Top: return Qt::TopDockWidgetArea; case KTextEditor::MainWindow::Bottom: return Qt::BottomDockWidgetArea; } Q_UNREACHABLE(); } QString id() const override { return m_identifier; } QWidget *container() const { return m_container; } private: QString m_text; QIcon m_icon; QString m_identifier; QPointer m_container; KTextEditor::MainWindow::ToolViewPosition m_pos; friend class KeepAliveWidget; }; KeepAliveWidget::~KeepAliveWidget() { // if the container is still valid, unparent it to prevent it from getting deleted // this happens when the user removes a tool view // on shutdown, the container does get deleted, thus we must guard against that. if (m_factory->container()) { Q_ASSERT(m_factory->container()->parentWidget() == this); m_factory->container()->setParent(nullptr); } } } namespace KTextEditorIntegration { Application::Application(QObject *parent) : QObject(parent) { } Application::~Application() { KTextEditor::Editor::instance()->setApplication(nullptr); } KTextEditor::MainWindow *Application::activeMainWindow() const { return toKteWrapper(Core::self()->uiController()->activeMainWindow()); } QList Application::mainWindows() const { return {activeMainWindow()}; } bool Application::closeDocument(KTextEditor::Document *document) const { - auto documentController = Core::self()->documentControllerInternal(); - for (auto doc : documentController->openDocuments()) { + const auto& openDocuments = Core::self()->documentControllerInternal()->openDocuments(); + for (auto doc : openDocuments) { if (doc->textDocument() == document) { return doc->close(); } } return false; } KTextEditor::Plugin *Application::plugin(const QString &id) const { auto kdevPlugin = Core::self()->pluginController()->loadPlugin(id); const auto plugin = dynamic_cast(kdevPlugin); return plugin ? plugin->interface() : nullptr; } QList Application::documents() { QList l; const auto openDocuments = Core::self()->documentControllerInternal()->openDocuments(); l.reserve(openDocuments.size()); for (auto* d : openDocuments) { l << d->textDocument(); } return l; } KTextEditor::Document *Application::openUrl(const QUrl &url, const QString &encoding) { Q_UNUSED(encoding); auto documentController = Core::self()->documentControllerInternal(); auto doc = url.isEmpty() ? documentController->openDocumentFromText(QString()) : documentController->openDocument(url); return doc->textDocument(); } MainWindow::MainWindow(KDevelop::MainWindow *mainWindow) : m_mainWindow(mainWindow) , m_interface(new KTextEditor::MainWindow(this)) { connect(mainWindow, &Sublime::MainWindow::viewAdded, this, [this] (Sublime::View *view) { if (auto kteView = toKteView(view)) { emit m_interface->viewCreated(kteView); } }); connect(mainWindow, &Sublime::MainWindow::activeViewChanged, this, [this] (Sublime::View *view) { auto kteView = toKteView(view); emit m_interface->viewChanged(kteView); if (auto viewBar = m_viewBars.value(kteView)) { m_mainWindow->viewBarContainer()->setCurrentViewBar(viewBar); } }); } MainWindow::~MainWindow() = default; QWidget *MainWindow::createToolView(KTextEditor::Plugin* plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text) { auto factory = new ToolViewFactory(text, icon, identifier, pos); Core::self()->uiController()->addToolView(text, factory); connect(plugin, &QObject::destroyed, this, [=] { Core::self()->uiController()->removeToolView(factory); }); return factory->container(); } KXMLGUIFactory *MainWindow::guiFactory() const { if (!m_mainWindow) { return nullptr; } return m_mainWindow->guiFactory(); } QWidget *MainWindow::window() const { return m_mainWindow; } QList MainWindow::views() const { QList views; if (m_mainWindow) { foreach (auto area, m_mainWindow->areas()) { foreach (auto view, area->views()) { if (auto kteView = toKteView(view)) { views << kteView; } } } } return views; } KTextEditor::View *MainWindow::activeView() const { if (!m_mainWindow) { return nullptr; } return toKteView(m_mainWindow->activeView()); } KTextEditor::View *MainWindow::activateView(KTextEditor::Document *doc) { if (!m_mainWindow) { return nullptr; } foreach (auto area, m_mainWindow->areas()) { foreach (auto view, area->views()) { if (auto kteView = toKteView(view)) { if (kteView->document() == doc) { m_mainWindow->activateView(view); return kteView; } } } } return activeView(); } QObject *MainWindow::pluginView(const QString &id) const { return m_pluginViews.value(id); } QWidget *MainWindow::createViewBar(KTextEditor::View *view) { Q_UNUSED(view); if (!m_mainWindow) { return nullptr; } // we reuse the central view bar for every view return m_mainWindow->viewBarContainer(); } void MainWindow::deleteViewBar(KTextEditor::View *view) { auto viewBar = m_viewBars.take(view); if (m_mainWindow) { m_mainWindow->viewBarContainer()->removeViewBar(viewBar); } delete viewBar; } void MainWindow::showViewBar(KTextEditor::View *view) { auto viewBar = m_viewBars.value(view); Q_ASSERT(viewBar); if (m_mainWindow) { m_mainWindow->viewBarContainer()->showViewBar(viewBar); } } void MainWindow::hideViewBar(KTextEditor::View *view) { auto viewBar = m_viewBars.value(view); Q_ASSERT(viewBar); if (m_mainWindow) { m_mainWindow->viewBarContainer()->hideViewBar(viewBar); } } void MainWindow::addWidgetToViewBar(KTextEditor::View *view, QWidget *widget) { Q_ASSERT(widget); m_viewBars[view] = widget; if (m_mainWindow) { m_mainWindow->viewBarContainer()->addViewBar(widget); } } KTextEditor::MainWindow *MainWindow::interface() const { return m_interface; } void MainWindow::addPluginView(const QString &id, QObject *view) { m_pluginViews.insert(id, view); emit m_interface->pluginViewCreated(id, view); } void MainWindow::removePluginView(const QString &id) { auto view = m_pluginViews.take(id).data(); delete view; emit m_interface->pluginViewDeleted(id, view); } void MainWindow::startDestroy() { m_mainWindow = nullptr; deleteLater(); } Plugin::Plugin(KTextEditor::Plugin *plugin, QObject *parent) : IPlugin({}, parent) , m_plugin(plugin) , m_tracker(new ObjectListTracker(ObjectListTracker::CleanupWhenDone, this)) { } Plugin::~Plugin() = default; void Plugin::unload() { if (auto mainWindow = KTextEditor::Editor::instance()->application()->activeMainWindow()) { auto integration = dynamic_cast(mainWindow->parent()); if (integration) { integration->removePluginView(pluginId()); } } m_tracker->deleteAll(); delete m_plugin; } KXMLGUIClient *Plugin::createGUIForMainWindow(Sublime::MainWindow* window) { auto ret = IPlugin::createGUIForMainWindow(window); auto mainWindow = dynamic_cast(window); Q_ASSERT(mainWindow); auto wrapper = mainWindow->kateWrapper(); auto view = m_plugin->createView(wrapper->interface()); wrapper->addPluginView(pluginId(), view); // ensure that unloading the plugin kills all views m_tracker->append(view); return ret; } KTextEditor::Plugin *Plugin::interface() const { return m_plugin.data(); } QString Plugin::pluginId() const { return Core::self()->pluginController()->pluginInfo(this).pluginId(); } void initialize() { auto app = new KTextEditor::Application(new Application(Core::self())); KTextEditor::Editor::instance()->setApplication(app); } void MainWindow::splitView(Qt::Orientation orientation) { if (m_mainWindow) { m_mainWindow->split(orientation); } } } #include "ktexteditorpluginintegration.moc" diff --git a/kdevplatform/shell/languagecontroller.cpp b/kdevplatform/shell/languagecontroller.cpp index e4a1246350..1da0172bac 100644 --- a/kdevplatform/shell/languagecontroller.cpp +++ b/kdevplatform/shell/languagecontroller.cpp @@ -1,381 +1,382 @@ /*************************************************************************** * Copyright 2006 Adam Treat * * Copyright 2007 Alexander Dymo * * * * 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 "languagecontroller.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include #include "problemmodelset.h" #include "core.h" #include "settings/languagepreferences.h" #include "completionsettings.h" #include "debug.h" namespace { QString KEY_SupportedMimeTypes() { return QStringLiteral("X-KDevelop-SupportedMimeTypes"); } QString KEY_ILanguageSupport() { return QStringLiteral("ILanguageSupport"); } } #if QT_VERSION < 0x050600 inline uint qHash(const QMimeType& mime, uint seed = 0) { return qHash(mime.name(), seed); } #endif namespace KDevelop { typedef QHash LanguageHash; typedef QHash > LanguageCache; class LanguageControllerPrivate { public: explicit LanguageControllerPrivate(LanguageController *controller) : dataMutex(QMutex::Recursive) , backgroundParser(new BackgroundParser(controller)) , staticAssistantsManager(nullptr) , m_cleanedUp(false) , problemModelSet(new ProblemModelSet(controller)) , m_controller(controller) {} void documentActivated(KDevelop::IDocument *document) { QUrl url = document->url(); if (!url.isValid()) { return; } activeLanguages.clear(); const QList languages = m_controller->languagesForUrl(url); activeLanguages.reserve(languages.size()); for (const auto lang : languages) { activeLanguages << lang; } } QList activeLanguages; mutable QMutex dataMutex; LanguageHash languages; //Maps language-names to languages LanguageCache languageCache; //Maps mimetype-names to languages typedef QMultiHash MimeTypeCache; MimeTypeCache mimeTypeCache; //Maps mimetypes to languages BackgroundParser *backgroundParser; StaticAssistantsManager* staticAssistantsManager; bool m_cleanedUp; void addLanguageSupport(ILanguageSupport* support, const QStringList& mimetypes); void addLanguageSupport(ILanguageSupport* support); ProblemModelSet *problemModelSet; private: LanguageController *m_controller; }; void LanguageControllerPrivate::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes) { Q_ASSERT(!languages.contains(languageSupport->name())); languages.insert(languageSupport->name(), languageSupport); for (const QString& mimeTypeName : mimetypes) { qCDebug(SHELL) << "adding supported mimetype:" << mimeTypeName << "language:" << languageSupport->name(); languageCache[mimeTypeName] << languageSupport; QMimeType mime = QMimeDatabase().mimeTypeForName(mimeTypeName); if (mime.isValid()) { mimeTypeCache.insert(mime, languageSupport); } else { qCWarning(SHELL) << "could not create mime-type" << mimeTypeName; } } } void LanguageControllerPrivate::addLanguageSupport(KDevelop::ILanguageSupport* languageSupport) { if (languages.contains(languageSupport->name())) return; Q_ASSERT(dynamic_cast(languageSupport)); KPluginMetaData info = Core::self()->pluginController()->pluginInfo(dynamic_cast(languageSupport)); QStringList mimetypes = KPluginMetaData::readStringList(info.rawData(), KEY_SupportedMimeTypes()); addLanguageSupport(languageSupport, mimetypes); } LanguageController::LanguageController(QObject *parent) : ILanguageController(parent) , d(new LanguageControllerPrivate(this)) { setObjectName(QStringLiteral("LanguageController")); } LanguageController::~LanguageController() = default; void LanguageController::initialize() { d->backgroundParser->loadSettings(); d->staticAssistantsManager = new StaticAssistantsManager(this); // make sure the DUChain is setup before we try to access it from different threads at the same time DUChain::self(); connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, [&] (IDocument* document) { d->documentActivated(document); }); } void LanguageController::cleanup() { QMutexLocker lock(&d->dataMutex); d->m_cleanedUp = true; } QList LanguageController::activeLanguages() { QMutexLocker lock(&d->dataMutex); return d->activeLanguages; } StaticAssistantsManager* LanguageController::staticAssistantsManager() const { return d->staticAssistantsManager; } ICompletionSettings *LanguageController::completionSettings() const { return &CompletionSettings::self(); } ProblemModelSet* LanguageController::problemModelSet() const { return d->problemModelSet; } QList LanguageController::loadedLanguages() const { QMutexLocker lock(&d->dataMutex); QList ret; if(d->m_cleanedUp) return ret; ret.reserve(d->languages.size()); foreach(ILanguageSupport* lang, d->languages) ret << lang; return ret; } ILanguageSupport* LanguageController::language(const QString &name) const { QMutexLocker lock(&d->dataMutex); if(d->m_cleanedUp) return nullptr; if(d->languages.contains(name)) return d->languages[name]; // temporary support for deprecated-in-5.1 "X-KDevelop-Language" as fallback // remove in later version const QString keys[2] = { QStringLiteral("X-KDevelop-Languages"), QStringLiteral("X-KDevelop-Language") }; QList supports; for (const auto& key : keys) { QVariantMap constraints; constraints.insert(key, name); supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); if (key == keys[1]) { - for (auto support : supports) { + for (auto support : qAsConst(supports)) { qCWarning(SHELL) << "Plugin" << Core::self()->pluginController()->pluginInfo(support).name() << " has deprecated (since 5.1) metadata key \"X-KDevelop-Language\", needs porting to: \"X-KDevelop-Languages\": ["<extension(); if(languageSupport) { d->addLanguageSupport(languageSupport); return languageSupport; } } return nullptr; } bool isNumeric(const QString& str) { int len = str.length(); if(len == 0) return false; for(int a = 0; a < len; ++a) if(!str[a].isNumber()) return false; return true; } QList LanguageController::languagesForUrl(const QUrl &url) { QMutexLocker lock(&d->dataMutex); QList languages; if(d->m_cleanedUp) return languages; const QString fileName = url.fileName(); ///TODO: cache regexp or simple string pattern for endsWith matching QRegExp exp(QString(), Qt::CaseInsensitive, QRegExp::Wildcard); ///non-crashy part: Use the mime-types of known languages for(LanguageControllerPrivate::MimeTypeCache::const_iterator it = d->mimeTypeCache.constBegin(); it != d->mimeTypeCache.constEnd(); ++it) { foreach(const QString& pattern, it.key().globPatterns()) { if(pattern.startsWith(QLatin1Char('*'))) { const QStringRef subPattern = pattern.midRef(1); if (!subPattern.contains(QLatin1Char('*'))) { //optimize: we can skip the expensive QRegExp in this case //and do a simple string compare (much faster) if (fileName.endsWith(subPattern)) { languages << *it; } continue; } } exp.setPattern(pattern); if(int position = exp.indexIn(fileName)) { if(position != -1 && exp.matchedLength() + position == fileName.length()) languages << *it; } } } if(!languages.isEmpty()) return languages; //Never use findByUrl from within a background thread, and never load a language support //from within the backgruond thread. Both is unsafe, and can lead to crashes if(!languages.isEmpty() || QThread::currentThread() != thread()) return languages; QMimeType mimeType; if (url.isLocalFile()) { mimeType = QMimeDatabase().mimeTypeForFile(url.toLocalFile()); } else { // remote file, only look at the extension mimeType = QMimeDatabase().mimeTypeForUrl(url); } if (mimeType.isDefault()) { // ask the document controller about a more concrete mimetype IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (doc) { mimeType = doc->mimeType(); } } languages = languagesForMimetype(mimeType.name()); return languages; } QList LanguageController::languagesForMimetype(const QString& mimetype) { QMutexLocker lock(&d->dataMutex); QList languages; LanguageCache::ConstIterator it = d->languageCache.constFind(mimetype); if (it != d->languageCache.constEnd()) { languages = it.value(); } else { QVariantMap constraints; constraints.insert(KEY_SupportedMimeTypes(), mimetype); const QList supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); if (supports.isEmpty()) { qCDebug(SHELL) << "no languages for mimetype:" << mimetype; d->languageCache.insert(mimetype, QList()); } else { for (IPlugin *support : supports) { ILanguageSupport* languageSupport = support->extension(); qCDebug(SHELL) << "language-support:" << languageSupport; if(languageSupport) { d->addLanguageSupport(languageSupport); languages << languageSupport; } } } } return languages; } QList LanguageController::mimetypesForLanguageName(const QString& languageName) { QMutexLocker lock(&d->dataMutex); QList mimetypes; for (LanguageCache::ConstIterator iter = d->languageCache.constBegin(); iter != d->languageCache.constEnd(); ++iter) { foreach (ILanguageSupport* language, iter.value()) { if (language->name() == languageName) { mimetypes << iter.key(); break; } } } return mimetypes; } BackgroundParser *LanguageController::backgroundParser() const { return d->backgroundParser; } void LanguageController::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes) { d->addLanguageSupport(languageSupport, mimetypes); } } #include "moc_languagecontroller.cpp" diff --git a/kdevplatform/shell/settings/templatepage.cpp b/kdevplatform/shell/settings/templatepage.cpp index 5aeff857a8..aa44bb45dd 100644 --- a/kdevplatform/shell/settings/templatepage.cpp +++ b/kdevplatform/shell/settings/templatepage.cpp @@ -1,140 +1,141 @@ /* * This file is part of KDevelop * Copyright 2012 Miha Čančula * * 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 "templatepage.h" #include "ui_templatepage.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include TemplatePage::TemplatePage (KDevelop::ITemplateProvider* provider, QWidget* parent) : QWidget (parent), m_provider(provider) { ui = new Ui::TemplatePage; ui->setupUi(this); ui->getNewButton->setVisible(!m_provider->knsConfigurationFile().isEmpty()); connect(ui->getNewButton, &QPushButton::clicked, this, &TemplatePage::getMoreTemplates); ui->shareButton->setVisible(!m_provider->knsConfigurationFile().isEmpty()); connect(ui->shareButton, &QPushButton::clicked, this, &TemplatePage::shareTemplates); ui->loadButton->setVisible(!m_provider->supportedMimeTypes().isEmpty()); connect(ui->loadButton, &QPushButton::clicked, this, &TemplatePage::loadFromFile); ui->extractButton->setEnabled(false); connect(ui->extractButton, &QPushButton::clicked, this, &TemplatePage::extractTemplate); provider->reload(); ui->treeView->setModel(provider->templatesModel()); ui->treeView->expandAll(); connect(ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TemplatePage::currentIndexChanged); } TemplatePage::~TemplatePage() { delete ui; } void TemplatePage::loadFromFile() { KDevelop::ScopedDialog fileDialog(this); fileDialog->setMimeTypeFilters(m_provider->supportedMimeTypes()); fileDialog->setFileMode(QFileDialog::ExistingFiles); if (!fileDialog->exec()) { return; } - for (const auto& file : fileDialog->selectedFiles()) { + const auto& selectedFiles = fileDialog->selectedFiles(); + for (const auto& file : selectedFiles) { m_provider->loadTemplate(file); } m_provider->reload(); } void TemplatePage::getMoreTemplates() { KDevelop::ScopedDialog dialog(m_provider->knsConfigurationFile(), this); if (!dialog->exec()) { return; } if (!dialog->changedEntries().isEmpty()) { m_provider->reload(); } } void TemplatePage::shareTemplates() { KDevelop::ScopedDialog dialog(m_provider->knsConfigurationFile(), this); dialog->exec(); } void TemplatePage::currentIndexChanged(const QModelIndex& index) { QString archive = ui->treeView->model()->data(index, KDevelop::TemplatesModel::ArchiveFileRole).toString(); ui->extractButton->setEnabled(QFileInfo::exists(archive)); } void TemplatePage::extractTemplate() { QModelIndex index = ui->treeView->currentIndex(); QString archiveName= ui->treeView->model()->data(index, KDevelop::TemplatesModel::ArchiveFileRole).toString(); QFileInfo info(archiveName); if (!info.exists()) { ui->extractButton->setEnabled(false); return; } QScopedPointer archive; if (info.suffix() == QLatin1String("zip")) { archive.reset(new KZip(archiveName)); } else { archive.reset(new KTar(archiveName)); } archive->open(QIODevice::ReadOnly); const QString destination = QFileDialog::getExistingDirectory() + QLatin1Char('/') + info.baseName(); archive->directory()->copyTo(destination); } diff --git a/kdevplatform/shell/sourceformatterselectionedit.cpp b/kdevplatform/shell/sourceformatterselectionedit.cpp index b0475291b1..c43def5134 100644 --- a/kdevplatform/shell/sourceformatterselectionedit.cpp +++ b/kdevplatform/shell/sourceformatterselectionedit.cpp @@ -1,576 +1,578 @@ /* This file is part of KDevelop * * Copyright (C) 2008 Cédric Pasteur * Copyright (C) 2017 Friedrich W. H. Kossebau * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "sourceformatterselectionedit.h" #include "ui_sourceformatterselectionedit.h" #include "sourceformattercontroller.h" #include "settings/editstyledialog.h" #include "debug.h" #include "core.h" #include "plugincontroller.h" #include #include #include // TODO: remove later +#include #include #include #include #include #include #include #include #include #define STYLE_ROLE (Qt::UserRole+1) using namespace KDevelop; namespace { namespace Strings { QString userStylePrefix() { return QStringLiteral("User"); } } } struct LanguageSettings { QList mimetypes; QSet formatters; // weak pointers to selected formatter and style, no ownership KDevelop::SourceFormatter* selectedFormatter = nullptr; // Should never be zero KDevelop::SourceFormatterStyle* selectedStyle = nullptr; // TODO: can this be zero? Assume that not }; typedef QMap LanguageMap; typedef QMap FormatterMap; class KDevelop::SourceFormatterSelectionEditPrivate { public: Ui::SourceFormatterSelectionEdit ui; // Language name -> language settings LanguageMap languages; // formatter name -> formatter. Formatters owned by this FormatterMap formatters; KTextEditor::Document* document; KTextEditor::View* view; }; SourceFormatterSelectionEdit::SourceFormatterSelectionEdit(QWidget* parent) : QWidget(parent) , d(new SourceFormatterSelectionEditPrivate) { d->ui.setupUi(this); connect(d->ui.cbLanguages, static_cast(&KComboBox::currentIndexChanged), this, &SourceFormatterSelectionEdit::selectLanguage); connect(d->ui.cbFormatters, static_cast(&KComboBox::currentIndexChanged), this, &SourceFormatterSelectionEdit::selectFormatter); connect(d->ui.styleList, &QListWidget::currentRowChanged, this, &SourceFormatterSelectionEdit::selectStyle); connect(d->ui.btnDelStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::deleteStyle); connect(d->ui.btnNewStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::newStyle); connect(d->ui.btnEditStyle, &QPushButton::clicked, this, &SourceFormatterSelectionEdit::editStyle); connect(d->ui.styleList, &QListWidget::itemChanged, this, &SourceFormatterSelectionEdit::styleNameChanged); d->document = KTextEditor::Editor::instance()->createDocument(this); d->document->setReadWrite(false); d->view = d->document->createView(d->ui.textEditor); d->view->setStatusBarEnabled(false); QVBoxLayout *layout2 = new QVBoxLayout(d->ui.textEditor); layout2->setMargin(0); layout2->addWidget(d->view); d->ui.textEditor->setLayout(layout2); d->view->show(); KTextEditor::ConfigInterface *iface = qobject_cast(d->view); if (iface) { iface->setConfigValue(QStringLiteral("dynamic-word-wrap"), false); iface->setConfigValue(QStringLiteral("icon-bar"), false); } SourceFormatterController* controller = Core::self()->sourceFormatterControllerInternal(); connect(controller, &SourceFormatterController::formatterLoaded, this, &SourceFormatterSelectionEdit::addSourceFormatter); connect(controller, &SourceFormatterController::formatterUnloading, this, &SourceFormatterSelectionEdit::removeSourceFormatter); - for (auto* formatter : controller->formatters()) { + const auto& formatters = controller->formatters(); + for (auto* formatter : formatters) { addSourceFormatter(formatter); } } SourceFormatterSelectionEdit::~SourceFormatterSelectionEdit() { qDeleteAll(d->formatters); } static void selectAvailableStyle(LanguageSettings& lang) { Q_ASSERT(!lang.selectedFormatter->styles.empty()); lang.selectedStyle = *lang.selectedFormatter->styles.begin(); } void SourceFormatterSelectionEdit::addSourceFormatter(ISourceFormatter* ifmt) { qCDebug(SHELL) << "Adding source formatter:" << ifmt->name(); SourceFormatter* formatter; FormatterMap::const_iterator iter = d->formatters.constFind(ifmt->name()); if (iter == d->formatters.constEnd()) { formatter = Core::self()->sourceFormatterControllerInternal()->createFormatterForPlugin(ifmt); d->formatters[ifmt->name()] = formatter; } else { qCWarning(SHELL) << "formatter plugin" << ifmt->name() << "loading which was already seen before by SourceFormatterSelectionEdit"; return; } foreach ( const SourceFormatterStyle* style, formatter->styles ) { foreach ( const SourceFormatterStyle::MimeHighlightPair& item, style->mimeTypes() ) { QMimeType mime = QMimeDatabase().mimeTypeForName(item.mimeType); if (!mime.isValid()) { qCWarning(SHELL) << "formatter plugin" << ifmt->name() << "supports unknown mimetype entry" << item.mimeType; continue; } QString languageName = item.highlightMode; LanguageSettings& l = d->languages[languageName]; l.mimetypes.append(mime); l.formatters.insert( formatter ); // init selection if needed if (!l.selectedFormatter) { l.selectedFormatter = formatter; selectAvailableStyle(l); } } } resetUi(); } void SourceFormatterSelectionEdit::removeSourceFormatter(ISourceFormatter* ifmt) { qCDebug(SHELL) << "Removing source formatter:" << ifmt->name(); auto iter = d->formatters.find(ifmt->name()); if (iter == d->formatters.end()) { qCWarning(SHELL) << "formatter plugin" << ifmt->name() << "unloading which was not seen before by SourceFormatterSelectionEdit"; return; } d->formatters.erase(iter); auto formatter = iter.value(); auto languageIter = d->languages.begin(); while (languageIter != d->languages.end()) { LanguageSettings& l = languageIter.value(); l.formatters.remove(formatter); if (l.formatters.isEmpty()) { languageIter = d->languages.erase(languageIter); } else { // reset selected formatter if needed if (l.selectedFormatter == formatter) { l.selectedFormatter = *l.formatters.begin(); selectAvailableStyle(l); } ++languageIter; } } delete formatter; resetUi(); } void SourceFormatterSelectionEdit::loadSettings(const KConfigGroup& config) { for (auto languageIter = d->languages.begin(); languageIter != d->languages.end(); ++languageIter) { // Pick the first appropriate mimetype for this language LanguageSettings& l = languageIter.value(); const QList mimetypes = l.mimetypes; for (const QMimeType& mimetype : mimetypes) { QStringList formatterAndStyleName = config.readEntry(mimetype.name(), QString()).split(QStringLiteral("||"), QString::KeepEmptyParts); FormatterMap::const_iterator formatterIter = d->formatters.constFind(formatterAndStyleName.first()); if (formatterIter == d->formatters.constEnd()) { qCDebug(SHELL) << "Reference to unknown formatter" << formatterAndStyleName.first(); Q_ASSERT(!l.formatters.empty()); // otherwise there should be no entry for 'name' l.selectedFormatter = *l.formatters.begin(); selectAvailableStyle(l); } else { l.selectedFormatter = formatterIter.value(); SourceFormatter::StyleMap::const_iterator styleIter = l.selectedFormatter->styles.constFind(formatterAndStyleName.at( 1 )); if (styleIter == l.selectedFormatter->styles.constEnd()) { qCDebug(SHELL) << "No style" << formatterAndStyleName.at( 1 ) << "found for formatter" << formatterAndStyleName.first(); selectAvailableStyle(l); } else { l.selectedStyle = styleIter.value(); } } } if (!l.selectedFormatter) { Q_ASSERT(!l.formatters.empty()); l.selectedFormatter = *l.formatters.begin(); } if (!l.selectedStyle) { selectAvailableStyle(l); } } resetUi(); } void SourceFormatterSelectionEdit::resetUi() { qCDebug(SHELL) << "Resetting UI"; // Sort the languages, preferring firstly active, then loaded languages QList sortedLanguages; foreach(const auto language, KDevelop::ICore::self()->languageController()->activeLanguages() + KDevelop::ICore::self()->languageController()->loadedLanguages()) { if (d->languages.contains(language->name()) && !sortedLanguages.contains(language->name())) { sortedLanguages.push_back( language->name() ); } } foreach (const QString& name, d->languages.keys()) { if( !sortedLanguages.contains( name ) ) sortedLanguages.push_back( name ); } bool b = blockSignals( true ); d->ui.cbLanguages->blockSignals(!b); d->ui.cbFormatters->blockSignals(!b); d->ui.styleList->blockSignals(!b); d->ui.cbLanguages->clear(); d->ui.cbFormatters->clear(); d->ui.styleList->clear(); foreach( const QString& name, sortedLanguages ) { d->ui.cbLanguages->addItem(name); } if (d->ui.cbLanguages->count() == 0) { d->ui.cbLanguages->setEnabled(false); selectLanguage( -1 ); } else { d->ui.cbLanguages->setCurrentIndex(0); d->ui.cbLanguages->setEnabled(true); selectLanguage( 0 ); } updatePreview(); blockSignals( b ); d->ui.cbLanguages->blockSignals(b); d->ui.cbFormatters->blockSignals(b); d->ui.styleList->blockSignals(b); } void SourceFormatterSelectionEdit::saveSettings(KConfigGroup& config) { // store formatters globally KConfigGroup globalConfig = Core::self()->sourceFormatterControllerInternal()->globalConfig(); foreach (SourceFormatter* fmt, d->formatters) { KConfigGroup fmtgrp = globalConfig.group( fmt->formatter->name() ); // delete all styles so we don't leave any behind when all user styles are deleted foreach( const QString& subgrp, fmtgrp.groupList() ) { if( subgrp.startsWith( Strings::userStylePrefix() ) ) { fmtgrp.deleteGroup( subgrp ); } } foreach( const SourceFormatterStyle* style, fmt->styles ) { if( style->name().startsWith( Strings::userStylePrefix() ) ) { KConfigGroup stylegrp = fmtgrp.group( style->name() ); stylegrp.writeEntry( SourceFormatterController::styleCaptionKey(), style->caption() ); stylegrp.writeEntry( SourceFormatterController::styleContentKey(), style->content() ); stylegrp.writeEntry( SourceFormatterController::styleMimeTypesKey(), style->mimeTypesVariant() ); stylegrp.writeEntry( SourceFormatterController::styleSampleKey(), style->overrideSample() ); } } } globalConfig.sync(); // store selected formatters in given language - for (const auto& setting : d->languages) { + for (const auto& setting : qAsConst(d->languages)) { for(const auto& mime : setting.mimetypes) { const QString formatterId = setting.selectedFormatter->formatter->name() + QLatin1String("||") + setting.selectedStyle->name(); config.writeEntry(mime.name(), formatterId); } } } void SourceFormatterSelectionEdit::enableStyleButtons() { bool userEntry = d->ui.styleList->currentItem() && d->ui.styleList->currentItem()->data(STYLE_ROLE).toString().startsWith(Strings::userStylePrefix()); QString languageName = d->ui.cbLanguages->currentText(); QMap::const_iterator it = d->languages.constFind(languageName); bool hasEditWidget = false; if (it != d->languages.constEnd()) { const LanguageSettings& l = it.value(); Q_ASSERT(l.selectedFormatter); ISourceFormatter* fmt = l.selectedFormatter->formatter; hasEditWidget = ( fmt && QScopedPointer(fmt->editStyleWidget( l.mimetypes.first() )) ); } d->ui.btnDelStyle->setEnabled(userEntry); d->ui.btnEditStyle->setEnabled(userEntry && hasEditWidget); d->ui.btnNewStyle->setEnabled(d->ui.cbFormatters->currentIndex() >= 0 && hasEditWidget); } void SourceFormatterSelectionEdit::selectLanguage( int idx ) { d->ui.cbFormatters->clear(); if( idx < 0 ) { d->ui.cbFormatters->setEnabled(false); selectFormatter( -1 ); return; } d->ui.cbFormatters->setEnabled(true); { QSignalBlocker blocker(d->ui.cbFormatters); LanguageSettings& l = d->languages[d->ui.cbLanguages->itemText(idx)]; foreach( const SourceFormatter* fmt, l.formatters ) { d->ui.cbFormatters->addItem(fmt->formatter->caption(), fmt->formatter->name()); } d->ui.cbFormatters->setCurrentIndex(d->ui.cbFormatters->findData(l.selectedFormatter->formatter->name())); } selectFormatter(d->ui.cbFormatters->currentIndex()); emit changed(); } void SourceFormatterSelectionEdit::selectFormatter( int idx ) { d->ui.styleList->clear(); if( idx < 0 ) { d->ui.styleList->setEnabled(false); enableStyleButtons(); return; } d->ui.styleList->setEnabled(true); LanguageSettings& l = d->languages[d->ui.cbLanguages->currentText()]; Q_ASSERT( idx < l.formatters.size() ); FormatterMap::const_iterator formatterIter = d->formatters.constFind(d->ui.cbFormatters->itemData(idx).toString()); Q_ASSERT( formatterIter != d->formatters.constEnd() ); Q_ASSERT( l.formatters.contains(formatterIter.value()) ); if (l.selectedFormatter != formatterIter.value()) { l.selectedFormatter = formatterIter.value(); l.selectedStyle = nullptr; // will hold 0 until a style is picked } foreach( const SourceFormatterStyle* style, formatterIter.value()->styles ) { if (!style->supportsLanguage(d->ui.cbLanguages->currentText())) { // do not list items which do not support the selected language continue; } QListWidgetItem* item = addStyle( *style ); if (style == l.selectedStyle) { d->ui.styleList->setCurrentItem(item); } } if (l.selectedStyle == nullptr) { d->ui.styleList->setCurrentRow(0); } enableStyleButtons(); emit changed(); } void SourceFormatterSelectionEdit::selectStyle( int row ) { if( row < 0 ) { enableStyleButtons(); return; } d->ui.styleList->setCurrentRow(row); LanguageSettings& l = d->languages[d->ui.cbLanguages->currentText()]; l.selectedStyle = l.selectedFormatter->styles[d->ui.styleList->item(row)->data(STYLE_ROLE).toString()]; enableStyleButtons(); updatePreview(); emit changed(); } void SourceFormatterSelectionEdit::deleteStyle() { Q_ASSERT( d->ui.styleList->currentRow() >= 0 ); QListWidgetItem* item = d->ui.styleList->currentItem(); LanguageSettings& l = d->languages[d->ui.cbLanguages->currentText()]; SourceFormatter* fmt = l.selectedFormatter; SourceFormatter::StyleMap::iterator styleIter = fmt->styles.find(item->data( STYLE_ROLE ).toString()); QStringList otherLanguageNames; QList otherlanguages; for (LanguageMap::iterator languageIter = d->languages.begin(); languageIter != d->languages.end(); ++languageIter) { if ( &languageIter.value() != &l && languageIter.value().selectedStyle == styleIter.value() ) { otherLanguageNames.append(languageIter.key()); otherlanguages.append(&languageIter.value()); } } if (!otherLanguageNames.empty() && KMessageBox::warningContinueCancel(this, i18n("The style %1 is also used for the following languages:\n%2.\nAre you sure you want to delete it?", styleIter.value()->caption(), otherLanguageNames.join(QLatin1Char('\n'))), i18n("Style being deleted")) != KMessageBox::Continue) { return; } d->ui.styleList->takeItem(d->ui.styleList->currentRow()); fmt->styles.erase(styleIter); delete item; selectStyle(d->ui.styleList->count() > 0 ? 0 : -1); foreach (LanguageSettings* lang, otherlanguages) { selectAvailableStyle(*lang); } updatePreview(); emit changed(); } void SourceFormatterSelectionEdit::editStyle() { QString language = d->ui.cbLanguages->currentText(); Q_ASSERT( d->languages.contains(language) ); LanguageSettings& l = d->languages[language]; SourceFormatter* fmt = l.selectedFormatter; QMimeType mimetype = l.mimetypes.first(); if( QScopedPointer(fmt->formatter->editStyleWidget( mimetype )) ) { KDevelop::ScopedDialog dlg(fmt->formatter, mimetype, *l.selectedStyle, this); if( dlg->exec() == QDialog::Accepted ) { l.selectedStyle->setContent(dlg->content()); } updatePreview(); emit changed(); } } void SourceFormatterSelectionEdit::newStyle() { QListWidgetItem* item = d->ui.styleList->currentItem(); LanguageSettings& l = d->languages[d->ui.cbLanguages->currentText()]; SourceFormatter* fmt = l.selectedFormatter; int idx = 0; for (int i = 0; i < d->ui.styleList->count(); ++i) { QString name = d->ui.styleList->item(i)->data(STYLE_ROLE).toString(); if( name.startsWith( Strings::userStylePrefix() ) && name.midRef( Strings::userStylePrefix().length() ).toInt() >= idx ) { idx = name.midRef( Strings::userStylePrefix().length() ).toInt(); } } // Increase number for next style idx++; SourceFormatterStyle* s = new SourceFormatterStyle( QStringLiteral( "%1%2" ).arg( Strings::userStylePrefix() ).arg( idx ) ); if( item ) { SourceFormatterStyle* existstyle = fmt->styles[ item->data( STYLE_ROLE ).toString() ]; s->setCaption( i18n( "New %1", existstyle->caption() ) ); s->copyDataFrom( existstyle ); } else { s->setCaption( i18n( "New Style" ) ); } fmt->styles[ s->name() ] = s; QListWidgetItem* newitem = addStyle( *s ); selectStyle(d->ui.styleList->row(newitem)); d->ui.styleList->editItem(newitem); emit changed(); } void SourceFormatterSelectionEdit::styleNameChanged( QListWidgetItem* item ) { if ( !item->isSelected() ) { return; } LanguageSettings& l = d->languages[d->ui.cbLanguages->currentText()]; l.selectedStyle->setCaption( item->text() ); emit changed(); } QListWidgetItem* SourceFormatterSelectionEdit::addStyle( const SourceFormatterStyle& s ) { QListWidgetItem* item = new QListWidgetItem(d->ui.styleList); item->setText( s.caption() ); item->setData( STYLE_ROLE, s.name() ); if( s.name().startsWith( Strings::userStylePrefix() ) ) { item->setFlags( item->flags() | Qt::ItemIsEditable ); } d->ui.styleList->addItem(item); return item; } void SourceFormatterSelectionEdit::updatePreview() { d->document->setReadWrite(true); QString langName = d->ui.cbLanguages->itemText(d->ui.cbLanguages->currentIndex()); if( !langName.isEmpty() ) { LanguageSettings& l = d->languages[langName]; SourceFormatter* fmt = l.selectedFormatter; SourceFormatterStyle* style = l.selectedStyle; d->ui.descriptionLabel->setText(style->description()); if( style->usePreview() ) { ISourceFormatter* ifmt = fmt->formatter; QMimeType mime = l.mimetypes.first(); d->document->setHighlightingMode(style->modeForMimetype(mime)); //NOTE: this is ugly, but otherwise kate might remove tabs again :-/ // see also: https://bugs.kde.org/show_bug.cgi?id=291074 KTextEditor::ConfigInterface* iface = qobject_cast(d->document); QVariant oldReplaceTabs; if (iface) { oldReplaceTabs = iface->configValue(QStringLiteral("replace-tabs")); iface->setConfigValue(QStringLiteral("replace-tabs"), false); } d->document->setText(ifmt->formatSourceWithStyle(*style, ifmt->previewText(*style, mime), QUrl(), mime)); if (iface) { iface->setConfigValue(QStringLiteral("replace-tabs"), oldReplaceTabs); } d->ui.previewLabel->show(); d->ui.textEditor->show(); }else{ d->ui.previewLabel->hide(); d->ui.textEditor->hide(); } } else { d->document->setText(i18n("No language selected")); } d->view->setCursorPosition(KTextEditor::Cursor(0, 0)); d->document->setReadWrite(false); } diff --git a/plugins/appwizard/appwizardplugin.cpp b/plugins/appwizard/appwizardplugin.cpp index 2dcc1d86bf..366e3dc208 100644 --- a/plugins/appwizard/appwizardplugin.cpp +++ b/plugins/appwizard/appwizardplugin.cpp @@ -1,566 +1,567 @@ /*************************************************************************** * Copyright 2001 Bernd Gehrmann * * Copyright 2004-2005 Sascha Cunz * * Copyright 2005 Ian Reinhart Geiser * * Copyright 2007 Alexander Dymo * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "appwizardplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "appwizarddialog.h" #include "projectselectionpage.h" #include "projectvcspage.h" #include "projecttemplatesmodel.h" #include "debug.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin();) AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent) , m_templatesModel(nullptr) { setXMLFile(QStringLiteral("kdevappwizard.rc")); m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new")); m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); m_newFromTemplate->setText(i18n("New From Template...")); connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject); m_newFromTemplate->setToolTip( i18n("Generate a new project from a template") ); m_newFromTemplate->setWhatsThis( i18n("This starts KDevelop's application wizard. " "It helps you to generate a skeleton for your " "application from a set of templates.") ); } AppWizardPlugin::~AppWizardPlugin() { } void AppWizardPlugin::slotNewProject() { model()->refresh(); ScopedDialog dlg(core()->pluginController(), m_templatesModel); if (dlg->exec() == QDialog::Accepted) { QString project = createProject( dlg->appInfo() ); if (!project.isEmpty()) { core()->projectController()->openProject(QUrl::fromLocalFile(project)); KConfig templateConfig(dlg->appInfo().appTemplate); KConfigGroup general(&templateConfig, "General"); const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), QString::SkipEmptyParts); for (const auto& fileArg : fileArgs) { QString file = KMacroExpander::expandMacros(fileArg.trimmed(), m_variables); if (QDir::isRelativePath(file)) { file = m_variables[QStringLiteral("PROJECTDIR")] + QLatin1Char('/') + file; } core()->documentController()->openDocument(QUrl::fromUserInput(file)); } } else { KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n("Could not create project from template\n"), i18n("Failed to create project") ); } } } namespace { IDistributedVersionControl* toDVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } ICentralizedVersionControl* toCVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } /*! Trouble while initializing version control. Show failure message to user. */ void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString()) { QString displayDetails = details; if (displayDetails.isEmpty()) { displayDetails = i18n("Please see the Version Control tool view."); } KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18n("Version Control System Error")); KIO::del(dest, KIO::HideProgressInfo)->exec(); tmpdir.remove(); } /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(dvcs); qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS"; const QUrl& dest = info.location; //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir VcsJob* job = dvcs->init(dest); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest); return false; } qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest; job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest); return false; } job = dvcs->commit(info.importCommitMessage, {dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString()); return false; } return true; // We're good } /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(cvcs); qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to" << info.repository.repositoryServer(); VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } qCDebug(PLUGIN_APPWIZARD) << "Checking out"; job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } return true; // initialization phase complete } QString generateIdentifier( const QString& appname ) { QString tmp = appname; QRegExp re("[^a-zA-Z0-9_]"); return tmp.replace(re, QStringLiteral("_")); } } // end anonymous namespace QString AppWizardPlugin::createProject(const ApplicationInfo& info) { QFileInfo templateInfo(info.appTemplate); if (!templateInfo.exists()) { qCWarning(PLUGIN_APPWIZARD) << "Project app template does not exist:" << info.appTemplate; return QString(); } QString templateName = templateInfo.baseName(); qCDebug(PLUGIN_APPWIZARD) << "Searching archive for template name:" << templateName; QString templateArchive; const QStringList filters = {templateName + QStringLiteral(".*")}; const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory); for (const QString& matchesPath : matchesPaths) { const QStringList files = QDir(matchesPath).entryList(filters); if(!files.isEmpty()) { templateArchive = matchesPath + files.first(); } } if(templateArchive.isEmpty()) { qCWarning(PLUGIN_APPWIZARD) << "Template name does not exist in the template list"; return QString(); } qCDebug(PLUGIN_APPWIZARD) << "Using template archive:" << templateArchive; QUrl dest = info.location; //prepare variable substitution hash m_variables.clear(); m_variables[QStringLiteral("APPNAME")] = info.name; m_variables[QStringLiteral("APPNAMEUC")] = info.name.toUpper(); m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower(); m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name); m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile(); // backwards compatibility m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")]; m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName(); m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName; KArchive* arch = nullptr; if( templateArchive.endsWith(QLatin1String(".zip")) ) { arch = new KZip(templateArchive); } else { arch = new KTar(templateArchive, QStringLiteral("application/x-bzip")); } if (arch->open(QIODevice::ReadOnly)) { QTemporaryDir tmpdir; QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName ); if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension() ) ) { if( !QFileInfo::exists( dest.toLocalFile() ) ) { QDir::root().mkpath( dest.toLocalFile() ); } unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory } else { QUrl url = KIO::upUrl(dest); if(!QFileInfo::exists(url.toLocalFile())) { QDir::root().mkpath(url.toLocalFile()); } } // estimate metadata files which should not be copied QStringList metaDataFileNames; // try by same name const KArchiveEntry *templateEntry = arch->directory()->entry(templateName + QLatin1String(".kdevtemplate")); // but could be different name, if e.g. downloaded, so make a guess if (!templateEntry || !templateEntry->isFile()) { - for (const auto& entryName : arch->directory()->entries()) { + const auto& entries = arch->directory()->entries(); + for (const auto& entryName : entries) { if (entryName.endsWith(QLatin1String(".kdevtemplate"))) { templateEntry = arch->directory()->entry(entryName); break; } } } if (templateEntry && templateEntry->isFile()) { metaDataFileNames << templateEntry->name(); // check if a preview file is to be ignored const KArchiveFile *templateFile = static_cast(templateEntry); QTemporaryDir temporaryDir; templateFile->copyTo(temporaryDir.path()); KConfig config(temporaryDir.path() + QLatin1Char('/') + templateEntry->name()); KConfigGroup group(&config, "General"); if (group.hasKey("Icon")) { const KArchiveEntry* iconEntry = arch->directory()->entry(group.readEntry("Icon")); if (iconEntry && iconEntry->isFile()) { metaDataFileNames << iconEntry->name(); } } } if (!unpackArchive(arch->directory(), unpackDir, metaDataFileNames)) { QString errorMsg = i18n("Could not create new project"); vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir)); return QString(); } if( !info.vcsPluginName.isEmpty() ) { if (!plugin) { // Red Alert, serious program corruption. // This should never happen, the vcs dialog presented a list of vcs // systems and now the chosen system doesn't exist anymore?? tmpdir.remove(); return QString(); } IDistributedVersionControl* dvcs = toDVCS(plugin); ICentralizedVersionControl* cvcs = toCVCS(plugin); bool success = false; if (dvcs) { success = initializeDVCS(dvcs, info, tmpdir); } else if (cvcs) { success = initializeCVCS(cvcs, info, tmpdir); } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel(nullptr, QStringLiteral("Failed to initialize version control system, " "plugin is neither VCS nor DVCS."))) success = true; } if (!success) return QString(); } tmpdir.remove(); }else { qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive"; return QString(); } QString projectFileName = QDir::cleanPath( dest.toLocalFile() + '/' + info.name + ".kdev4" ); // Loop through the new project directory and try to detect the first .kdev4 file. // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the // project templates can be more complex. QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories); if(it.hasNext() == true) { projectFileName = it.next(); } qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ; const QFileInfo projectFileInfo(projectFileName); if (!projectFileInfo.exists()) { qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file"; KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig ); KConfigGroup project = cfg->group( "Project" ); project.writeEntry( "Name", info.name ); QString manager = QStringLiteral("KDevGenericManager"); QDir d( dest.toLocalFile() ); const auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); for (const KPluginMetaData& info : data) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); if (!filter.isEmpty()) { if (!d.entryList(filter).isEmpty()) { manager = info.pluginId(); break; } } } project.writeEntry( "Manager", manager ); project.sync(); cfg->sync(); KConfigGroup project2 = cfg->group( "Project" ); qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" ); } // create developer .kde4 file const QString developerProjectFileName = projectFileInfo.canonicalPath() + QLatin1String("/.kdev4/") + projectFileInfo.fileName(); qCDebug(PLUGIN_APPWIZARD) << "creating developer .kdev4 file:" << developerProjectFileName; KSharedConfigPtr developerCfg = KSharedConfig::openConfig(developerProjectFileName, KConfig::SimpleConfig); KConfigGroup developerProjectGroup = developerCfg->group("Project"); developerProjectGroup.writeEntry("VersionControlSupport", info.vcsPluginName); developerProjectGroup.sync(); developerCfg->sync(); return projectFileName; } bool AppWizardPlugin::unpackArchive(const KArchiveDirectory* dir, const QString& dest, const QStringList& skipList) { qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest; const QStringList entries = dir->entries(); qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QLatin1Char(',')); //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; for (const auto& entryName : entries) { if (skipList.contains(entryName)) { continue; } const auto entry = dir->entry(entryName); if (entry->isDirectory()) { const KArchiveDirectory* subdir = static_cast(entry); QString newdest = dest + '/' + KMacroExpander::expandMacros(subdir->name(), m_variables); if( !QFileInfo::exists( newdest ) ) { QDir::root().mkdir( newdest ); } ret |= unpackArchive(subdir, newdest); } else if (entry->isFile()) { const KArchiveFile* file = static_cast(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, QWidget* parent) { Q_UNUSED(parent); 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 { const QStringList types{ QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), 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/appwizard/projectselectionpage.cpp b/plugins/appwizard/projectselectionpage.cpp index e07e5362bb..d6c9f08b33 100644 --- a/plugins/appwizard/projectselectionpage.cpp +++ b/plugins/appwizard/projectselectionpage.cpp @@ -1,365 +1,366 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * Copyright 2011 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "projectselectionpage.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_projectselectionpage.h" #include "projecttemplatesmodel.h" using namespace KDevelop; ProjectSelectionPage::ProjectSelectionPage(ProjectTemplatesModel *templatesModel, AppWizardDialog *wizardDialog) : AppWizardPageWidget(wizardDialog), m_templatesModel(templatesModel) { ui = new Ui::ProjectSelectionPage(); ui->setupUi(this); setContentsMargins(0,0,0,0); ui->descriptionContent->setBackgroundRole(QPalette::Base); ui->descriptionContent->setForegroundRole(QPalette::Text); ui->locationUrl->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly ); ui->locationUrl->setUrl(KDevelop::ICore::self()->projectController()->projectsBaseDirectory()); ui->locationValidWidget->hide(); ui->locationValidWidget->setMessageType(KMessageWidget::Error); ui->locationValidWidget->setCloseButtonVisible(false); connect( ui->locationUrl->lineEdit(), &KLineEdit::textEdited, this, &ProjectSelectionPage::urlEdited); connect( ui->locationUrl, &KUrlRequester::urlSelected, this, &ProjectSelectionPage::urlEdited); connect( ui->projectNameEdit, &QLineEdit::textEdited, this, &ProjectSelectionPage::nameChanged ); ui->listView->setLevels(2); ui->listView->setHeaderLabels(QStringList{i18n("Category"), i18n("Project Type")}); ui->listView->setModel(templatesModel); ui->listView->setLastLevelViewMode(MultiLevelListView::DirectChildren); connect (ui->listView, &MultiLevelListView::currentIndexChanged, this, &ProjectSelectionPage::typeChanged); typeChanged(ui->listView->currentIndex()); connect( ui->templateType, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSelectionPage::templateChanged ); QPushButton* getMoreButton = new QPushButton(i18n("Get More Templates"), ui->listView); getMoreButton->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); connect (getMoreButton, &QPushButton::clicked, this, &ProjectSelectionPage::moreTemplatesClicked); ui->listView->addWidget(0, getMoreButton); QPushButton* loadButton = new QPushButton(ui->listView); loadButton->setText(i18n("Load Template From File")); loadButton->setIcon(QIcon::fromTheme(QStringLiteral("application-x-archive"))); connect (loadButton, &QPushButton::clicked, this, &ProjectSelectionPage::loadFileClicked); ui->listView->addWidget(0, loadButton); m_wizardDialog = wizardDialog; } void ProjectSelectionPage::nameChanged() { validateData(); emit locationChanged( location() ); } ProjectSelectionPage::~ProjectSelectionPage() { delete ui; } void ProjectSelectionPage::typeChanged(const QModelIndex& idx) { if (!idx.model()) { qCDebug(PLUGIN_APPWIZARD) << "Index with no model"; return; } int children = idx.model()->rowCount(idx); ui->templateType->setVisible(children); ui->templateType->setEnabled(children > 1); if (children) { ui->templateType->setModel(m_templatesModel); ui->templateType->setRootModelIndex(idx); ui->templateType->setCurrentIndex(0); itemChanged(idx.model()->index(0, 0, idx)); } else { itemChanged(idx); } } void ProjectSelectionPage::templateChanged(int current) { QModelIndex idx=m_templatesModel->index(current, 0, ui->templateType->rootModelIndex()); itemChanged(idx); } void ProjectSelectionPage::itemChanged( const QModelIndex& current) { TemplatePreviewIcon icon = current.data(KDevelop::TemplatesModel::PreviewIconRole).value(); QPixmap pixmap = icon.pixmap(); ui->icon->setPixmap(pixmap); ui->icon->setFixedHeight(pixmap.height()); // header name is either from this index directly or the parents if we show the combo box const QVariant headerData = ui->templateType->isVisible() ? current.parent().data() : current.data(); ui->header->setText(QStringLiteral("

%1

").arg(headerData.toString().trimmed())); ui->description->setText(current.data(KDevelop::TemplatesModel::CommentRole).toString()); validateData(); ui->propertiesBox->setEnabled(true); } QString ProjectSelectionPage::selectedTemplate() { QStandardItem *item = currentItem(); if (item) return item->data().toString(); else return QString(); } QUrl ProjectSelectionPage::location() { QUrl url = ui->locationUrl->url().adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + '/' + encodedProjectName()); return url; } QString ProjectSelectionPage::projectName() { return ui->projectNameEdit->text(); } void ProjectSelectionPage::urlEdited() { validateData(); emit locationChanged( location() ); } void ProjectSelectionPage::validateData() { QUrl url = ui->locationUrl->url(); if( !url.isLocalFile() || url.isEmpty() ) { ui->locationValidWidget->setText( i18n("Invalid location") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } if (projectName().isEmpty()) { ui->locationValidWidget->setText( i18n("Empty project name") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } if (!projectName().isEmpty()) { QString projectName = this->projectName(); QString templatefile = m_wizardDialog->appInfo().appTemplate; // Read template file KConfig config(templatefile); KConfigGroup configgroup(&config, "General"); QString pattern = configgroup.readEntry( "ValidProjectName" , "^[a-zA-Z][a-zA-Z0-9_]+$" ); // Validation int pos = 0; QRegExp regex( pattern ); QRegExpValidator validator( regex ); if( validator.validate(projectName, pos) == QValidator::Invalid ) { ui->locationValidWidget->setText( i18n("Invalid project name") ); emit invalid(); return; } } QDir tDir(url.toLocalFile()); while (!tDir.exists() && !tDir.isRoot()) { if (!tDir.cdUp()) { break; } } if (tDir.exists()) { QFileInfo tFileInfo(tDir.absolutePath()); if (!tFileInfo.isWritable() || !tFileInfo.isExecutable()) { ui->locationValidWidget->setText( i18n("Unable to create subdirectories, " "missing permissions on: %1", tDir.absolutePath()) ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } } QStandardItem* item = currentItem(); if( item && !item->hasChildren() ) { ui->locationValidWidget->animatedHide(); emit valid(); } else { ui->locationValidWidget->setText( i18n("Invalid project template, please choose a leaf item") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } // Check for non-empty target directory. Not an error, but need to display a warning. url.setPath( url.path() + '/' + encodedProjectName() ); QFileInfo fi( url.toLocalFile() ); if( fi.exists() && fi.isDir() ) { if( !QDir( fi.absoluteFilePath()).entryList( QDir::NoDotAndDotDot | QDir::AllEntries ).isEmpty() ) { ui->locationValidWidget->setText( i18n("Path already exists and contains files. Open it as a project.") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } } } QByteArray ProjectSelectionPage::encodedProjectName() { // : < > * ? / \ | " are invalid on windows QByteArray tEncodedName = projectName().toUtf8(); for (int i = 0; i < tEncodedName.size(); ++i) { QChar tChar(tEncodedName.at( i )); if (tChar.isDigit() || tChar.isSpace() || tChar.isLetter() || tChar == '%') continue; QByteArray tReplace = QUrl::toPercentEncoding( tChar ); tEncodedName.replace( tEncodedName.at( i ) ,tReplace ); i = i + tReplace.size() - 1; } return tEncodedName; } QStandardItem* ProjectSelectionPage::currentItem() const { QStandardItem* item = m_templatesModel->itemFromIndex( ui->listView->currentIndex() ); if ( item && item->hasChildren() ) { const int currect = ui->templateType->currentIndex(); const QModelIndex idx = m_templatesModel->index( currect, 0, ui->templateType->rootModelIndex() ); item = m_templatesModel->itemFromIndex(idx); } return item; } bool ProjectSelectionPage::shouldContinue() { QFileInfo fi(location().toLocalFile()); if (fi.exists() && fi.isDir()) { if (!QDir(fi.absoluteFilePath()).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) { int res = KMessageBox::questionYesNo(this, i18n("The specified path already exists and contains files. " "Are you sure you want to proceed?")); return res == KMessageBox::Yes; } } return true; } void ProjectSelectionPage::loadFileClicked() { const QStringList supportedMimeTypes { QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), QStringLiteral("application/zip") }; ScopedDialog fileDialog(this, i18n("Load Template From File")); fileDialog->setMimeTypeFilters(supportedMimeTypes); fileDialog->setFileMode(QFileDialog::ExistingFiles); if (!fileDialog->exec()) { return; } - for (const auto& fileName : fileDialog->selectedFiles()) { + const auto& fileNames = fileDialog->selectedFiles(); + for (const auto& fileName : fileNames) { QString destination = m_templatesModel->loadTemplateFile(fileName); QModelIndexList indexes = m_templatesModel->templateIndexes(destination); if (indexes.size() > 2) { ui->listView->setCurrentIndex(indexes.at(1)); ui->templateType->setCurrentIndex(indexes.at(2).row()); } } } void ProjectSelectionPage::moreTemplatesClicked() { ScopedDialog dialog(QStringLiteral("kdevappwizard.knsrc"), this); if (!dialog->exec()) return; const auto entries = dialog->changedEntries(); if (entries.isEmpty()) { return; } m_templatesModel->refresh(); bool updated = false; for (const KNS3::Entry& entry : entries) { if (!entry.installedFiles().isEmpty()) { updated = true; setCurrentTemplate(entry.installedFiles().at(0)); break; } } if (!updated) { ui->listView->setCurrentIndex(QModelIndex()); } } void ProjectSelectionPage::setCurrentTemplate (const QString& fileName) { QModelIndexList indexes = m_templatesModel->templateIndexes(fileName); if (indexes.size() > 1) { ui->listView->setCurrentIndex(indexes.at(1)); } if (indexes.size() > 2) { ui->templateType->setCurrentIndex(indexes.at(2).row()); } } diff --git a/plugins/clang/clangparsejob.cpp b/plugins/clang/clangparsejob.cpp index 576e4f5d62..99b06c35c3 100644 --- a/plugins/clang/clangparsejob.cpp +++ b/plugins/clang/clangparsejob.cpp @@ -1,385 +1,386 @@ /* This file is part of KDevelop Copyright 2013 Olivier de Gaalon 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 "clangparsejob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "clangsettings/clangsettingsmanager.h" #include "duchain/clanghelpers.h" #include "duchain/clangpch.h" #include "duchain/duchainutils.h" #include "duchain/parsesession.h" #include "duchain/clangindex.h" #include "duchain/clangparsingenvironmentfile.h" #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" #include "clangsupport.h" #include "duchain/documentfinderhelpers.h" #include #include #include #include #include using namespace KDevelop; namespace { QString findConfigFile(const QString& forFile, const QString& configFileName) { QDir dir = QFileInfo(forFile).dir(); while (dir.exists()) { const QFileInfo customIncludePaths(dir, configFileName); if (customIncludePaths.exists()) { return customIncludePaths.absoluteFilePath(); } if (!dir.cdUp()) { break; } } return {}; } Path::List readPathListFile(const QString& filepath) { if (filepath.isEmpty()) { return {}; } QFile f(filepath); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; } const QString text = QString::fromLocal8Bit(f.readAll()); const QStringList lines = text.split(QLatin1Char('\n'), QString::SkipEmptyParts); Path::List paths; paths.reserve(lines.length()); for (const auto& line : lines) { paths << Path(line); } return paths; } /** * File should contain the header to precompile and use while parsing * @returns the first path in the file */ Path userDefinedPchIncludeForFile(const QString& sourcefile) { const QString pchIncludeFilename = QStringLiteral(".kdev_pch_include"); const auto paths = readPathListFile(findConfigFile(sourcefile, pchIncludeFilename)); return paths.isEmpty() ? Path() : paths.first(); } ProjectFileItem* findProjectFileItem(const IndexedString& url, bool* hasBuildSystemInfo) { ProjectFileItem* file = nullptr; *hasBuildSystemInfo = false; - for (auto project: ICore::self()->projectController()->projects()) { + const auto& projects = ICore::self()->projectController()->projects(); + for (auto project : projects) { auto files = project->filesForPath(url); if (files.isEmpty()) { continue; } file = files.last(); // A file might be defined in different targets. // Prefer file items defined inside a target with non-empty includes. for (auto f: files) { if (!dynamic_cast(f->parent())) { continue; } file = f; if (!IDefinesAndIncludesManager::manager()->includes(f, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { break; } } } if (file && file->project()) { if (auto bsm = file->project()->buildSystemManager()) { *hasBuildSystemInfo = bsm->hasBuildInfo(file); } } return file; } ClangParsingEnvironmentFile* parsingEnvironmentFile(const TopDUContext* context) { return dynamic_cast(context->parsingEnvironmentFile().data()); } DocumentChangeTracker* trackerForUrl(const IndexedString& url) { return ICore::self()->languageController()->backgroundParser()->trackerForUrl(url); } } ClangParseJob::ClangParseJob(const IndexedString& url, ILanguageSupport* languageSupport) : ParseJob(url, languageSupport) { const auto tuUrl = clang()->index()->translationUnitForUrl(url); bool hasBuildSystemInfo; if (auto file = findProjectFileItem(tuUrl, &hasBuildSystemInfo)) { m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includes(file)); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectories(file)); m_environment.addDefines(IDefinesAndIncludesManager::manager()->defines(file)); m_environment.setParserSettings(ClangSettingsManager::self()->parserSettings(file)); } else { m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includes(tuUrl.str())); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectories(tuUrl.str())); m_environment.addDefines(IDefinesAndIncludesManager::manager()->defines(tuUrl.str())); m_environment.setParserSettings(ClangSettingsManager::self()->parserSettings(tuUrl.str())); } const bool isSource = ClangHelpers::isSource(tuUrl.str()); m_environment.setQuality( isSource ? (hasBuildSystemInfo ? ClangParsingEnvironment::BuildSystem : ClangParsingEnvironment::Source) : ClangParsingEnvironment::Unknown ); m_environment.setTranslationUnitUrl(tuUrl); Path::List projectPaths; const auto& projects = ICore::self()->projectController()->projects(); projectPaths.reserve(projects.size()); for (auto project : projects) { projectPaths.append(project->path()); } m_environment.setProjectPaths(projectPaths); m_unsavedFiles = ClangUtils::unsavedFiles(); foreach(auto document, ICore::self()->documentController()->openDocuments()) { auto textDocument = document->textDocument(); if ( !textDocument ) { continue; } const IndexedString indexedUrl(textDocument->url()); if (indexedUrl == tuUrl) { m_tuDocumentIsUnsaved = true; } m_unsavedRevisions.insert(indexedUrl, ModificationRevision::revisionForFile(indexedUrl)); } if (auto tracker = trackerForUrl(url)) { tracker->reset(); } } ClangSupport* ClangParseJob::clang() const { return static_cast(languageSupport()); } void ClangParseJob::run(ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread* /*thread*/) { QReadLocker parseLock(languageSupport()->parseLock()); if (abortRequested()) { return; } { const auto tuUrlStr = m_environment.translationUnitUrl().str(); if (!m_tuDocumentIsUnsaved && !QFile::exists(tuUrlStr)) { // maybe we requested a parse job some time ago but now the file // does not exist anymore. return early then clang()->index()->unpinTranslationUnitForUrl(document()); return; } m_environment.addIncludes(IDefinesAndIncludesManager::manager()->includesInBackground(tuUrlStr)); m_environment.addFrameworkDirectories(IDefinesAndIncludesManager::manager()->frameworkDirectoriesInBackground(tuUrlStr)); m_environment.addDefines(IDefinesAndIncludesManager::manager()->definesInBackground(tuUrlStr)); m_environment.setPchInclude(userDefinedPchIncludeForFile(tuUrlStr)); } if (abortRequested()) { return; } // NOTE: we must have all declarations, contexts and uses available for files that are opened in the editor // it is very hard to check this for all included files of this TU, and previously lead to problems // when we tried to skip function bodies as an optimization for files that where not open in the editor. // now, we always build everything, which is correct but a tad bit slower. we can try to optimize later. setMinimumFeatures(static_cast(minimumFeatures() | TopDUContext::AllDeclarationsContextsAndUses)); if (minimumFeatures() & AttachASTWithoutUpdating) { // The context doesn't need to be updated, but has no AST attached (restored from disk), // so attach AST to it, without updating DUChain ParseSession session(createSessionData()); DUChainWriteLocker lock; auto ctx = DUChainUtils::standardContextForUrl(document().toUrl()); if (!ctx) { clangDebug() << "Lost context while attaching AST"; return; } ctx->setAst(IAstContainer::Ptr(session.data())); if (minimumFeatures() & UpdateHighlighting) { lock.unlock(); languageSupport()->codeHighlighting()->highlightDUChain(ctx); } return; } { UrlParseLock urlLock(document()); if (abortRequested() || !isUpdateRequired(ParseSession::languageString())) { return; } } ParseSession session(ClangIntegration::DUChainUtils::findParseSessionData(document(), m_environment.translationUnitUrl())); if (abortRequested()) { return; } if (!session.data() || !session.reparse(m_unsavedFiles, m_environment)) { session.setData(createSessionData()); } if (!session.unit()) { // failed to parse file, unpin and don't try again clang()->index()->unpinTranslationUnitForUrl(document()); return; } if (!clang_getFile(session.unit(), document().byteArray().constData())) { // this parse job's document does not exist in the pinned translation unit // so we need to unpin and re-add this document // Ideally we'd reset m_environment and session, but this is much simpler // and shouldn't be a common case clang()->index()->unpinTranslationUnitForUrl(document()); if (!(minimumFeatures() & Rescheduled)) { auto features = static_cast(minimumFeatures() | Rescheduled); ICore::self()->languageController()->backgroundParser()->addDocument(document(), features, priority()); } return; } Imports imports = ClangHelpers::tuImports(session.unit()); IncludeFileContexts includedFiles; if (auto pch = clang()->index()->pch(m_environment)) { auto pchFile = pch->mapFile(session.unit()); includedFiles = pch->mapIncludes(session.unit()); includedFiles.insert(pchFile, pch->context()); auto tuFile = clang_getFile(session.unit(), m_environment.translationUnitUrl().byteArray().constData()); imports.insert(tuFile, { pchFile, CursorInRevision(0, 0) } ); } if (abortRequested()) { return; } auto context = ClangHelpers::buildDUChain(session.mainFile(), imports, session, minimumFeatures(), includedFiles, clang()->index(), [this] { return abortRequested(); }); setDuChain(context); if (abortRequested()) { return; } if (context) { if (minimumFeatures() & TopDUContext::AST) { DUChainWriteLocker lock; context->setAst(IAstContainer::Ptr(session.data())); } #ifdef QT_DEBUG DUChainReadLocker lock; auto file = parsingEnvironmentFile(context); Q_ASSERT(file); // verify that features and environment where properly set in ClangHelpers::buildDUChain Q_ASSERT(file->featuresSatisfied(TopDUContext::Features(minimumFeatures() & ~TopDUContext::ForceUpdateRecursive))); if (trackerForUrl(context->url())) { Q_ASSERT(file->featuresSatisfied(TopDUContext::AllDeclarationsContextsAndUses)); } #endif } foreach(const auto& context, includedFiles) { if (!context) { continue; } { // prefer the editor modification revision, instead of the on-disk revision auto it = m_unsavedRevisions.find(context->url()); if (it != m_unsavedRevisions.end()) { DUChainWriteLocker lock; auto file = parsingEnvironmentFile(context); Q_ASSERT(file); file->setModificationRevision(it.value()); } } if (trackerForUrl(context->url())) { if (clang()->index()->translationUnitForUrl(context->url()) == m_environment.translationUnitUrl()) { // cache the parse session and the contained translation unit for this chain // this then allows us to quickly reparse the document if it is changed by // the user // otherwise no editor component is open for this document and we can dispose // the TU to save memory // share the session data with all contexts that are pinned to this TU DUChainWriteLocker lock; context->setAst(IAstContainer::Ptr(session.data())); } languageSupport()->codeHighlighting()->highlightDUChain(context); } } } ParseSessionData::Ptr ClangParseJob::createSessionData() const { return ParseSessionData::Ptr(new ParseSessionData(m_unsavedFiles, clang()->index(), m_environment, ParseSessionData::NoOption)); } const ParsingEnvironment* ClangParseJob::environment() const { return &m_environment; } diff --git a/plugins/clang/clangsupport.cpp b/plugins/clang/clangsupport.cpp index 3e17ec270f..74f67a185b 100644 --- a/plugins/clang/clangsupport.cpp +++ b/plugins/clang/clangsupport.cpp @@ -1,438 +1,440 @@ /* This file is part of KDevelop Copyright 2013 Olivier de Gaalon Copyright 2013 Milian Wolff Copyright 2014 Kevin Funk 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 "clangsupport.h" #include "clangparsejob.h" #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" #include "codecompletion/model.h" #include "clanghighlighting.h" #include #include #include #include #include "codegen/clangrefactoring.h" #include "codegen/clangclasshelper.h" #include "codegen/adaptsignatureassistant.h" #include "duchain/documentfinderhelpers.h" #include "duchain/clangindex.h" #include "duchain/navigationwidget.h" #include "duchain/macrodefinition.h" #include "duchain/clangparsingenvironmentfile.h" #include "duchain/duchainutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "clangsettings/sessionsettings/sessionsettings.h" #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KDevClangSupportFactory, "kdevclangsupport.json", registerPlugin(); ) using namespace KDevelop; namespace { QPair lineInDocument(const QUrl &url, const KTextEditor::Cursor& position) { KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (!doc || !doc->textDocument() || !ICore::self()->documentController()->activeTextDocumentView()) { return {}; } const int lineNumber = position.line(); const int lineLength = doc->textDocument()->lineLength(lineNumber); KTextEditor::Range range(lineNumber, 0, lineNumber, lineLength); QString line = doc->textDocument()->text(range); return {line, range}; } QPair importedContextForPosition(const QUrl &url, const KTextEditor::Cursor& position) { auto pair = lineInDocument(url, position); const QString line = pair.first; if (line.isEmpty()) return {{}, KTextEditor::Range::invalid()}; KTextEditor::Range wordRange = ClangUtils::rangeForIncludePathSpec(line, pair.second); if (!wordRange.isValid()) { return {{}, KTextEditor::Range::invalid()}; } // Since this is called by the editor while editing, use a fast timeout so the editor stays responsive DUChainReadLocker lock(nullptr, 100); if (!lock.locked()) { clangDebug() << "Failed to lock the du-chain in time"; return {TopDUContextPointer(), KTextEditor::Range::invalid()}; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (line.isEmpty() || !topContext || !topContext->parsingEnvironmentFile()) { return {TopDUContextPointer(), KTextEditor::Range::invalid()}; } // It's an #include, find out which file was included at the given line foreach(const DUContext::Import &imported, topContext->importedParentContexts()) { auto context = imported.context(nullptr); if (context) { if(topContext->transformFromLocalRevision(topContext->importPosition(context)).line() == wordRange.start().line()) { if (auto importedTop = dynamic_cast(context)) return {TopDUContextPointer(importedTop), wordRange}; } } } // The last resort. Check if the file is already included (maybe recursively from another files). // This is needed as clang doesn't visit (clang_getInclusions) those inclusions. // TODO: Maybe create an assistant that'll report whether the file is already included? auto includeName = line.mid(wordRange.start().column(), wordRange.end().column() - wordRange.start().column()); if (!includeName.isEmpty()) { if (includeName.startsWith(QLatin1Char('.'))) { const Path dir = Path(url).parent(); includeName = Path(dir, includeName).toLocalFile(); } const auto recursiveImports = topContext->recursiveImportIndices(); auto iterator = recursiveImports.iterator(); while (iterator) { const auto str = (*iterator).url().str(); if (str == includeName || (str.endsWith(includeName) && str[str.size()-includeName.size()-1] == QLatin1Char('/'))) { return {TopDUContextPointer((*iterator).data()), wordRange}; } ++iterator; } } return {{}, KTextEditor::Range::invalid()}; } QPair macroExpansionForPosition(const QUrl &url, const KTextEditor::Cursor& position) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (topContext) { int useAt = topContext->findUseAt(topContext->transformToLocalRevision(position)); if (useAt >= 0) { Use use = topContext->uses()[useAt]; if (dynamic_cast(use.usedDeclaration(topContext))) { return {TopDUContextPointer(topContext), use}; } } } return {{}, Use()}; } } ClangSupport::ClangSupport(QObject* parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevclangsupport"), parent ) , ILanguageSupport() , m_highlighting(nullptr) , m_refactoring(nullptr) , m_index(nullptr) { { const auto builtinDir = ClangIntegration::DUChainUtils::clangBuiltinIncludePath(); const auto headerToCheck = QLatin1String("cpuid.h"); if (!QFile::exists(builtinDir + QLatin1Char('/') + headerToCheck)) { setErrorDescription(i18n("The clang builtin include path \"%1\" is invalid (missing %2 header).\n" "Try setting the KDEV_CLANG_BUILTIN_DIR environment variable manually to fix this.\n" "See also: https://bugs.kde.org/show_bug.cgi?id=393779", builtinDir, headerToCheck)); return; } } setXMLFile( QStringLiteral("kdevclangsupport.rc") ); ClangIntegration::DUChainUtils::registerDUChainItems(); m_highlighting = new ClangHighlighting(this); m_refactoring = new ClangRefactoring(this); m_index.reset(new ClangIndex); auto model = new KDevelop::CodeCompletion( this, new ClangCodeCompletionModel(m_index.data(), this), name() ); // TODO: use direct signal/slot connect syntax for 5.1 connect(model, &CodeCompletion::registeredToView, this, &ClangSupport::disableKeywordCompletion); connect(model, &CodeCompletion::unregisteredFromView, this, &ClangSupport::enableKeywordCompletion); - for(const auto& type : DocumentFinderHelpers::mimeTypesList()){ + const auto& mimeTypes = DocumentFinderHelpers::mimeTypesList(); + for (const auto& type : mimeTypes) { KDevelop::IBuddyDocumentFinder::addFinder(type, this); } auto assistantsManager = core()->languageController()->staticAssistantsManager(); assistantsManager->registerAssistant(StaticAssistant::Ptr(new RenameAssistant(this))); assistantsManager->registerAssistant(StaticAssistant::Ptr(new AdaptSignatureAssistant(this))); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ClangSupport::documentActivated); } ClangSupport::~ClangSupport() { parseLock()->lockForWrite(); // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state parseLock()->unlock(); - for(const auto& type : DocumentFinderHelpers::mimeTypesList()) { + const auto& mimeTypes = DocumentFinderHelpers::mimeTypesList(); + for (const auto& type : mimeTypes) { KDevelop::IBuddyDocumentFinder::removeFinder(type); } ClangIntegration::DUChainUtils::unregisterDUChainItems(); } KDevelop::ConfigPage* ClangSupport::configPage(int number, QWidget* parent) { return number == 0 ? new SessionSettings(parent) : nullptr; } int ClangSupport::configPages() const { return 1; } ParseJob* ClangSupport::createParseJob(const IndexedString& url) { return new ClangParseJob(url, this); } QString ClangSupport::name() const { return QStringLiteral("clang"); } ICodeHighlighting* ClangSupport::codeHighlighting() const { return m_highlighting; } BasicRefactoring* ClangSupport::refactoring() const { return m_refactoring; } ICreateClassHelper* ClangSupport::createClassHelper() const { return new ClangClassHelper; } ClangIndex* ClangSupport::index() { return m_index.data(); } bool ClangSupport::areBuddies(const QUrl &url1, const QUrl& url2) { return DocumentFinderHelpers::areBuddies(url1, url2); } bool ClangSupport::buddyOrder(const QUrl &url1, const QUrl& url2) { return DocumentFinderHelpers::buddyOrder(url1, url2); } QVector ClangSupport::potentialBuddies(const QUrl& url) const { return DocumentFinderHelpers::potentialBuddies(url); } void ClangSupport::createActionsForMainWindow (Sublime::MainWindow* /*window*/, QString& _xmlFile, KActionCollection& actions) { _xmlFile = xmlFile(); QAction* renameDeclarationAction = actions.addAction(QStringLiteral("code_rename_declaration")); renameDeclarationAction->setText( i18n("Rename Declaration") ); renameDeclarationAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); actions.setDefaultShortcut(renameDeclarationAction, Qt::CTRL | Qt::SHIFT | Qt::Key_R); connect(renameDeclarationAction, &QAction::triggered, m_refactoring, &ClangRefactoring::executeRenameAction); QAction* moveIntoSourceAction = actions.addAction(QStringLiteral("code_move_definition")); moveIntoSourceAction->setText(i18n("Move into Source")); actions.setDefaultShortcut(moveIntoSourceAction, Qt::CTRL | Qt::ALT | Qt::Key_S); connect(moveIntoSourceAction, &QAction::triggered, m_refactoring, &ClangRefactoring::executeMoveIntoSourceAction); } KDevelop::ContextMenuExtension ClangSupport::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { ContextMenuExtension cm; EditorContext *ec = dynamic_cast(context); if (ec && ICore::self()->languageController()->languagesForUrl(ec->url()).contains(this)) { // It's a C++ file, let's add our context menu. m_refactoring->fillContextMenu(cm, context, parent); } return cm; } KTextEditor::Range ClangSupport::specialLanguageObjectRange(const QUrl &url, const KTextEditor::Cursor& position) { DUChainReadLocker lock; const QPair macroExpansion = macroExpansionForPosition(url, position); if (macroExpansion.first) { return macroExpansion.first->transformFromLocalRevision(macroExpansion.second.m_range); } const QPair import = importedContextForPosition(url, position); if(import.first) { return import.second; } return KTextEditor::Range::invalid(); } QPair ClangSupport::specialLanguageObjectJumpCursor(const QUrl &url, const KTextEditor::Cursor& position) { const QPair import = importedContextForPosition(url, position); DUChainReadLocker lock; if (import.first) { return qMakePair(import.first->url().toUrl(), KTextEditor::Cursor(0,0)); } return {{}, KTextEditor::Cursor::invalid()}; } QWidget* ClangSupport::specialLanguageObjectNavigationWidget(const QUrl &url, const KTextEditor::Cursor& position) { DUChainReadLocker lock; const QPair macroExpansion = macroExpansionForPosition(url, position); if (macroExpansion.first) { Declaration* declaration = macroExpansion.second.usedDeclaration(macroExpansion.first.data()); const MacroDefinition::Ptr macroDefinition(dynamic_cast(declaration)); Q_ASSERT(macroDefinition); auto rangeInRevision = macroExpansion.first->transformFromLocalRevision(macroExpansion.second.m_range.start); return new ClangNavigationWidget(macroDefinition, DocumentCursor(IndexedString(url), rangeInRevision)); } const QPair import = importedContextForPosition(url, position); if (import.first) { return import.first->createNavigationWidget(); } return nullptr; } TopDUContext* ClangSupport::standardContext(const QUrl &url, bool /*proxyContext*/) { ClangParsingEnvironment env; return DUChain::self()->chainForDocument(url, &env); } void ClangSupport::documentActivated(IDocument* doc) { TopDUContext::Features features; { DUChainReadLocker lock; auto ctx = DUChainUtils::standardContextForUrl(doc->url()); if (!ctx) { return; } auto file = ctx->parsingEnvironmentFile(); if (!file) { return; } if (file->type() != CppParsingEnvironment) { return; } if (file->needsUpdate()) { return; } features = ctx->features(); } const auto indexedUrl = IndexedString(doc->url()); auto sessionData = ClangIntegration::DUChainUtils::findParseSessionData(indexedUrl, index()->translationUnitForUrl(IndexedString(doc->url()))); if (sessionData) { return; } if ((features & TopDUContext::AllDeclarationsContextsAndUses) != TopDUContext::AllDeclarationsContextsAndUses) { // the file was parsed in simplified mode, we need to reparse it to get all data // now that its opened in the editor features = TopDUContext::AllDeclarationsContextsAndUses; } else { features = static_cast(ClangParseJob::AttachASTWithoutUpdating | features); if (ICore::self()->languageController()->backgroundParser()->isQueued(indexedUrl)) { // The document is already scheduled for parsing (happens when opening a project with an active document) // The background parser will optimize the previous request out, so we need to update highlighting features = static_cast(ClangParseJob::UpdateHighlighting | features); } } ICore::self()->languageController()->backgroundParser()->addDocument(indexedUrl, features); } static void setKeywordCompletion(KTextEditor::View* view, bool enabled) { if (auto config = qobject_cast(view)) { config->setConfigValue(QStringLiteral("keyword-completion"), enabled); } } int ClangSupport::suggestedReparseDelayForChange(KTextEditor::Document* /*doc*/, const KTextEditor::Range& /*changedRange*/, const QString& /*changedText*/, bool /*removal*/) const { return ILanguageSupport::DefaultDelay; } void ClangSupport::disableKeywordCompletion(KTextEditor::View* view) { setKeywordCompletion(view, false); } void ClangSupport::enableKeywordCompletion(KTextEditor::View* view) { setKeywordCompletion(view, true); } #include "clangsupport.moc" diff --git a/plugins/clang/codecompletion/context.cpp b/plugins/clang/codecompletion/context.cpp index 8836db23b3..9dc622152c 100644 --- a/plugins/clang/codecompletion/context.cpp +++ b/plugins/clang/codecompletion/context.cpp @@ -1,1305 +1,1307 @@ /* * This file is part of KDevelop * Copyright 2014 Milian Wolff * Copyright 2015 Sergey Kalinichev * * 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 "context.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include "../util/clangdebug.h" #include "../util/clangtypes.h" #include "../util/clangutils.h" #include "../duchain/clangdiagnosticevaluator.h" #include "../duchain/parsesession.h" #include "../duchain/duchainutils.h" #include "../duchain/navigationwidget.h" #include "../clangsettings/clangsettingsmanager.h" #include #include #include #include using namespace KDevelop; namespace { /// Maximum return-type string length in completion items const int MAX_RETURN_TYPE_STRING_LENGTH = 20; /// Priority of code-completion results. NOTE: Keep in sync with Clang code base. enum CodeCompletionPriority { /// Priority for the next initialization in a constructor initializer list. CCP_NextInitializer = 7, /// Priority for an enumeration constant inside a switch whose condition is of the enumeration type. CCP_EnumInCase = 7, CCP_LocalDeclarationMatch = 8, CCP_DeclarationMatch = 12, CCP_LocalDeclarationSimiliar = 17, /// Priority for a send-to-super completion. CCP_SuperCompletion = 20, CCP_DeclarationSimiliar = 25, /// Priority for a declaration that is in the local scope. CCP_LocalDeclaration = 34, /// Priority for a member declaration found from the current method or member function. CCP_MemberDeclaration = 35, /// Priority for a language keyword (that isn't any of the other categories). CCP_Keyword = 40, /// Priority for a code pattern. CCP_CodePattern = 40, /// Priority for a non-type declaration. CCP_Declaration = 50, /// Priority for a type. CCP_Type = CCP_Declaration, /// Priority for a constant value (e.g., enumerator). CCP_Constant = 65, /// Priority for a preprocessor macro. CCP_Macro = 70, /// Priority for a nested-name-specifier. CCP_NestedNameSpecifier = 75, /// Priority for a result that isn't likely to be what the user wants, but is included for completeness. CCP_Unlikely = 80 }; /** * Common base class for Clang code completion items. */ template class CompletionItem : public Base { public: CompletionItem(const QString& display, const QString& prefix) : Base() , m_display(display) , m_prefix(prefix) , m_unimportant(false) { } ~CompletionItem() override = default; QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* /*model*/) const override { if (role == Qt::DisplayRole) { if (index.column() == CodeCompletionModel::Prefix) { return m_prefix; } else if (index.column() == CodeCompletionModel::Name) { return m_display; } } return {}; } void markAsUnimportant() { m_unimportant = true; } protected: QString m_display; QString m_prefix; bool m_unimportant; }; class OverrideItem : public CompletionItem { public: OverrideItem(const QString& nameAndParams, const QString& returnType) : CompletionItem( nameAndParams, i18n("Override %1", returnType) ) , m_returnType(returnType) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole) { if (index.column() == KTextEditor::CodeCompletionModel::Icon) { static const QIcon icon = QIcon::fromTheme(QStringLiteral("CTparents")); return icon; } } return CompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_returnType + QLatin1Char(' ') + m_display.replace(QRegularExpression(QStringLiteral("\\s*=\\s*0")), QString()) + QLatin1String(" override;")); } private: QString m_returnType; }; /** * Specialized completion item class for items which are represented by a Declaration */ class DeclarationItem : public CompletionItem { public: DeclarationItem(Declaration* dec, const QString& display, const QString& prefix, const QString& replacement) : CompletionItem(display, prefix) , m_replacement(replacement) { m_declaration = dec; } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::MatchQuality && m_matchQuality) { return m_matchQuality; } auto ret = CompletionItem::data(index, role, model); if (ret.isValid()) { return ret; } return NormalDeclarationCompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { QString repl = m_replacement; DUChainReadLocker lock; if(!m_declaration){ return; } if(m_declaration->isFunctionDeclaration()) { const auto functionType = m_declaration->type(); // protect against buggy code that created the m_declaration, // to mark it as a function but not assign a function type if (!functionType) return; auto doc = view->document(); // Function pointer? bool funcptr = false; const auto line = doc->line(word.start().line()); auto pos = word.end().column() - 1; while ( pos > 0 && (line.at(pos).isLetterOrNumber() || line.at(pos) == QLatin1Char(':')) ) { pos--; if ( line.at(pos) == QLatin1Char('&') ) { funcptr = true; break; } } auto restEmpty = doc->characterAt(word.end() + KTextEditor::Cursor{0, 1}) == QChar(); bool didAddParentheses = false; if ( !funcptr && doc->characterAt(word.end()) != QLatin1Char('(') ) { repl += QLatin1String("()"); didAddParentheses = true; } view->document()->replaceText(word, repl); if (functionType->indexedArgumentsSize() && didAddParentheses) { view->setCursorPosition(word.start() + KTextEditor::Cursor(0, repl.size() - 1)); } auto returnTypeIntegral = functionType->returnType().cast(); if ( restEmpty && !funcptr && returnTypeIntegral && returnTypeIntegral->dataType() == IntegralType::TypeVoid ) { // function returns void and rest of line is empty -- nothing can be done with the result if (functionType->indexedArgumentsSize() ) { // we placed the cursor inside the () view->document()->insertText(view->cursorPosition() + KTextEditor::Cursor(0, 1), QStringLiteral(";")); } else { // we placed the cursor after the () view->document()->insertText(view->cursorPosition(), QStringLiteral(";")); view->setCursorPosition(view->cursorPosition() + KTextEditor::Cursor{0, 1}); } } } else { view->document()->replaceText(word, repl); } } bool createsExpandingWidget() const override { return true; } QWidget* createExpandingWidget(const CodeCompletionModel* /*model*/) const override { return new ClangNavigationWidget(m_declaration, AbstractNavigationWidget::EmbeddableWidget); } int matchQuality() const { return m_matchQuality; } ///Sets match quality from 0 to 10. 10 is the best fit. void setMatchQuality(int value) { m_matchQuality = value; } void setInheritanceDepth(int depth) { m_inheritanceDepth = depth; } int argumentHintDepth() const override { return m_depth; } void setArgumentHintDepth(int depth) { m_depth = depth; } protected: int m_matchQuality = 0; int m_depth = 0; QString m_replacement; }; class ImplementsItem : public DeclarationItem { public: static QString replacement(const FuncImplementInfo& info) { QString replacement = info.templatePrefix; if (!info.isDestructor && !info.isConstructor) { replacement += info.returnType + QLatin1Char(' '); } replacement += info.prototype + QLatin1String("\n{\n}\n"); return replacement; } explicit ImplementsItem(const FuncImplementInfo& item) : DeclarationItem(item.declaration.data(), item.prototype, i18n("Implement %1", item.isConstructor ? QStringLiteral("") : item.isDestructor ? QStringLiteral("") : item.returnType), replacement(item) ) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (index.column() == CodeCompletionModel::Arguments) { // our display string already contains the arguments return {}; } return DeclarationItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_replacement); } }; class ArgumentHintItem : public DeclarationItem { public: struct CurrentArgumentRange { int start; int end; }; ArgumentHintItem(Declaration* decl, const QString& prefix, const QString& name, const QString& arguments, const CurrentArgumentRange& range) : DeclarationItem(decl, name, prefix, {}) , m_range(range) , m_arguments(arguments) {} QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::CustomHighlight && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { QTextCharFormat boldFormat; boldFormat.setFontWeight(QFont::Bold); const QList highlighting { QVariant(m_range.start), QVariant(m_range.end), boldFormat, }; return highlighting; } if (role == CodeCompletionModel::HighlightingMethod && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { return QVariant(CodeCompletionModel::CustomHighlighting); } if (index.column() == CodeCompletionModel::Arguments) { return m_arguments; } return DeclarationItem::data(index, role, model); } private: CurrentArgumentRange m_range; QString m_arguments; }; /** * A minimalistic completion item for macros and such */ class SimpleItem : public CompletionItem { public: SimpleItem(const QString& display, const QString& prefix, const QString& replacement, const QIcon& icon = QIcon()) : CompletionItem(display, prefix) , m_replacement(replacement) , m_icon(icon) { } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_replacement); } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { return m_icon; } if (role == CodeCompletionModel::UnimportantItemRole) { return m_unimportant; } return CompletionItem::data(index, role, model); } private: QString m_replacement; QIcon m_icon; }; /** * Return true in case position @p position represents a cursor inside a comment */ bool isInsideComment(CXTranslationUnit unit, CXFile file, const KTextEditor::Cursor& position) { if (!position.isValid()) { return false; } // TODO: This may get very slow for a large TU, investigate if we can improve this function auto begin = clang_getLocation(unit, file, 1, 1); auto end = clang_getLocation(unit, file, position.line() + 1, position.column() + 1); CXSourceRange range = clang_getRange(begin, end); // tokenize the whole range from the start until 'position' // if we detect a comment token at this position, return true const ClangTokens tokens(unit, range); for (CXToken token : tokens) { CXTokenKind tokenKind = clang_getTokenKind(token); if (tokenKind != CXToken_Comment) { continue; } auto range = ClangRange(clang_getTokenExtent(unit, token)); if (range.toRange().contains(position)) { return true; } } return false; } QString& elideStringRight(QString& str, int length) { if (str.size() > length + 3) { return str.replace(length, str.size() - length, QStringLiteral("...")); } return str; } /** * @return Value suited for @ref CodeCompletionModel::MatchQuality in the range [0.0, 10.0] (the higher the better) * * See http://clang.llvm.org/doxygen/CodeCompleteConsumer_8h_source.html for list of priorities * They (currently) have a range from [-3, 80] (the lower, the better) */ int codeCompletionPriorityToMatchQuality(unsigned int completionPriority) { return 10u - qBound(0u, completionPriority, 80u) / 8; } int adjustPriorityForType(const AbstractType::Ptr& type, int completionPriority) { const auto modifier = 4; if (type) { const auto whichType = type->whichType(); if (whichType == AbstractType::TypePointer || whichType == AbstractType::TypeReference) { // Clang considers all pointers as similar, this is not what we want. completionPriority += modifier; } else if (whichType == AbstractType::TypeStructure) { // Clang considers all classes as similar too... completionPriority += modifier; } else if (whichType == AbstractType::TypeDelayed) { completionPriority += modifier; } else if (whichType == AbstractType::TypeAlias) { auto aliasedType = type.cast(); return adjustPriorityForType(aliasedType ? aliasedType->type() : AbstractType::Ptr(), completionPriority); } else if (whichType == AbstractType::TypeFunction) { auto functionType = type.cast(); return adjustPriorityForType(functionType ? functionType->returnType() : AbstractType::Ptr(), completionPriority); } } else { completionPriority += modifier; } return completionPriority; } /// Adjusts priority for the @p decl int adjustPriorityForDeclaration(Declaration* decl, unsigned int completionPriority) { if(completionPriority < CCP_LocalDeclarationSimiliar || completionPriority > CCP_SuperCompletion){ return completionPriority; } return adjustPriorityForType(decl->abstractType(), completionPriority); } /** * @return Whether the declaration represented by identifier @p identifier qualifies as completion result * * For example, we don't want to offer SomeClass::SomeClass as completion item to the user * (otherwise we'd end up generating code such as 's.SomeClass();') */ bool isValidCompletionIdentifier(const QualifiedIdentifier& identifier) { const int count = identifier.count(); if (identifier.count() < 2) { return true; } const Identifier scope = identifier.at(count-2); const Identifier id = identifier.last(); if (scope == id) { return false; // is constructor } const QString idString = id.toString(); if (idString.startsWith(QLatin1Char('~')) && scope.toString() == idString.midRef(1)) { return false; // is destructor } return true; } /** * @return Whether the declaration represented by identifier @p identifier qualifies as "special" completion result * * "Special" completion results are items that are likely not regularly used. * * Examples: * - 'SomeClass::operator=(const SomeClass&)' */ bool isValidSpecialCompletionIdentifier(const QualifiedIdentifier& identifier) { if (identifier.count() < 2) { return false; } const Identifier id = identifier.last(); const QString idString = id.toString(); if (idString.startsWith(QLatin1String("operator="))) { return true; // is assignment operator } return false; } Declaration* findDeclaration(const QualifiedIdentifier& qid, const DUContextPointer& ctx, const CursorInRevision& position, QSet& handled) { PersistentSymbolTable::Declarations decl = PersistentSymbolTable::self().getDeclarations(qid); const auto top = ctx->topContext(); const auto& importedContexts = top->importedParentContexts(); for (auto it = decl.iterator(); it; ++it) { // if the context is not included, then this match is not correct for our consideration // this fixes issues where we used to include matches from files that did not have // anything to do with the current TU, e.g. the main from a different file or stuff like that // it also reduces the chance of us picking up a function of the same name from somewhere else // also, this makes sure the context has the correct language and we don't get confused by stuff // from other language plugins if (std::none_of(importedContexts.begin(), importedContexts.end(), [it] (const DUContext::Import& import) { return import.topContextIndex() == it->indexedTopContext().index(); })) { continue; } auto declaration = it->declaration(); if (!declaration) { // Mitigate problems such as: Cannot load a top-context from file "/home/kfunk/.cache/kdevduchain/kdevelop-{foo}/topcontexts/6085" // - the required language-support for handling ID 55 is probably not loaded qCWarning(KDEV_CLANG) << "Detected an invalid declaration for" << qid; continue; } if (declaration->kind() == Declaration::Instance && !declaration->isFunctionDeclaration()) { break; } if (!handled.contains(declaration)) { handled.insert(declaration); return declaration; } } const auto foundDeclarations = ctx->findDeclarations(qid, position); for (auto dec : foundDeclarations) { if (!handled.contains(dec)) { handled.insert(dec); return dec; } } return nullptr; } /// If any parent of this context is a class, the closest class declaration is returned, nullptr otherwise Declaration* classDeclarationForContext(const DUContextPointer& context, const CursorInRevision& position) { auto parent = context; while (parent) { if (parent->type() == DUContext::Class) { break; } if (auto owner = parent->owner()) { // Work-around for out-of-line methods. They have Helper context instead of Class context if (owner->context() && owner->context()->type() == DUContext::Helper) { auto qid = owner->qualifiedIdentifier(); qid.pop(); QSet tmp; auto decl = findDeclaration(qid, context, position, tmp); if (decl && decl->internalContext() && decl->internalContext()->type() == DUContext::Class) { parent = decl->internalContext(); break; } } } parent = parent->parentContext(); } return parent ? parent->owner() : nullptr; } class LookAheadItemMatcher { public: explicit LookAheadItemMatcher(const TopDUContextPointer& ctx) : m_topContext(ctx) , m_enabled(ClangSettingsManager::self()->codeCompletionSettings().lookAhead) {} /// Adds all local declarations for @p declaration into possible look-ahead items. void addDeclarations(Declaration* declaration) { if (!m_enabled) { return; } if (declaration->kind() != Declaration::Instance) { return; } auto type = typeForDeclaration(declaration); auto identifiedType = dynamic_cast(type.data()); if (!identifiedType) { return; } addDeclarationsForType(identifiedType, declaration); } /// Add type for matching. This type'll be used for filtering look-ahead items /// Only items with @p type will be returned through @sa matchedItems void addMatchedType(const IndexedType& type) { matchedTypes.insert(type); } /// @return look-ahead items that math given types. @sa addMatchedType QList matchedItems() { QList lookAheadItems; - for (const auto& pair: possibleLookAheadDeclarations) { + for (const auto& pair: qAsConst(possibleLookAheadDeclarations)) { auto decl = pair.first; if (matchedTypes.contains(decl->indexedType())) { auto parent = pair.second; const QString access = parent->abstractType()->whichType() == AbstractType::TypePointer ? QStringLiteral("->") : QStringLiteral("."); const QString text = parent->identifier().toString() + access + decl->identifier().toString(); auto item = new DeclarationItem(decl, text, {}, text); item->setMatchQuality(8); lookAheadItems.append(CompletionTreeItemPointer(item)); } } return lookAheadItems; } private: AbstractType::Ptr typeForDeclaration(const Declaration* decl) { return TypeUtils::targetType(decl->abstractType(), m_topContext.data()); } void addDeclarationsForType(const IdentifiedType* identifiedType, Declaration* declaration) { if (auto typeDecl = identifiedType->declaration(m_topContext.data())) { if (dynamic_cast(typeDecl->logicalDeclaration(m_topContext.data()))) { if (!typeDecl->internalContext()) { return; } - for (auto localDecl : typeDecl->internalContext()->localDeclarations()) { + const auto& localDeclarations = typeDecl->internalContext()->localDeclarations(); + for (auto localDecl : localDeclarations) { if(localDecl->identifier().isEmpty()){ continue; } if(auto classMember = dynamic_cast(localDecl)){ // TODO: Also add protected/private members if completion is inside this class context. if(classMember->accessPolicy() != Declaration::Public){ continue; } } if(!declaration->abstractType()){ continue; } if (declaration->abstractType()->whichType() == AbstractType::TypeIntegral) { if (auto integralType = declaration->abstractType().cast()) { if (integralType->dataType() == IntegralType::TypeVoid) { continue; } } } possibleLookAheadDeclarations.insert({localDecl, declaration}); } } } } // Declaration and it's context typedef QPair DeclarationContext; /// Types of declarations that look-ahead completion items can have QSet matchedTypes; // List of declarations that can be added to the Look Ahead group // Second declaration represents context QSet possibleLookAheadDeclarations; TopDUContextPointer m_topContext; bool m_enabled; }; struct MemberAccessReplacer : public QObject { Q_OBJECT public: enum Type { None, DotToArrow, ArrowToDot }; public Q_SLOTS: void replaceCurrentAccess(MemberAccessReplacer::Type type) { if (auto document = ICore::self()->documentController()->activeDocument()) { if (auto textDocument = document->textDocument()) { auto activeView = document->activeTextView(); if (!activeView) { return; } auto cursor = activeView->cursorPosition(); QString oldAccess, newAccess; if (type == ArrowToDot) { oldAccess = QStringLiteral("->"); newAccess = QStringLiteral("."); } else { oldAccess = QStringLiteral("."); newAccess = QStringLiteral("->"); } auto oldRange = KTextEditor::Range(cursor - KTextEditor::Cursor(0, oldAccess.length()), cursor); // This code needed for testReplaceMemberAccess test // Maybe we should do a similar thing for '->' to '.' direction, but this is not so important while (textDocument->text(oldRange) == QLatin1String(" ") && oldRange.start().column() >= 0) { oldRange = KTextEditor::Range({oldRange.start().line(), oldRange.start().column() - 1}, {oldRange.end().line(), oldRange.end().column() - 1}); } if (oldRange.start().column() >= 0 && textDocument->text(oldRange) == oldAccess) { textDocument->replaceText(oldRange, newAccess); } } } } }; static MemberAccessReplacer s_memberAccessReplacer; } Q_DECLARE_METATYPE(MemberAccessReplacer::Type) ClangCodeCompletionContext::ClangCodeCompletionContext(const DUContextPointer& context, const ParseSessionData::Ptr& sessionData, const QUrl& url, const KTextEditor::Cursor& position, const QString& text, const QString& followingText ) : CodeCompletionContext(context, text + followingText, CursorInRevision::castFromSimpleCursor(position), 0) , m_results(nullptr, clang_disposeCodeCompleteResults) , m_parseSessionData(sessionData) { qRegisterMetaType(); const QByteArray file = url.toLocalFile().toUtf8(); ParseSession session(m_parseSessionData); QVector otherUnsavedFiles; { ForegroundLock lock; otherUnsavedFiles = ClangUtils::unsavedFiles(); } QVector allUnsaved; { const unsigned int completeOptions = clang_defaultCodeCompleteOptions(); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); allUnsaved.reserve(otherUnsavedFiles.size() + 1); - for ( const auto& f : otherUnsavedFiles ) { + for (const auto& f : qAsConst(otherUnsavedFiles)) { allUnsaved.append(f.toClangApi()); } allUnsaved.append(unsaved); m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1, allUnsaved.data(), allUnsaved.size(), completeOptions)); if (!m_results) { qCWarning(KDEV_CLANG) << "Something went wrong during 'clang_codeCompleteAt' for file" << file; return; } auto numDiagnostics = clang_codeCompleteGetNumDiagnostics(m_results.get()); for (uint i = 0; i < numDiagnostics; i++) { auto diagnostic = clang_codeCompleteGetDiagnostic(m_results.get(), i); auto diagnosticType = ClangDiagnosticEvaluator::diagnosticType(diagnostic); clang_disposeDiagnostic(diagnostic); if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithArrowProblem || diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { MemberAccessReplacer::Type replacementType; if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { replacementType = MemberAccessReplacer::ArrowToDot; } else { replacementType = MemberAccessReplacer::DotToArrow; } QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, replacementType)); m_valid = false; return; } } auto addMacros = ClangSettingsManager::self()->codeCompletionSettings().macros; if (!addMacros) { m_filters |= NoMacros; } } if (!m_results->NumResults) { const auto trimmedText = text.trimmed(); if (trimmedText.endsWith(QLatin1Char('.'))) { // TODO: This shouldn't be needed if Clang provided diagnostic. // But it doesn't always do it, so let's try to manually determine whether '.' is used instead of '->' m_text = trimmedText.leftRef(trimmedText.size() - 1) + QStringLiteral("->"); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); allUnsaved[allUnsaved.size() - 1] = unsaved; m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1 + 1, allUnsaved.data(), allUnsaved.size(), clang_defaultCodeCompleteOptions())); if (m_results && m_results->NumResults) { QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, MemberAccessReplacer::DotToArrow)); } m_valid = false; return; } } // check 'isValidPosition' after parsing the new content auto clangFile = session.file(file); if (!isValidPosition(session.unit(), clangFile)) { m_valid = false; return; } m_completionHelper.computeCompletions(session, clangFile, position); } ClangCodeCompletionContext::~ClangCodeCompletionContext() { } bool ClangCodeCompletionContext::isValidPosition(CXTranslationUnit unit, CXFile file) const { if (isInsideComment(unit, file, m_position.castToSimpleCursor())) { clangDebug() << "Invalid completion context: Inside comment"; return false; } return true; } QList ClangCodeCompletionContext::completionItems(bool& abort, bool /*fullCompletion*/) { if (!m_valid || !m_duContext || !m_results) { return {}; } const auto ctx = DUContextPointer(m_duContext->findContextAt(m_position)); /// Normal completion items, such as 'void Foo::foo()' QList items; /// Stuff like 'Foo& Foo::operator=(const Foo&)', etc. Not regularly used by our users. QList specialItems; /// Macros from the current context QList macros; /// Builtins reported by Clang QList builtin; // two sets of handled declarations to prevent duplicates and make sure we show // all available overloads QSet handled; // this is only used for the CXCursor_OverloadCandidate completion items QSet overloadsHandled; LookAheadItemMatcher lookAheadMatcher(TopDUContextPointer(ctx->topContext())); // If ctx is/inside the Class context, this represents that context. const auto currentClassContext = classDeclarationForContext(ctx, m_position); clangDebug() << "Clang found" << m_results->NumResults << "completion results"; for (uint i = 0; i < m_results->NumResults; ++i) { if (abort) { return {}; } auto result = m_results->Results[i]; #if CINDEX_VERSION_MINOR >= 30 const bool isOverloadCandidate = result.CursorKind == CXCursor_OverloadCandidate; #else const bool isOverloadCandidate = false; #endif const auto availability = clang_getCompletionAvailability(result.CompletionString); if (availability == CXAvailability_NotAvailable) { continue; } const bool isMacroDefinition = result.CursorKind == CXCursor_MacroDefinition; if (isMacroDefinition && m_filters & NoMacros) { continue; } const bool isBuiltin = (result.CursorKind == CXCursor_NotImplemented); if (isBuiltin && m_filters & NoBuiltins) { continue; } const bool isDeclaration = !isMacroDefinition && !isBuiltin; if (isDeclaration && m_filters & NoDeclarations) { continue; } if (availability == CXAvailability_NotAccessible && (!isDeclaration || !currentClassContext)) { continue; } // the string that would be needed to type, usually the identifier of something. Also we use it as name for code completion declaration items. QString typed; // the return type of a function e.g. QString resultType; // the replacement text when an item gets executed QString replacement; QString arguments; ArgumentHintItem::CurrentArgumentRange argumentRange; //BEGIN function signature parsing // nesting depth of parentheses int parenDepth = 0; enum FunctionSignatureState { // not yet inside the function signature Before, // any token is part of the function signature now Inside, // finished parsing the function signature After }; // current state FunctionSignatureState signatureState = Before; //END function signature parsing std::function processChunks = [&] (CXCompletionString completionString) { const uint chunks = clang_getNumCompletionChunks(completionString); for (uint j = 0; j < chunks; ++j) { const auto kind = clang_getCompletionChunkKind(completionString, j); if (kind == CXCompletionChunk_Optional) { completionString = clang_getCompletionChunkCompletionString(completionString, j); if (completionString) { processChunks(completionString); } continue; } // We don't need function signature for declaration items, we can get it directly from the declaration. Also adding the function signature to the "display" would break the "Detailed completion" option. if (isDeclaration && !typed.isEmpty()) { // TODO: When parent context for CXCursor_OverloadCandidate is fixed remove this check if (!isOverloadCandidate) { break; } } const QString string = ClangString(clang_getCompletionChunkText(completionString, j)).toString(); switch (kind) { case CXCompletionChunk_TypedText: typed = string; replacement += string; break; case CXCompletionChunk_ResultType: resultType = string; continue; case CXCompletionChunk_Placeholder: if (signatureState == Inside) { arguments += string; } continue; case CXCompletionChunk_LeftParen: if (signatureState == Before && !parenDepth) { signatureState = Inside; } parenDepth++; break; case CXCompletionChunk_RightParen: --parenDepth; if (signatureState == Inside && !parenDepth) { arguments += QLatin1Char(')'); signatureState = After; } break; case CXCompletionChunk_Text: if (isOverloadCandidate) { typed += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } break; case CXCompletionChunk_CurrentParameter: argumentRange.start = arguments.size(); argumentRange.end = string.size(); break; default: break; } if (signatureState == Inside) { arguments += string; } } }; processChunks(result.CompletionString); // TODO: No closing paren if default parameters present if (isOverloadCandidate && !arguments.endsWith(QLatin1Char(')'))) { arguments += QLatin1Char(')'); } // ellide text to the right for overly long result types (templates especially) elideStringRight(resultType, MAX_RETURN_TYPE_STRING_LENGTH); static const auto noIcon = QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/pics/namespace.png"))); if (isDeclaration) { const Identifier id(typed); QualifiedIdentifier qid; ClangString parent(clang_getCompletionParent(result.CompletionString, nullptr)); if (parent.c_str() != nullptr) { qid = QualifiedIdentifier(parent.toString()); } qid.push(id); if (!isValidCompletionIdentifier(qid)) { continue; } if (isOverloadCandidate && resultType.isEmpty() && parent.isEmpty()) { // workaround: find constructor calls for non-namespaced classes // TODO: return the namespaced class as parent in libclang qid.push(id); } auto found = findDeclaration(qid, ctx, m_position, isOverloadCandidate ? overloadsHandled : handled); CompletionTreeItemPointer item; if (found) { // TODO: Bug in Clang: protected members from base classes not accessible in derived classes. if (availability == CXAvailability_NotAccessible) { if (auto cl = dynamic_cast(found)) { if (cl->accessPolicy() != Declaration::Protected) { continue; } auto declarationClassContext = classDeclarationForContext(DUContextPointer(found->context()), m_position); uint steps = 10; auto inheriters = DUChainUtils::getInheriters(declarationClassContext, steps); if(!inheriters.contains(currentClassContext)){ continue; } } else { continue; } } DeclarationItem* declarationItem = nullptr; if (isOverloadCandidate) { declarationItem = new ArgumentHintItem(found, resultType, typed, arguments, argumentRange); declarationItem->setArgumentHintDepth(1); } else { declarationItem = new DeclarationItem(found, typed, resultType, replacement); } const unsigned int completionPriority = adjustPriorityForDeclaration(found, clang_getCompletionPriority(result.CompletionString)); const bool bestMatch = completionPriority <= CCP_SuperCompletion; //don't set best match property for internal identifiers, also prefer declarations from current file const auto isInternal = found->indexedIdentifier().identifier().toString().startsWith(QLatin1String("__")); if (bestMatch && !isInternal ) { const int matchQuality = codeCompletionPriorityToMatchQuality(completionPriority); declarationItem->setMatchQuality(matchQuality); // TODO: LibClang missing API to determine expected code completion type. lookAheadMatcher.addMatchedType(found->indexedType()); } else { declarationItem->setInheritanceDepth(completionPriority); lookAheadMatcher.addDeclarations(found); } if ( isInternal ) { declarationItem->markAsUnimportant(); } item = declarationItem; } else { if (isOverloadCandidate) { // TODO: No parent context for CXCursor_OverloadCandidate items, hence qid is broken -> no declaration found auto ahi = new ArgumentHintItem({}, resultType, typed, arguments, argumentRange); ahi->setArgumentHintDepth(1); item = ahi; } else { // still, let's trust that Clang found something useful and put it into the completion result list clangDebug() << "Could not find declaration for" << qid; auto instance = new SimpleItem(typed + arguments, resultType, replacement, noIcon); instance->markAsUnimportant(); item = CompletionTreeItemPointer(instance); } } if (isValidSpecialCompletionIdentifier(qid)) { // If it's a special completion identifier e.g. "operator=(const&)" and we don't have a declaration for it, don't add it into completion list, as this item is completely useless and pollutes the test case. // This happens e.g. for "class A{}; a.|". At | we have "operator=(const A&)" as a special completion identifier without a declaration. if(item->declaration()){ specialItems.append(item); } } else { items.append(item); } continue; } if (result.CursorKind == CXCursor_MacroDefinition) { // TODO: grouping of macros and built-in stuff const auto text = QString(typed + arguments); auto instance = new SimpleItem(text, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); if ( text.startsWith(QLatin1Char('_')) ) { instance->markAsUnimportant(); } macros.append(item); } else if (result.CursorKind == CXCursor_NotImplemented) { auto instance = new SimpleItem(typed, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); builtin.append(item); } } if (abort) { return {}; } addImplementationHelperItems(); addOverwritableItems(); eventuallyAddGroup(i18n("Special"), 700, specialItems); eventuallyAddGroup(i18n("Look-ahead Matches"), 800, lookAheadMatcher.matchedItems()); eventuallyAddGroup(i18n("Builtin"), 900, builtin); eventuallyAddGroup(i18n("Macros"), 1000, macros); return items; } void ClangCodeCompletionContext::eventuallyAddGroup(const QString& name, int priority, const QList& items) { if (items.isEmpty()) { return; } auto* node = new CompletionCustomGroupNode(name, priority); node->appendChildren(items); m_ungrouped << CompletionTreeElementPointer(node); } void ClangCodeCompletionContext::addOverwritableItems() { auto overrideList = m_completionHelper.overrides(); if (overrideList.isEmpty()) { return; } QList overrides; QList overridesAbstract; for (const auto& info : overrideList) { QStringList params; params.reserve(info.params.size()); for (const auto& param : info.params) { params << param.type + QLatin1Char(' ') + param.id; } QString nameAndParams = info.name + QLatin1Char('(') + params.join(QStringLiteral(", ")) + QLatin1Char(')'); if(info.isConst) nameAndParams = nameAndParams + QLatin1String(" const"); if(info.isPureVirtual) nameAndParams = nameAndParams + QLatin1String(" = 0"); auto item = CompletionTreeItemPointer(new OverrideItem(nameAndParams, info.returnType)); if (info.isPureVirtual) overridesAbstract << item; else overrides << item; } eventuallyAddGroup(i18n("Abstract Override"), 0, overridesAbstract); eventuallyAddGroup(i18n("Virtual Override"), 0, overrides); } void ClangCodeCompletionContext::addImplementationHelperItems() { const auto implementsList = m_completionHelper.implements(); if (implementsList.isEmpty()) { return; } QList implements; implements.reserve(implementsList.size()); for (const auto& info : implementsList) { implements << CompletionTreeItemPointer(new ImplementsItem(info)); } eventuallyAddGroup(i18n("Implement Function"), 0, implements); } QList ClangCodeCompletionContext::ungroupedElements() { return m_ungrouped; } ClangCodeCompletionContext::ContextFilters ClangCodeCompletionContext::filters() const { return m_filters; } void ClangCodeCompletionContext::setFilters(const ClangCodeCompletionContext::ContextFilters& filters) { m_filters = filters; } #include "context.moc" diff --git a/plugins/clang/codecompletion/includepathcompletioncontext.cpp b/plugins/clang/codecompletion/includepathcompletioncontext.cpp index b4f257d0ac..56252b978f 100644 --- a/plugins/clang/codecompletion/includepathcompletioncontext.cpp +++ b/plugins/clang/codecompletion/includepathcompletioncontext.cpp @@ -1,295 +1,296 @@ /* * This file is part of KDevelop * Copyright 2014 Sergey Kalinichev * Copyright 2015 Milian Wolff * * 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 "includepathcompletioncontext.h" #include "duchain/navigationwidget.h" #include "duchain/clanghelpers.h" #include +#include #include #include #include using namespace KDevelop; /** * Parse the last line of @p text and extract information about any existing include path from it. */ IncludePathProperties IncludePathProperties::parseText(const QString& text, int rightBoundary) { IncludePathProperties properties; int idx = text.lastIndexOf(QLatin1Char('\n')); if (idx == -1) { idx = 0; } if (rightBoundary == -1) { rightBoundary = text.length(); } // what follows is a relatively simple parser for include lines that may contain comments, i.e.: // /*comment*/ #include /*comment*/ "path.h" /*comment*/ enum FindState { FindBang, FindInclude, FindType, FindTypeEnd }; FindState state = FindBang; QChar expectedEnd = QLatin1Char('>'); for (; idx < text.size(); ++idx) { const auto c = text.at(idx); if (c.isSpace()) { continue; } if (c == QLatin1Char('/') && state != FindTypeEnd) { // skip comments if (idx >= text.length() - 1 || text.at(idx + 1) != QLatin1Char('*')) { properties.valid = false; return properties; } idx += 2; while (idx < text.length() - 1 && (text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/'))) { ++idx; } if (idx >= text.length() - 1 || text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/')) { properties.valid = false; return properties; } ++idx; continue; } switch (state) { case FindBang: if (c != QLatin1Char('#')) { return properties; } state = FindInclude; break; case FindInclude: if (text.midRef(idx, 7) != QLatin1String("include")) { return properties; } idx += 6; state = FindType; properties.valid = true; break; case FindType: properties.inputFrom = idx + 1; if (c == QLatin1Char('"')) { expectedEnd = QLatin1Char('"'); properties.local = true; } else if (c != QLatin1Char('<')) { properties.valid = false; return properties; } state = FindTypeEnd; break; case FindTypeEnd: if (c == expectedEnd) { properties.inputTo = idx; // stop iteration idx = text.size(); } break; } } if (!properties.valid) { return properties; } // properly append to existing paths without overriding it // i.e.: #include should become #include // or: #include should again become #include // see unit tests for more examples if (properties.inputFrom != -1) { int end = properties.inputTo; if (end >= rightBoundary || end == -1) { end = text.lastIndexOf(QLatin1Char('/'), rightBoundary - 1) + 1; } if (end > 0) { properties.prefixPath = text.mid(properties.inputFrom, end - properties.inputFrom); properties.inputFrom += properties.prefixPath.length(); } } return properties; } namespace { QVector includeItemsForUrl(const QUrl& url, const IncludePathProperties& properties, const ClangParsingEnvironment::IncludePaths& includePaths) { QVector includeItems; Path::List paths; if (properties.local) { paths.reserve(1 + includePaths.project.size() + includePaths.system.size()); paths.push_back(Path(url).parent()); paths += includePaths.project; paths += includePaths.system; } else { paths = includePaths.system + includePaths.project; } // ensure we don't add duplicate paths QSet handledPaths; // search paths QSet foundIncludePaths; // found items int pathNumber = 0; - for (auto searchPath : paths) { + for (auto searchPath : qAsConst(paths)) { if (handledPaths.contains(searchPath)) { continue; } handledPaths.insert(searchPath); if (!properties.prefixPath.isEmpty()) { searchPath.addPath(properties.prefixPath); } QDirIterator dirIterator(searchPath.toLocalFile()); while (dirIterator.hasNext()) { dirIterator.next(); KDevelop::IncludeItem item; item.name = dirIterator.fileName(); if (item.name.startsWith(QLatin1Char('.')) || item.name.endsWith(QLatin1Char('~'))) { //filter out ".", "..", hidden files, and backups continue; } const auto info = dirIterator.fileInfo(); item.isDirectory = info.isDir(); // filter files that are not a header // note: system headers sometimes don't have any extension, and we still want to show those if (!item.isDirectory && item.name.contains(QLatin1Char('.')) && !ClangHelpers::isHeader(item.name)) { continue; } const QString fullPath = info.canonicalFilePath(); if (foundIncludePaths.contains(fullPath)) { continue; } else { foundIncludePaths.insert(fullPath); } item.basePath = searchPath.toUrl(); item.pathNumber = pathNumber; includeItems << item; } ++pathNumber; } return includeItems; } } class IncludeFileCompletionItem : public AbstractIncludeFileCompletionItem { public: explicit IncludeFileCompletionItem(const IncludeItem& include) : AbstractIncludeFileCompletionItem(include) {} void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { auto document = view->document(); auto range = word; const int lineNumber = word.end().line(); const QString line = document->line(lineNumber); const auto properties = IncludePathProperties::parseText(line, word.end().column()); if (!properties.valid) { return; } QString newText = includeItem.isDirectory ? (includeItem.name + QLatin1Char('/')) : includeItem.name; if (properties.inputFrom == -1) { newText.prepend(QLatin1Char('<')); } else { range.setStart({lineNumber, properties.inputFrom}); } if (properties.inputTo == -1) { // Add suffix if (properties.local) { newText += QLatin1Char('"'); } else { newText += QLatin1Char('>'); } // replace the whole line range.setEnd({lineNumber, line.size()}); } else { range.setEnd({lineNumber, properties.inputTo}); } document->replaceText(range, newText); if (includeItem.isDirectory) { // ensure we can continue to add files/paths when we just added a directory int offset = (properties.inputTo == -1) ? 1 : 0; view->setCursorPosition(range.start() + KTextEditor::Cursor(0, newText.length() - offset)); } else { // place cursor at end of line view->setCursorPosition({lineNumber, document->lineLength(lineNumber)}); } } }; IncludePathCompletionContext::IncludePathCompletionContext(const DUContextPointer& context, const ParseSessionData::Ptr& sessionData, const QUrl& url, const KTextEditor::Cursor& position, const QString& text) : CodeCompletionContext(context, text, CursorInRevision::castFromSimpleCursor(position), 0) { const IncludePathProperties properties = IncludePathProperties::parseText(text); if (!properties.valid) { return; } m_includeItems = includeItemsForUrl(url, properties, sessionData->environment().includes()); } QList< CompletionTreeItemPointer > IncludePathCompletionContext::completionItems(bool& abort, bool) { QList items; - for (const auto& includeItem: m_includeItems) { + for (const auto& includeItem: qAsConst(m_includeItems)) { if (abort) { return items; } items << CompletionTreeItemPointer(new IncludeFileCompletionItem(includeItem)); } return items; } diff --git a/plugins/clang/codegen/clangclasshelper.cpp b/plugins/clang/codegen/clangclasshelper.cpp index 033e89d266..1bf87325df 100644 --- a/plugins/clang/codegen/clangclasshelper.cpp +++ b/plugins/clang/codegen/clangclasshelper.cpp @@ -1,308 +1,309 @@ /* * KDevelop C++ Language Support * * Copyright 2008 Hamish Rodda * Copyright 2012 Miha Čančula * Copyright 2017 Friedrich W. H. Kossebau * * 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 "clangclasshelper.h" #include "util/clangdebug.h" #include "duchain/unknowndeclarationproblem.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; ClangClassHelper::ClangClassHelper() { } ClangClassHelper::~ClangClassHelper() = default; TemplateClassGenerator* ClangClassHelper::createGenerator(const QUrl& baseUrl) { return new ClangTemplateNewClass(baseUrl); } QList ClangClassHelper::defaultMethods(const QString& name) const { // TODO: this is the oldcpp approach, perhaps clang provides this directly? // TODO: default destructor misses info about virtualness, possible needs ICreateClassHelper change? QTemporaryFile file(QDir::tempPath() + QLatin1String("/class_") + name + QLatin1String("_XXXXXX.cpp")); file.open(); QTextStream stream(&file); stream << "class " << name << " {\n" << " public:\n" // default ctor << " " << name << "();\n" // copy ctor << " " << name << "(const " << name << "& other);\n" // default dtor << " ~" << name << "();\n" // assignment operator << " " << name << "& operator=(const " << name << "& other);\n" // equality operators << " bool operator==(const " << name << "& other) const;\n" << " bool operator!=(const " << name << "& other) const;\n" << "};\n"; file.close(); ReferencedTopDUContext context(DUChain::self()->waitForUpdate(IndexedString(file.fileName()), TopDUContext::AllDeclarationsAndContexts)); QList methods; { DUChainReadLocker lock; if (context && context->childContexts().size() == 1) { const auto localDeclarations = context->childContexts().first()->localDeclarations(); methods.reserve(localDeclarations.size()); for (auto* declaration : localDeclarations) { methods << DeclarationPointer(declaration); } } } return methods; } ClangTemplateNewClass::ClangTemplateNewClass(const QUrl& url) : TemplateClassGenerator(url) { } ClangTemplateNewClass::~ClangTemplateNewClass() = default; namespace { QString includeArgumentForFile(const QString& includefile, const Path::List& includePaths, const Path& source) { const auto sourceFolder = source.parent(); const Path canonicalFile(QFileInfo(includefile).canonicalFilePath()); QString shortestDirective; bool isRelative = false; // we can include the file directly if (sourceFolder == canonicalFile.parent()) { shortestDirective = canonicalFile.lastPathSegment(); isRelative = true; } else { // find the include directive with the shortest length for (const auto& includePath : includePaths) { QString relative = includePath.relativePath(canonicalFile); if (relative.startsWith(QLatin1String("./"))) { relative.remove(0, 2); } if (shortestDirective.isEmpty() || relative.length() < shortestDirective.length()) { shortestDirective = relative; isRelative = (includePath == sourceFolder); } } } // Item not found in include path? if (shortestDirective.isEmpty()) { return {}; } if (isRelative) { return QLatin1Char('\"') + shortestDirective + QLatin1Char('\"'); } return QLatin1Char('<') + shortestDirective + QLatin1Char('>'); } QString includeDirectiveArgumentFromPath(const Path& file, const DeclarationPointer& declaration) { const auto includeManager = IDefinesAndIncludesManager::manager(); const auto filePath = file.toLocalFile(); const auto projectModel = ICore::self()->projectController()->projectModel(); auto item = projectModel->itemForPath(IndexedString(filePath)); if (!item) { // try the folder where the file is placed and guess includes from there // prefer target over file const auto folderPath = IndexedString(file.parent().toLocalFile()); clangDebug() << "File not known, guessing includes from items in folder:" << folderPath.str(); // default to the folder, if no targets or files item = projectModel->itemForPath(folderPath); if (item) { const auto targetItems = item->targetList(); bool itemChosen = false; // Prefer items defined inside a target with non-empty includes. for (const auto& targetItem : targetItems) { item = targetItem; if (!includeManager->includes(targetItem, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { clangDebug() << "Guessing includes from target" << targetItem->baseName(); itemChosen = true; break; } } if (!itemChosen) { const auto fileItems = item->fileList(); // Prefer items defined inside a target with non-empty includes. for (const auto& fileItem : fileItems) { item = fileItem; if (!includeManager->includes(fileItem, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { clangDebug() << "Guessing includes from file" << fileItem->baseName(); break; } } } } } const auto includePaths = includeManager->includes(item); if (includePaths.isEmpty()) { clangDebug() << "Include path is empty"; return {}; } clangDebug() << "found include paths for" << file << ":" << includePaths; const auto includeFiles = UnknownDeclarationProblem::findMatchingIncludeFiles(QVector {declaration.data()}); if (includeFiles.isEmpty()) { // return early as the computation of the include paths is quite expensive return {}; } // create include arguments for all candidates QStringList includeArguments; includeArguments.reserve(includeFiles.size()); for (const auto& includeFile : includeFiles) { const auto includeArgument = includeArgumentForFile(includeFile, includePaths, file); if (includeArgument.isEmpty()) { clangDebug() << "unable to create include argument for" << includeFile << "in" << file.toLocalFile(); } includeArguments << includeArgument; } if (includeArguments.isEmpty()) { return {}; } std::sort(includeArguments.begin(), includeArguments.end(), [](const QString& lhs, const QString& rhs) { return lhs.length() < rhs.length(); }); return includeArguments.at(0); } template void addVariables(QVariantHash* variables, QLatin1String suffix, const Map& map) { for (auto it = map.begin(), end = map.end(); it != end; ++it) { variables->insert(it.key() + suffix, CodeDescription::toVariantList(it.value())); } } } QVariantHash ClangTemplateNewClass::extraVariables() const { QVariantHash variables; const QString publicAccess = QStringLiteral("public"); QHash variableDescriptions; QHash functionDescriptions; QHash slotDescriptions; FunctionDescriptionList signalDescriptions; const auto desc = description(); for (const auto& function : desc.methods) { const QString& access = function.access.isEmpty() ? publicAccess : function.access; if (function.isSignal) { signalDescriptions << function; } else if (function.isSlot) { slotDescriptions[access] << function; } else { functionDescriptions[access] << function; } } for (const auto& variable : desc.members) { const QString& access = variable.access.isEmpty() ? publicAccess : variable.access; variableDescriptions[access] << variable; } ::addVariables(&variables, QLatin1String("_members"), variableDescriptions); ::addVariables(&variables, QLatin1String("_functions"), functionDescriptions); ::addVariables(&variables, QLatin1String("_slots"), slotDescriptions); variables[QStringLiteral("signals")] = CodeDescription::toVariantList(signalDescriptions); variables[QStringLiteral("needs_qobject_macro")] = !slotDescriptions.isEmpty() || !signalDescriptions.isEmpty(); QStringList includedFiles; DUChainReadLocker locker(DUChain::lock()); QUrl sourceUrl; const auto urls = fileUrls(); if (!urls.isEmpty()) { sourceUrl = urls.constBegin().value(); } else { // includeDirectiveArgumentFromPath() expects a path to the folder where includes are used from sourceUrl = baseUrl(); sourceUrl.setPath(sourceUrl.path() + QLatin1String("/.h")); } const Path sourcePath(sourceUrl); - for (const auto& baseClass : directBaseClasses()) { + const auto& directBaseClasses = this->directBaseClasses(); + for (const auto& baseClass : directBaseClasses) { if (!baseClass) { continue; } clangDebug() << "Looking for includes for class" << baseClass->identifier().toString(); const QString includeDirective = includeDirectiveArgumentFromPath(sourcePath, baseClass); if (!includeDirective.isEmpty()) { includedFiles << includeDirective; } } variables[QStringLiteral("included_files")] = includedFiles; return variables; } DocumentChangeSet ClangTemplateNewClass::generate() { addVariables(extraVariables()); return TemplateClassGenerator::generate(); } diff --git a/plugins/clang/duchain/clangproblem.cpp b/plugins/clang/duchain/clangproblem.cpp index 190b5f4c1c..aa1005e7c0 100644 --- a/plugins/clang/duchain/clangproblem.cpp +++ b/plugins/clang/duchain/clangproblem.cpp @@ -1,269 +1,271 @@ /* * Copyright 2014 Kevin Funk * * 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 "clangproblem.h" #include #include #include "util/clangtypes.h" #include "util/clangdebug.h" #include #include +#include #include using namespace KDevelop; namespace { IProblem::Severity diagnosticSeverityToSeverity(CXDiagnosticSeverity severity, const QString& optionName) { switch (severity) { case CXDiagnostic_Fatal: case CXDiagnostic_Error: return IProblem::Error; case CXDiagnostic_Warning: if (optionName.startsWith(QLatin1String("-Wunused-"))) { return IProblem::Hint; } return IProblem::Warning; break; default: return IProblem::Hint; } } /** * Clang diagnostic messages always start with a lowercase character * * @return Prettified version, starting with uppercase character */ inline QString prettyDiagnosticSpelling(const QString& str) { QString ret = str; if (ret.isEmpty()) { return {}; } ret[0] = ret[0].toUpper(); return ret; } ClangFixits fixitsForDiagnostic(CXDiagnostic diagnostic) { ClangFixits fixits; auto numFixits = clang_getDiagnosticNumFixIts(diagnostic); fixits.reserve(numFixits); for (uint i = 0; i < numFixits; ++i) { CXSourceRange range; const QString replacementText = ClangString(clang_getDiagnosticFixIt(diagnostic, i, &range)).toString(); const auto docRange = ClangRange(range).toDocumentRange(); auto doc = KDevelop::ICore::self()->documentController()->documentForUrl(docRange.document.toUrl()); const QString original = doc ? doc->text(docRange) : QString{}; fixits << ClangFixit{replacementText, docRange, QString(), original}; } return fixits; } } QDebug operator<<(QDebug debug, const ClangFixit& fixit) { debug.nospace() << "ClangFixit[" << "replacementText=" << fixit.replacementText << ", range=" << fixit.range << ", description=" << fixit.description << "]"; return debug; } ClangProblem::ClangProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) { const QString diagnosticOption = ClangString(clang_getDiagnosticOption(diagnostic, nullptr)).toString(); auto severity = diagnosticSeverityToSeverity(clang_getDiagnosticSeverity(diagnostic), diagnosticOption); setSeverity(severity); QString description = ClangString(clang_getDiagnosticSpelling(diagnostic)).toString(); if (!diagnosticOption.isEmpty()) { description.append(QLatin1String(" [") + diagnosticOption + QLatin1Char(']')); } setDescription(prettyDiagnosticSpelling(description)); ClangLocation location(clang_getDiagnosticLocation(diagnostic)); CXFile diagnosticFile; clang_getFileLocation(location, &diagnosticFile, nullptr, nullptr, nullptr); const ClangString fileName(clang_getFileName(diagnosticFile)); DocumentRange docRange(IndexedString(QUrl::fromLocalFile(fileName.toString()).adjusted(QUrl::NormalizePathSegments)), KTextEditor::Range(location, location)); const uint numRanges = clang_getDiagnosticNumRanges(diagnostic); for (uint i = 0; i < numRanges; ++i) { auto range = ClangRange(clang_getDiagnosticRange(diagnostic, i)).toRange(); if(!range.isValid()){ continue; } if (range.start() < docRange.start()) { docRange.setStart(range.start()); } if (range.end() > docRange.end()) { docRange.setEnd(range.end()); } } if (docRange.isEmpty()) { // try to find a bigger range for the given location by using the token at the given location CXFile file = nullptr; unsigned line = 0; unsigned column = 0; clang_getExpansionLocation(location, &file, &line, &column, nullptr); // just skip ahead some characters, hoping that it's sufficient to encompass // a token we can use for building the range auto nextLocation = clang_getLocation(unit, file, line, column + 100); auto rangeToTokenize = clang_getRange(location, nextLocation); const ClangTokens tokens(unit, rangeToTokenize); if (tokens.size()) { docRange.setRange(ClangRange(clang_getTokenExtent(unit, tokens.at(0))).toRange()); } } setFixits(fixitsForDiagnostic(diagnostic)); setFinalLocation(docRange); setSource(IProblem::SemanticAnalysis); QVector diagnostics; auto childDiagnostics = clang_getChildDiagnostics(diagnostic); auto numChildDiagnostics = clang_getNumDiagnosticsInSet(childDiagnostics); diagnostics.reserve(numChildDiagnostics); for (uint j = 0; j < numChildDiagnostics; ++j) { auto childDiagnostic = clang_getDiagnosticInSet(childDiagnostics, j); ClangProblem::Ptr problem(new ClangProblem(childDiagnostic, unit)); diagnostics << ProblemPointer(problem.data()); } setDiagnostics(diagnostics); } IAssistant::Ptr ClangProblem::solutionAssistant() const { if (allFixits().isEmpty()) { return {}; } return IAssistant::Ptr(new ClangFixitAssistant(allFixits())); } ClangFixits ClangProblem::fixits() const { return m_fixits; } void ClangProblem::setFixits(const ClangFixits& fixits) { m_fixits = fixits; } ClangFixits ClangProblem::allFixits() const { ClangFixits result; result << m_fixits; - for (const IProblem::Ptr& diagnostic : diagnostics()) { + const auto& diagnostics = this->diagnostics(); + for (const IProblem::Ptr& diagnostic : diagnostics) { const Ptr problem(dynamic_cast(diagnostic.data())); Q_ASSERT(problem); result << problem->allFixits(); } return result; } ClangFixitAssistant::ClangFixitAssistant(const ClangFixits& fixits) : m_title(i18n("Fix-it Hints")) , m_fixits(fixits) { } ClangFixitAssistant::ClangFixitAssistant(const QString& title, const ClangFixits& fixits) : m_title(title) , m_fixits(fixits) { } QString ClangFixitAssistant::title() const { return m_title; } void ClangFixitAssistant::createActions() { KDevelop::IAssistant::createActions(); - for (const ClangFixit& fixit : m_fixits) { + for (const ClangFixit& fixit : qAsConst(m_fixits)) { addAction(IAssistantAction::Ptr(new ClangFixitAction(fixit))); } } ClangFixits ClangFixitAssistant::fixits() const { return m_fixits; } ClangFixitAction::ClangFixitAction(const ClangFixit& fixit) : m_fixit(fixit) { } QString ClangFixitAction::description() const { if (!m_fixit.description.isEmpty()) return m_fixit.description; const auto range = m_fixit.range; if (range.start() == range.end()) { return i18n("Insert \"%1\" at line: %2, column: %3", m_fixit.replacementText, range.start().line()+1, range.start().column()+1); } else if (range.start().line() == range.end().line()) { if (m_fixit.currentText.isEmpty()) { return i18n("Replace text at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } else return i18n("Replace \"%1\" with: \"%2\"", m_fixit.currentText, m_fixit.replacementText); } else { return i18n("Replace multiple lines starting at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } } void ClangFixitAction::execute() { DocumentChangeSet changes; { DUChainReadLocker lock; DocumentChange change(m_fixit.range.document, m_fixit.range, m_fixit.currentText, m_fixit.replacementText); change.m_ignoreOldText = !m_fixit.currentText.isEmpty(); changes.addChange(change); } changes.setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); changes.applyAllChanges(); emit executed(this); } diff --git a/plugins/clang/duchain/navigationwidget.cpp b/plugins/clang/duchain/navigationwidget.cpp index bb17212edb..d79c811ec5 100644 --- a/plugins/clang/duchain/navigationwidget.cpp +++ b/plugins/clang/duchain/navigationwidget.cpp @@ -1,127 +1,128 @@ /* * This file is part of KDevelop * Copyright 2014 Milian Wolff * Copyright 2014 Kevin Funk * * 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 "navigationwidget.h" #include "macronavigationcontext.h" #include "types/classspecializationtype.h" #include #include #include using namespace KDevelop; class DeclarationNavigationContext : public AbstractDeclarationNavigationContext { public: using AbstractDeclarationNavigationContext::AbstractDeclarationNavigationContext; void htmlIdentifiedType(AbstractType::Ptr type, const IdentifiedType* idType) override { AbstractDeclarationNavigationContext::htmlIdentifiedType(type, idType); if (auto cst = dynamic_cast(type.data())) { modifyHtml() += QStringLiteral("< ").toHtmlEscaped(); bool first = true; - for (const auto& type : cst->templateParameters()) { + const auto& templateParameters = cst->templateParameters(); + for (const auto& type : templateParameters) { if (first) { first = false; } else { modifyHtml() += QStringLiteral(", "); } eventuallyMakeTypeLinks(type.abstractType()); } modifyHtml() += QStringLiteral(" >").toHtmlEscaped(); } } }; class IncludeNavigationContext : public KDevelop::AbstractIncludeNavigationContext { public: IncludeNavigationContext(const KDevelop::IncludeItem& item, const KDevelop::TopDUContextPointer& topContext); protected: bool filterDeclaration(KDevelop::Declaration* decl) override; }; IncludeNavigationContext::IncludeNavigationContext(const IncludeItem& item, const KDevelop::TopDUContextPointer& topContext) : AbstractIncludeNavigationContext(item, topContext, StandardParsingEnvironment) {} bool IncludeNavigationContext::filterDeclaration(Declaration* decl) { QString declId = decl->identifier().identifier().str(); //filter out forward-declarations and macro-expansions without a range //And filter out declarations with reserved identifiers return !decl->qualifiedIdentifier().toString().isEmpty() && !decl->range().isEmpty() && !decl->isForwardDeclaration() && !(declId.startsWith(QLatin1String("__")) || (declId.startsWith(QLatin1Char('_')) && declId.length() > 1 && declId[1].isUpper()) ); } ClangNavigationWidget::ClangNavigationWidget(const DeclarationPointer& declaration, KDevelop::AbstractNavigationWidget::DisplayHints hints) : AbstractNavigationWidget() { setDisplayHints(hints); if (auto macro = declaration.dynamicCast()) { initBrowser(200); setContext(NavigationContextPointer(new MacroNavigationContext(macro))); } else { initBrowser(400); setContext(NavigationContextPointer(new DeclarationNavigationContext(declaration, {}))); } } ClangNavigationWidget::ClangNavigationWidget(const MacroDefinition::Ptr& macro, const KDevelop::DocumentCursor& expansionLocation, KDevelop::AbstractNavigationWidget::DisplayHints hints) : AbstractNavigationWidget() { setDisplayHints(hints); initBrowser(400); setContext(NavigationContextPointer(new MacroNavigationContext(macro, expansionLocation))); } ClangNavigationWidget::ClangNavigationWidget(const IncludeItem& includeItem, const KDevelop::TopDUContextPointer& topContext, const QString& htmlPrefix, const QString& htmlSuffix, KDevelop::AbstractNavigationWidget::DisplayHints hints) : AbstractNavigationWidget() { setDisplayHints(hints); initBrowser(200); //The first context is registered so it is kept alive by the shared-pointer mechanism auto context = new IncludeNavigationContext(includeItem, topContext); context->setPrefixSuffix(htmlPrefix, htmlSuffix); setContext(NavigationContextPointer(context)); } QString ClangNavigationWidget::shortDescription(const IncludeItem& includeItem) { IncludeNavigationContext ctx(includeItem, {}); return ctx.html(true); } diff --git a/plugins/clang/duchain/types/classspecializationtype.cpp b/plugins/clang/duchain/types/classspecializationtype.cpp index 8ba42df90c..308ed1bb2e 100644 --- a/plugins/clang/duchain/types/classspecializationtype.cpp +++ b/plugins/clang/duchain/types/classspecializationtype.cpp @@ -1,156 +1,157 @@ /* * This file is part of KDevelop * * Copyright 2015 Sergey Kalinichev * * 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 "classspecializationtype.h" using namespace KDevelop; #include #include // The type is registered in DUChainUtils::registerDUChainItems // REGISTER_TYPE(ClassSpecializationType); DEFINE_LIST_MEMBER_HASH(ClassSpecializationTypeData, parameters, IndexedType) ClassSpecializationTypeData::ClassSpecializationTypeData() { initializeAppendedLists(m_dynamic); } ClassSpecializationTypeData::ClassSpecializationTypeData(const ClassSpecializationTypeData& rhs) : KDevelop::StructureTypeData(rhs) { initializeAppendedLists(m_dynamic); copyListsFrom(rhs); } ClassSpecializationTypeData::~ClassSpecializationTypeData() { freeAppendedLists(); } ClassSpecializationTypeData& ClassSpecializationTypeData::operator=(const ClassSpecializationTypeData&) { return *this; } ClassSpecializationType::ClassSpecializationType(const ClassSpecializationType& rhs) : KDevelop::StructureType(copyData(*rhs.d_func())) {} ClassSpecializationType::ClassSpecializationType(ClassSpecializationTypeData& data) : KDevelop::StructureType(data) {} AbstractType* ClassSpecializationType::clone() const { return new ClassSpecializationType(*this); } ClassSpecializationType::ClassSpecializationType() : KDevelop::StructureType(createData()) {} uint ClassSpecializationType::hash() const { KDevHash kdevhash(StructureType::hash()); FOREACH_FUNCTION(const auto& param, d_func()->parameters) { kdevhash << param.hash(); } return kdevhash; } namespace { // we need to skip the template parameters of the last identifier, // so do the stringification manually here QString strippedQid(const QualifiedIdentifier& qid) { QString result; if (qid.explicitlyGlobal()) { result += QLatin1String("::"); } const auto parts = qid.count(); for (int i = 0; i < parts - 1; ++i) { result += qid.at(i).toString() + QLatin1String("::"); } const auto last = qid.at(parts - 1); result += last.identifier().str(); return result; } } QString ClassSpecializationType::toString() const { QualifiedIdentifier id = qualifiedIdentifier(); if (!id.isEmpty()) { QString result = AbstractType::toString() + strippedQid(id) + QLatin1String("< "); bool first = true; - for (const auto& param : templateParameters()) { + const auto& templateParameters = this->templateParameters(); + for (const auto& param : templateParameters) { if (first) { first = false; } else { result += QLatin1String(", "); } result += param.abstractType()->toString(); } result += QLatin1String(" >"); return result; } return StructureType::toString(); } bool ClassSpecializationType::equals(const KDevelop::AbstractType* rhs) const { if (this == rhs) { return true; } auto tt = dynamic_cast(rhs); if (!tt || templateParameters() != tt->templateParameters()) { return false; } return StructureType::equals(rhs); } QVector ClassSpecializationType::templateParameters() const { const auto size = d_func()->parametersSize(); QVector parameters(size); std::copy_n(d_func()->parameters(), size, parameters.begin()); return parameters; } void ClassSpecializationType::addParameter(const KDevelop::IndexedType& param) { d_func_dynamic()->parametersList().append(param); } void ClassSpecializationType::clearParameters() { d_func_dynamic()->parametersList().clear(); } diff --git a/plugins/clang/duchain/unknowndeclarationproblem.cpp b/plugins/clang/duchain/unknowndeclarationproblem.cpp index 94599f3454..54b15c2c5a 100644 --- a/plugins/clang/duchain/unknowndeclarationproblem.cpp +++ b/plugins/clang/duchain/unknowndeclarationproblem.cpp @@ -1,559 +1,566 @@ /* * Copyright 2014 Jørgen Kvalsvik * Copyright 2014 Kevin Funk * * 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 "unknowndeclarationproblem.h" #include "clanghelpers.h" #include "parsesession.h" #include "../util/clangdebug.h" #include "../util/clangutils.h" #include "../util/clangtypes.h" #include "../clangsettings/clangsettingsmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { /** Under some conditions, such as when looking up suggestions * for the undeclared namespace 'std' we will get an awful lot * of suggestions. This parameter limits how many suggestions * will pop up, as rarely more than a few will be relevant anyways * * Forward declaration suggestions are included in this number */ const int maxSuggestions = 5; /** * We don't want anything from the bits directory - * we'd rather prefer forwarding includes, such as */ bool isBlacklisted(const QString& path) { if (ClangHelpers::isSource(path)) return true; // Do not allow including directly from the bits directory. // Instead use one of the forwarding headers in other directories, when possible. if (path.contains( QLatin1String("bits") ) && path.contains(QLatin1String("/include/c++/"))) return true; return false; } QStringList scanIncludePaths( const QString& identifier, const QDir& dir, int maxDepth = 3 ) { if (!maxDepth) { return {}; } QStringList candidates; const auto path = dir.absolutePath(); if( isBlacklisted( path ) ) { return {}; } const QStringList nameFilters = {identifier, identifier + QLatin1String(".*")}; - for (const auto& file : dir.entryList(nameFilters, QDir::Files)) { + const auto& files = dir.entryList(nameFilters, QDir::Files); + for (const auto& file : files) { if (identifier.compare(file, Qt::CaseInsensitive) == 0 || ClangHelpers::isHeader(file)) { const QString filePath = path + QLatin1Char('/') + file; clangDebug() << "Found candidate file" << filePath; candidates.append( filePath ); } } maxDepth--; - for( const auto& subdir : dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) ) + const auto& subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const auto& subdir : subdirs) { candidates += scanIncludePaths( identifier, QDir{ path + QLatin1Char('/') + subdir }, maxDepth ); + } return candidates; } /** * Find files in dir that match the given identifier. Matches common C++ header file extensions only. */ QStringList scanIncludePaths( const QualifiedIdentifier& identifier, const KDevelop::Path::List& includes ) { const auto stripped_identifier = identifier.last().toString(); QStringList candidates; for( const auto& include : includes ) { candidates += scanIncludePaths( stripped_identifier, QDir{ include.toLocalFile() } ); } std::sort( candidates.begin(), candidates.end() ); candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() ); return candidates; } /** * Determine how much path is shared between two includes. * boost/tr1/unordered_map * boost/tr1/unordered_set * have a shared path of 2 where * boost/tr1/unordered_map * boost/vector * have a shared path of 1 */ int sharedPathLevel(const QString& a, const QString& b) { int shared = -1; for(auto x = a.begin(), y = b.begin(); *x == *y && x != a.end() && y != b.end() ; ++x, ++y ) { if( *x == QDir::separator() ) { ++shared; } } return shared; } /** * Try to find a proper include position from the DUChain: * * look at existing imports (i.e. #include's) and find a fitting * file with the same/similar path to the new include file and use that * * TODO: Implement a fallback scheme */ KDevelop::DocumentRange includeDirectivePosition(const KDevelop::Path& source, const QString& includeFile) { static const QRegularExpression mocFilenameExpression(QStringLiteral("(moc_[^\\/\\\\]+\\.cpp$|\\.moc$)") ); DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range"; return KDevelop::DocumentRange::invalid(); } int line = -1; // look at existing #include statements and re-use them int currentMatchQuality = -1; - for( const auto& import : top->importedParentContexts() ) { + const auto& importedParentContexts = top->importedParentContexts(); + for (const auto& import : importedParentContexts) { const auto importFilename = import.context(top)->url().str(); const int matchQuality = sharedPathLevel( importFilename , includeFile ); if( matchQuality < currentMatchQuality ) { continue; } const auto match = mocFilenameExpression.match(importFilename); if (match.hasMatch()) { clangDebug() << "moc file detected in" << source.toUrl().toDisplayString() << ":" << importFilename << "-- not using as include insertion location"; continue; } line = import.position.line + 1; currentMatchQuality = matchQuality; } if( line == -1 ) { /* Insert at the top of the document */ return {IndexedString(source.pathOrUrl()), {0, 0, 0, 0}}; } return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}}; } KDevelop::DocumentRange forwardDeclarationPosition(const QualifiedIdentifier& identifier, const KDevelop::Path& source) { DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range"; return KDevelop::DocumentRange::invalid(); } if (!top->findDeclarations(identifier).isEmpty()) { // Already forward-declared return KDevelop::DocumentRange::invalid(); } int line = std::numeric_limits< int >::max(); - for( const auto decl : top->localDeclarations() ) { + const auto& localDeclarations = top->localDeclarations(); + for (const auto decl : localDeclarations) { line = std::min( line, decl->range().start.line ); } if( line == std::numeric_limits< int >::max() ) { return KDevelop::DocumentRange::invalid(); } // We want it one line above the first declaration line = std::max( line - 1, 0 ); return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}}; } /** * Iteratively build all levels of the current scope. A (missing) type anywhere * can be arbitrarily namespaced, so we create the permutations of possible * nestings of namespaces it can currently be in, * * TODO: add detection of namespace aliases, such as 'using namespace KDevelop;' * * namespace foo { * namespace bar { * function baz() { * type var; * } * } * } * * Would give: * foo::bar::baz::type * foo::bar::type * foo::type * type */ QVector findPossibleQualifiedIdentifiers( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::CursorInRevision& cursor ) { DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( file.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << file.toLocalFile() << "Not creating duchain candidates"; return {}; } const auto* context = top->findContextAt( cursor ); if( !context ) { clangDebug() << "No context found at" << cursor; return {}; } QVector declarations{ identifier }; auto scopes = context->scopeIdentifier(); declarations.reserve(declarations.size() + scopes.count()); for (; !scopes.isEmpty(); scopes.pop()) { declarations.append( scopes + identifier ); } clangDebug() << "Possible declarations:" << declarations; return declarations; } } QStringList UnknownDeclarationProblem::findMatchingIncludeFiles(const QVector& declarations) { DUChainReadLocker lock; QStringList candidates; for (const auto decl: declarations) { // skip declarations that don't belong to us const auto& file = decl->topContext()->parsingEnvironmentFile(); if (!file || file->language() != ParseSession::languageString()) { continue; } if( dynamic_cast( decl ) ) { continue; } if( decl->isForwardDeclaration() ) { continue; } const auto filepath = decl->url().toUrl().toLocalFile(); if( !isBlacklisted( filepath ) ) { candidates << filepath; clangDebug() << "Adding" << filepath << "determined from candidate" << decl->toString(); } - for( const auto importer : file->importers() ) { + const auto& importers = file->importers(); + for (const auto& importer : importers) { if( importer->imports().count() != 1 && !isBlacklisted( filepath ) ) { continue; } if( importer->topContext()->localDeclarations().count() ) { continue; } const auto filePath = importer->url().toUrl().toLocalFile(); if( isBlacklisted( filePath ) ) { continue; } /* This file is a forwarder, such as * does not actually implement the functions, but include other headers that do * we prefer this to other headers */ candidates << filePath; clangDebug() << "Adding forwarder file" << filePath << "to the result set"; } } std::sort( candidates.begin(), candidates.end() ); candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() ); clangDebug() << "Candidates: " << candidates; return candidates; } namespace { /** * Takes a filepath and the include paths and determines what directive to use. */ ClangFixit directiveForFile( const QString& includefile, const KDevelop::Path::List& includepaths, const KDevelop::Path& source ) { const auto sourceFolder = source.parent(); const Path canonicalFile( QFileInfo( includefile ).canonicalFilePath() ); QString shortestDirective; bool isRelative = false; // we can include the file directly if (sourceFolder == canonicalFile.parent()) { shortestDirective = canonicalFile.lastPathSegment(); isRelative = true; } else { // find the include directive with the shortest length for( const auto& includePath : includepaths ) { QString relative = includePath.relativePath( canonicalFile ); if( relative.startsWith( QLatin1String("./") ) ) relative.remove(0, 2); if( shortestDirective.isEmpty() || relative.length() < shortestDirective.length() ) { shortestDirective = relative; isRelative = includePath == sourceFolder; } } } if( shortestDirective.isEmpty() ) { // Item not found in include path return {}; } const auto range = DocumentRange(IndexedString(source.pathOrUrl()), includeDirectivePosition(source, canonicalFile.lastPathSegment())); if( !range.isValid() ) { clangDebug() << "unable to determine valid position for" << includefile << "in" << source.pathOrUrl(); return {}; } QString directive; if( isRelative ) { directive = QStringLiteral("#include \"%1\"").arg(shortestDirective); } else { directive = QStringLiteral("#include <%1>").arg(shortestDirective); } return ClangFixit{directive + QLatin1Char('\n'), range, i18n("Insert \'%1\'", directive)}; } KDevelop::Path::List includePaths( const KDevelop::Path& file ) { // Find project's custom include paths const auto source = file.toLocalFile(); const auto item = ICore::self()->projectController()->projectModel()->itemForPath( KDevelop::IndexedString( source ) ); return IDefinesAndIncludesManager::manager()->includes(item); } /** * Return a list of header files viable for inclusions. All elements will be unique */ QStringList includeFiles(const QualifiedIdentifier& identifier, const QVector& declarations, const KDevelop::Path& file) { const auto includes = includePaths( file ); if( includes.isEmpty() ) { clangDebug() << "Include path is empty"; return {}; } const auto candidates = UnknownDeclarationProblem::findMatchingIncludeFiles(declarations); if( !candidates.isEmpty() ) { // If we find a candidate from the duchain we don't bother scanning the include paths return candidates; } return scanIncludePaths(identifier, includes); } /** * Construct viable forward declarations for the type name. */ ClangFixits forwardDeclarations(const QVector& matchingDeclarations, const Path& source) { DUChainReadLocker lock; ClangFixits fixits; for (const auto decl : matchingDeclarations) { const auto qid = decl->qualifiedIdentifier(); if (qid.count() > 1) { // TODO: Currently we're not able to determine what is namespaces, class names etc // and makes a suitable forward declaration, so just suggest "vanilla" declarations. continue; } const auto range = forwardDeclarationPosition(qid, source); if (!range.isValid()) { continue; // do not know where to insert } if (const auto classDecl = dynamic_cast(decl)) { const auto name = qid.last().toString(); switch (classDecl->classType()) { case ClassDeclarationData::Class: fixits += { QLatin1String("class ") + name + QLatin1String(";\n"), range, i18n("Forward declare as 'class'") }; break; case ClassDeclarationData::Struct: fixits += { QLatin1String("struct ") + name + QLatin1String(";\n"), range, i18n("Forward declare as 'struct'") }; break; default: break; } } } return fixits; } /** * Search the persistent symbol table for matching declarations for identifiers @p identifiers */ QVector findMatchingDeclarations(const QVector& identifiers) { DUChainReadLocker lock; QVector matchingDeclarations; matchingDeclarations.reserve(identifiers.size()); for (const auto& declaration : identifiers) { clangDebug() << "Considering candidate declaration" << declaration; const IndexedDeclaration* declarations; uint declarationCount; PersistentSymbolTable::self().declarations( declaration , declarationCount, declarations ); for (uint i = 0; i < declarationCount; ++i) { // Skip if the declaration is invalid or if it is an alias declaration - // we want the actual declaration (and its file) if (auto decl = declarations[i].declaration()) { matchingDeclarations << decl; } } } return matchingDeclarations; } ClangFixits fixUnknownDeclaration( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::DocumentRange& docrange ) { ClangFixits fixits; const CursorInRevision cursor{docrange.start().line(), docrange.start().column()}; const auto possibleIdentifiers = findPossibleQualifiedIdentifiers(identifier, file, cursor); const auto matchingDeclarations = findMatchingDeclarations(possibleIdentifiers); if (ClangSettingsManager::self()->assistantsSettings().forwardDeclare) { - for (const auto& fixit : forwardDeclarations(matchingDeclarations, file)) { + const auto& forwardDeclareFixits = forwardDeclarations(matchingDeclarations, file); + for (const auto& fixit : forwardDeclareFixits) { fixits << fixit; if (fixits.size() == maxSuggestions) { return fixits; } } } const auto includefiles = includeFiles(identifier, matchingDeclarations, file); if (includefiles.isEmpty()) { // return early as the computation of the include paths is quite expensive return fixits; } const auto includepaths = includePaths( file ); clangDebug() << "found include paths for" << file << ":" << includepaths; /* create fixits for candidates */ for( const auto& includeFile : includefiles ) { const auto fixit = directiveForFile( includeFile, includepaths, file /* UP */ ); if (!fixit.range.isValid()) { clangDebug() << "unable to create directive for" << includeFile << "in" << file.toLocalFile(); continue; } fixits << fixit; if (fixits.size() == maxSuggestions) { return fixits; } } return fixits; } QString symbolFromDiagnosticSpelling(const QString& str) { /* in all error messages the symbol is in in the first pair of quotes */ const auto split = str.split( QLatin1Char('\'') ); auto symbol = split.value( 1 ); if( str.startsWith( QLatin1String("No member named") ) ) { symbol = split.value( 3 ) + QLatin1String("::") + split.value( 1 ); } return symbol; } } UnknownDeclarationProblem::UnknownDeclarationProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) : ClangProblem(diagnostic, unit) { setSymbol(QualifiedIdentifier(symbolFromDiagnosticSpelling(description()))); } void UnknownDeclarationProblem::setSymbol(const QualifiedIdentifier& identifier) { m_identifier = identifier; } IAssistant::Ptr UnknownDeclarationProblem::solutionAssistant() const { const Path path(finalLocation().document.str()); const auto fixits = allFixits() + fixUnknownDeclaration(m_identifier, path, finalLocation()); return IAssistant::Ptr(new ClangFixitAssistant(fixits)); } diff --git a/plugins/clang/tests/clang-parser.cpp b/plugins/clang/tests/clang-parser.cpp index d0651bb690..adfde4caf7 100644 --- a/plugins/clang/tests/clang-parser.cpp +++ b/plugins/clang/tests/clang-parser.cpp @@ -1,148 +1,148 @@ /* 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 #include #include "../duchain/parsesession.h" #include "../duchain/debugvisitor.h" #include "../duchain/clangindex.h" #include "../util/clangtypes.h" using namespace KDevelop; using namespace KDevelopUtils; class ClangParser { public: ClangParser(const bool printAst, const bool printTokens) : m_session({}) , m_printAst(printAst) , m_printTokens(printTokens) { } /// parse contents of a file void parseFile( const QString &fileName ) { if (!QFile::exists(fileName)) { qerr << "File to parse does not exist: " << fileName << endl; return; } m_session.setData(ParseSessionData::Ptr(new ParseSessionData({}, &m_index, environment(fileName)))); runSession(); } /// parse code directly void parseCode( const QString &code ) { const QString fileName = QStringLiteral("stdin.cpp"); m_session.setData(ParseSessionData::Ptr(new ParseSessionData({UnsavedFile(fileName, {code})}, &m_index, environment(fileName)))); runSession(); } void setIncludePaths(const QStringList& paths) { m_includePaths = paths; } private: /** * actually run the parse session */ void runSession() { if (!m_session.unit()) { qerr << "failed to parse code" << endl; } if (m_printTokens) { CXTranslationUnit TU = m_session.unit(); auto cursor = clang_getTranslationUnitCursor(TU); CXSourceRange range = clang_getCursorExtent(cursor); const ClangTokens tokens(TU, range); for (CXToken token : tokens) { CXString spelling = clang_getTokenSpelling(TU, token); qout << "token= " << clang_getCString(spelling) << endl; clang_disposeString(spelling); } } if (!m_session.unit()) { qerr << "no AST tree could be generated" << endl; exit(255); return; } qout << "AST tree successfully generated" << endl; auto file = m_session.mainFile(); if (m_printAst) { DebugVisitor visitor(&m_session); visitor.visit(m_session.unit(), file); } const auto problems = m_session.problemsForFile(file); if (!problems.isEmpty()) { qerr << endl << "problems encountered during parsing:" << endl; - for (const ProblemPointer problem : problems) { + for (const ProblemPointer& problem : problems) { qerr << problem->toString() << endl; } } else { qout << "no problems encountered during parsing" << endl; } } ClangParsingEnvironment environment(const QString& fileName) const { ClangParsingEnvironment environment; environment.setTranslationUnitUrl(IndexedString(fileName)); environment.addIncludes(toPathList(m_includePaths)); return environment; } ParseSession m_session; const bool m_printAst; const bool m_printTokens; ClangIndex m_index; QStringList m_includePaths; }; namespace KDevelopUtils { template<> void setupCustomArgs(QCommandLineParser* args) { args->addOption(QCommandLineOption{QStringList{"I", "include"}, i18n("add include path"), QStringLiteral("include")}); } template<> void setCustomArgs(ClangParser* parser, QCommandLineParser* args) { parser->setIncludePaths(args->values(QStringLiteral("include"))); } } int main(int argc, char* argv[]) { KAboutData aboutData( QStringLiteral("clang-parser"), i18n( "clang-parser" ), QStringLiteral("1"), i18n("KDevelop Clang parser debugging utility"), KAboutLicense::GPL, i18n( "2013 Milian Wolff" ), QString(), QStringLiteral("http://www.kdevelop.org") ); return KDevelopUtils::initAndRunParser(aboutData, argc, argv); } diff --git a/plugins/clang/tests/test_duchain.cpp b/plugins/clang/tests/test_duchain.cpp index f325ea58da..997fac9bee 100644 --- a/plugins/clang/tests/test_duchain.cpp +++ b/plugins/clang/tests/test_duchain.cpp @@ -1,2087 +1,2088 @@ /* * Copyright 2014 Milian Wolff * Copyright 2014 Kevin Funk * Copyright 2015 Sergey Kalinichev * * 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 "test_duchain.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 "duchain/clangparsingenvironmentfile.h" #include "duchain/clangparsingenvironment.h" #include "duchain/parsesession.h" #include #include #include #include #include #include QTEST_MAIN(TestDUChain) using namespace KDevelop; class TestEnvironmentProvider final : public IDefinesAndIncludesManager::BackgroundProvider { public: ~TestEnvironmentProvider() override = default; QHash< QString, QString > definesInBackground(const QString& /*path*/) const override { return defines; } Path::List includesInBackground(const QString& /*path*/) const override { return includes; } Path::List frameworkDirectoriesInBackground(const QString&) const override { return {}; } IDefinesAndIncludesManager::Type type() const override { return IDefinesAndIncludesManager::UserDefined; } QHash defines; Path::List includes; }; TestDUChain::~TestDUChain() = default; void TestDUChain::initTestCase() { QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevelop.plugins.clang.debug=true\n")); QVERIFY(qputenv("KDEV_CLANG_DISPLAY_DIAGS", "1")); AutoTestShell::init({QStringLiteral("kdevclangsupport")}); auto core = TestCore::initialize(); delete core->projectController(); m_projectController = new TestProjectController(core); core->setProjectController(m_projectController); } void TestDUChain::cleanupTestCase() { TestCore::shutdown(); } void TestDUChain::cleanup() { if (m_provider) { IDefinesAndIncludesManager::manager()->unregisterBackgroundProvider(m_provider.data()); } } void TestDUChain::init() { m_provider.reset(new TestEnvironmentProvider); IDefinesAndIncludesManager::manager()->registerBackgroundProvider(m_provider.data()); } struct ExpectedComment { QString identifier; QString comment; }; Q_DECLARE_METATYPE(ExpectedComment) Q_DECLARE_METATYPE(AbstractType::WhichType) void TestDUChain::testComments() { QFETCH(QString, code); QFETCH(ExpectedComment, expectedComment); TestFile file(code, QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); auto candidates = top->findDeclarations(QualifiedIdentifier(expectedComment.identifier)); QVERIFY(!candidates.isEmpty()); auto decl = candidates.first(); QString comment = QString::fromLocal8Bit(decl->comment()); const auto plainText = KDevelop::htmlToPlainText(comment, KDevelop::CompleteMode); // if comment is e.g. "("code"); QTest::addColumn("expectedComment"); // note: Clang only retrieves the comments when in doxygen-style format (i.e. '///', '/**', '///<') QTest::newRow("invalid1") << "//this is foo\nint foo;" << ExpectedComment{"foo", QString()}; QTest::newRow("invalid2") << "/*this is foo*/\nint foo;" << ExpectedComment{"foo", QString()}; QTest::newRow("basic1") << "///this is foo\nint foo;" << ExpectedComment{"foo", "this is foo"}; QTest::newRow("basic2") << "/**this is foo*/\nint foo;" << ExpectedComment{"foo", "this is foo"}; // as long as https://bugs.llvm.org/show_bug.cgi?id=35333 is not fixed, we don't fully parse and render // doxygen-style comments properly (cf. `makeComment` in builder.cpp) #define PARSE_COMMENTS 0 QTest::newRow("enumerator") << "enum Foo { bar1, ///localDeclarations().size(), 2); auto decl = file.topContext()->localDeclarations()[1]; QVERIFY(decl); auto function = dynamic_cast(decl); QVERIFY(function); auto functionType = function->type(); QVERIFY(functionType); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("namespace", "The ElaboratedType is not exposed through the libclang interface, not much we can do here", Abort); #endif QVERIFY(functionType->returnType()->whichType() != AbstractType::TypeDelayed); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("typedef", "After using clang_getCanonicalType on ElaboratedType all typedef information get's stripped away", Continue); #endif QCOMPARE(functionType->returnType()->whichType(), type); } void TestDUChain::testElaboratedType_data() { QTest::addColumn("code"); QTest::addColumn("type"); QTest::newRow("namespace") << "namespace NS{struct Type{};} struct NS::Type foo();" << AbstractType::TypeStructure; QTest::newRow("enum") << "enum Enum{}; enum Enum foo();" << AbstractType::TypeEnumeration; QTest::newRow("typedef") << "namespace NS{typedef int type;} NS::type foo();" << AbstractType::TypeAlias; } void TestDUChain::testInclude() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); // NOTE: header is _not_ explicitly being parsed, instead the impl job does that TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::AllDeclarationsContextsAndUses); auto implCtx = impl.topContext(); QVERIFY(implCtx); DUChainReadLocker lock; QCOMPARE(implCtx->localDeclarations().size(), 1); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(!headerCtx->parsingEnvironmentFile()->needsUpdate()); QCOMPARE(headerCtx->localDeclarations().size(), 1); QVERIFY(implCtx->imports(headerCtx, CursorInRevision(0, 10))); Declaration* foo = headerCtx->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); QCOMPARE(foo->uses().begin().key(), impl.url()); QCOMPARE(foo->uses().begin()->size(), 1); QCOMPARE(foo->uses().begin()->first(), RangeInRevision(1, 20, 1, 23)); } void TestDUChain::testMissingInclude() { auto code = R"( #pragma once #include "missing1.h" template class A { T a; }; #include "missing2.h" class B : public A { }; )"; TestFile header(code, QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n", QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::AllDeclarationsContextsAndUses)); DUChainReadLocker lock; auto top = impl.topContext(); QVERIFY(top); QCOMPARE(top->importedParentContexts().count(), 1); TopDUContext* headerCtx = dynamic_cast(top->importedParentContexts().first().context(top)); QVERIFY(headerCtx); QCOMPARE(headerCtx->url(), header.url()); #if CINDEX_VERSION_MINOR < 34 QEXPECT_FAIL("", "Second missing header isn't reported", Continue); #endif QCOMPARE(headerCtx->problems().count(), 2); QCOMPARE(headerCtx->localDeclarations().count(), 2); auto a = dynamic_cast(headerCtx->localDeclarations().first()); QVERIFY(a); auto b = dynamic_cast(headerCtx->localDeclarations().last()); QVERIFY(b); // NOTE: This fails and needs fixing. If the include of "missing2.h" // above is commented out, then it doesn't fail. Maybe // clang stops processing when it encounters the second missing // header, or similar. // XFAIL this check until https://bugs.llvm.org/show_bug.cgi?id=38155 is fixed QEXPECT_FAIL("", "Base class isn't assigned correctly", Continue); QCOMPARE(b->baseClassesSize(), 1u); #if CINDEX_VERSION_MINOR < 34 // at least the one problem we have should have been propagated QCOMPARE(top->problems().count(), 1); #else // two errors: // /tmp/testfile_f32415.h:3:10: error: 'missing1.h' file not found // /tmp/testfile_f32415.h:11:10: error: 'missing2.h' file not found QCOMPARE(top->problems().count(), 2); #endif } QByteArray createCode(const QByteArray& prefix, const int functions) { QByteArray code; code += "#ifndef " + prefix + "_H\n"; code += "#define " + prefix + "_H\n"; for (int i = 0; i < functions; ++i) { code += "void myFunc_" + prefix + "(int arg1, char arg2, const char* arg3);\n"; } code += "#endif\n"; return code; } void TestDUChain::testIncludeLocking() { TestFile header1(createCode("Header1", 1000), QStringLiteral("h")); TestFile header2(createCode("Header2", 1000), QStringLiteral("h")); TestFile header3(createCode("Header3", 1000), QStringLiteral("h")); ICore::self()->languageController()->backgroundParser()->setThreadCount(3); TestFile impl1("#include \"" + header1.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "#include \"" + header3.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); TestFile impl2("#include \"" + header2.url().str() + "\"\n" "#include \"" + header1.url().str() + "\"\n" "#include \"" + header3.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); TestFile impl3("#include \"" + header3.url().str() + "\"\n" "#include \"" + header1.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "int main() { return 0; }", QStringLiteral("cpp")); impl1.parse(TopDUContext::AllDeclarationsContextsAndUses); impl2.parse(TopDUContext::AllDeclarationsContextsAndUses); impl3.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(impl1.waitForParsed(5000)); QVERIFY(impl2.waitForParsed(5000)); QVERIFY(impl3.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(DUChain::self()->chainForDocument(header1.url())); QVERIFY(DUChain::self()->chainForDocument(header2.url())); QVERIFY(DUChain::self()->chainForDocument(header3.url())); } void TestDUChain::testReparse() { TestFile file(QStringLiteral("int main() { int i = 42; return i; }"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); DeclarationPointer mainDecl; DeclarationPointer iDecl; for (int i = 0; i < 3; ++i) { QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 1); DUContext *exprContext = file.topContext()->childContexts().first()->childContexts().first(); QCOMPARE(exprContext->localDeclarations().size(), 1); if (i) { QVERIFY(mainDecl); QCOMPARE(mainDecl.data(), file.topContext()->localDeclarations().first()); QVERIFY(iDecl); QCOMPARE(iDecl.data(), exprContext->localDeclarations().first()); } mainDecl = file.topContext()->localDeclarations().first(); iDecl = exprContext->localDeclarations().first(); QVERIFY(mainDecl->uses().isEmpty()); QCOMPARE(iDecl->uses().size(), 1); QCOMPARE(iDecl->uses().begin()->size(), 1); if (i == 1) { file.setFileContents(QStringLiteral("int main()\n{\nfloat i = 13; return i - 5;\n}\n")); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testReparseError() { TestFile file(QStringLiteral("int i = 1 / 0;\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; QVERIFY(file.topContext()); if (!i) { QCOMPARE(file.topContext()->problems().size(), 1); file.setFileContents(QStringLiteral("int i = 0;\n")); } else { QCOMPARE(file.topContext()->problems().size(), 0); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testTemplate() { TestFile file("template struct foo { T bar; };\n" "int main() { foo myFoo; return myFoo.bar; }\n", QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto fooDecl = file.topContext()->localDeclarations().first(); QVERIFY(fooDecl->internalContext()); QCOMPARE(fooDecl->internalContext()->localDeclarations().size(), 2); QCOMPARE(file.topContext()->findDeclarations(QualifiedIdentifier("foo< T >")).size(), 1); QCOMPARE(file.topContext()->findDeclarations(QualifiedIdentifier("foo< T >::bar")).size(), 1); auto mainCtx = file.topContext()->localDeclarations().last()->internalContext()->childContexts().first(); QVERIFY(mainCtx); auto myFoo = mainCtx->localDeclarations().first(); QVERIFY(myFoo); QCOMPARE(myFoo->abstractType()->toString().remove(' '), QStringLiteral("foo")); } void TestDUChain::testNamespace() { TestFile file("namespace foo { struct bar { int baz; }; }\n" "int main() { foo::bar myBar; }\n", QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto fooDecl = file.topContext()->localDeclarations().first(); QVERIFY(fooDecl->internalContext()); QCOMPARE(fooDecl->internalContext()->localDeclarations().size(), 1); DUContext* top = file.topContext().data(); DUContext* mainCtx = file.topContext()->childContexts().last(); auto foo = top->localDeclarations().first(); QCOMPARE(foo->qualifiedIdentifier().toString(), QString("foo")); DUContext* fooCtx = file.topContext()->childContexts().first(); QCOMPARE(fooCtx->localScopeIdentifier().toString(), QString("foo")); QCOMPARE(fooCtx->scopeIdentifier(true).toString(), QString("foo")); QCOMPARE(fooCtx->localDeclarations().size(), 1); auto bar = fooCtx->localDeclarations().first(); QCOMPARE(bar->qualifiedIdentifier().toString(), QString("foo::bar")); QCOMPARE(fooCtx->childContexts().size(), 1); DUContext* barCtx = fooCtx->childContexts().first(); QCOMPARE(barCtx->localScopeIdentifier().toString(), QString("bar")); QCOMPARE(barCtx->scopeIdentifier(true).toString(), QString("foo::bar")); QCOMPARE(barCtx->localDeclarations().size(), 1); auto baz = barCtx->localDeclarations().first(); QCOMPARE(baz->qualifiedIdentifier().toString(), QString("foo::bar::baz")); for (auto ctx : {top, mainCtx}) { QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo")).size(), 1); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo::bar")).size(), 1); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo::bar::baz")).size(), 1); } } void TestDUChain::testAutoTypeDeduction() { TestFile file(QStringLiteral(R"( const volatile auto foo = 5; template struct myTemplate {}; myTemplate& > templRefParam; auto autoTemplRefParam = templRefParam; )"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* ctx = file.topContext().data(); QVERIFY(ctx); QCOMPARE(ctx->localDeclarations().size(), 4); QCOMPARE(ctx->findDeclarations(QualifiedIdentifier("foo")).size(), 1); Declaration* decl = ctx->findDeclarations(QualifiedIdentifier(QStringLiteral("foo")))[0]; QCOMPARE(decl->identifier(), Identifier("foo")); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "No type deduction here unfortunately, missing API in Clang", Continue); #endif QVERIFY(decl->type()); #if CINDEX_VERSION_MINOR < 31 QCOMPARE(decl->toString(), QStringLiteral("const volatile auto foo")); #else QCOMPARE(decl->toString(), QStringLiteral("const volatile int foo")); #endif decl = ctx->findDeclarations(QualifiedIdentifier(QStringLiteral("autoTemplRefParam")))[0]; QVERIFY(decl); QVERIFY(decl->abstractType()); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "Auto type is not exposed via LibClang", Continue); #endif QCOMPARE(decl->abstractType()->toString(), QStringLiteral("myTemplate< myTemplate< int >& >")); } void TestDUChain::testTypeDeductionInTemplateInstantiation() { // see: http://clang-developers.42468.n3.nabble.com/RFC-missing-libclang-query-functions-features-td2504253.html TestFile file(QStringLiteral("template struct foo { T member; } foo f; auto i = f.member;"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* ctx = file.topContext().data(); QVERIFY(ctx); QCOMPARE(ctx->localDeclarations().size(), 3); Declaration* decl = nullptr; // check 'foo' declaration decl = ctx->localDeclarations()[0]; QVERIFY(decl); QCOMPARE(decl->identifier(), Identifier("foo< T >")); // check type of 'member' inside declaration-scope QCOMPARE(ctx->childContexts().size(), 1); DUContext* fooCtx = ctx->childContexts().first(); QVERIFY(fooCtx); // Should there really be two declarations? QCOMPARE(fooCtx->localDeclarations().size(), 2); decl = fooCtx->localDeclarations()[1]; QCOMPARE(decl->identifier(), Identifier("member")); // check type of 'member' in definition of 'f' decl = ctx->localDeclarations()[1]; QCOMPARE(decl->identifier(), Identifier("f")); decl = ctx->localDeclarations()[2]; QCOMPARE(decl->identifier(), Identifier("i")); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "No type deduction here unfortunately, missing API in Clang", Continue); #endif QVERIFY(decl->type()); } void TestDUChain::testVirtualMemberFunction() { //Forward-declarations with "struct" or "class" are considered equal, so make sure the override is detected correctly. TestFile file(QStringLiteral("struct S {}; struct A { virtual S* ret(); }; struct B : public A { virtual S* ret(); };"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* top = file.topContext().data(); QVERIFY(top); QCOMPARE(top->childContexts().count(), 3); QCOMPARE(top->localDeclarations().count(), 3); QCOMPARE(top->childContexts()[2]->localDeclarations().count(), 1); Declaration* decl = top->childContexts()[2]->localDeclarations()[0]; QCOMPARE(decl->identifier(), Identifier("ret")); QVERIFY(DUChainUtils::getOverridden(decl)); } void TestDUChain::testBaseClasses() { TestFile file(QStringLiteral("class Base {}; class Inherited : public Base {};"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; DUContext* top = file.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().count(), 2); Declaration* baseDecl = top->localDeclarations().first(); QCOMPARE(baseDecl->identifier(), Identifier("Base")); ClassDeclaration* inheritedDecl = dynamic_cast(top->localDeclarations()[1]); QCOMPARE(inheritedDecl->identifier(), Identifier("Inherited")); QVERIFY(inheritedDecl); QCOMPARE(inheritedDecl->baseClassesSize(), 1u); QCOMPARE(baseDecl->uses().count(), 1); QCOMPARE(baseDecl->uses().first().count(), 1); QCOMPARE(baseDecl->uses().first().first(), RangeInRevision(0, 40, 0, 44)); } void TestDUChain::testReparseBaseClasses() { TestFile file(QStringLiteral("struct a{}; struct b : a {};\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { qDebug() << "run: " << i; QVERIFY(file.waitForParsed(500)); DUChainWriteLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->childContexts().first()->importers().size(), 1); QCOMPARE(file.topContext()->childContexts().last()->importedParentContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto aDecl = dynamic_cast(file.topContext()->localDeclarations().first()); QVERIFY(aDecl); QCOMPARE(aDecl->baseClassesSize(), 0u); auto bDecl = dynamic_cast(file.topContext()->localDeclarations().last()); QVERIFY(bDecl); QCOMPARE(bDecl->baseClassesSize(), 1u); int distance = 0; QVERIFY(bDecl->isPublicBaseClass(aDecl, file.topContext(), &distance)); QCOMPARE(distance, 1); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testReparseBaseClassesTemplates() { TestFile file(QStringLiteral("template struct a{}; struct b : a {};\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); for (int i = 0; i < 2; ++i) { qDebug() << "run: " << i; QVERIFY(file.waitForParsed(500)); DUChainWriteLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->childContexts().first()->importers().size(), 1); QCOMPARE(file.topContext()->childContexts().last()->importedParentContexts().size(), 1); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto aDecl = dynamic_cast(file.topContext()->localDeclarations().first()); QVERIFY(aDecl); QCOMPARE(aDecl->baseClassesSize(), 0u); auto bDecl = dynamic_cast(file.topContext()->localDeclarations().last()); QVERIFY(bDecl); QCOMPARE(bDecl->baseClassesSize(), 1u); int distance = 0; QVERIFY(bDecl->isPublicBaseClass(aDecl, file.topContext(), &distance)); QCOMPARE(distance, 1); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); } } void TestDUChain::testGetInheriters_data() { QTest::addColumn("code"); QTest::newRow("inline") << "struct Base { struct Inner {}; }; struct Inherited : Base, Base::Inner {};"; QTest::newRow("outline") << "struct Base { struct Inner; }; struct Base::Inner {}; struct Inherited : Base, Base::Inner {};"; } void TestDUChain::testGetInheriters() { QFETCH(QString, code); TestFile file(code, QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(top->problems().isEmpty()); QCOMPARE(top->localDeclarations().count(), 2); Declaration* baseDecl = top->localDeclarations().first(); QCOMPARE(baseDecl->identifier(), Identifier("Base")); DUContext* baseCtx = baseDecl->internalContext(); QVERIFY(baseCtx); QCOMPARE(baseCtx->localDeclarations().count(), 1); Declaration* innerDecl = baseCtx->localDeclarations().first(); QCOMPARE(innerDecl->identifier(), Identifier("Inner")); if (auto forward = dynamic_cast(innerDecl)) { innerDecl = forward->resolve(top); } QVERIFY(dynamic_cast(innerDecl)); Declaration* inheritedDecl = top->localDeclarations().last(); QVERIFY(inheritedDecl); QCOMPARE(inheritedDecl->identifier(), Identifier("Inherited")); uint maxAllowedSteps = uint(-1); auto baseInheriters = DUChainUtils::getInheriters(baseDecl, maxAllowedSteps); QCOMPARE(baseInheriters, QList() << inheritedDecl); maxAllowedSteps = uint(-1); auto innerInheriters = DUChainUtils::getInheriters(innerDecl, maxAllowedSteps); QCOMPARE(innerInheriters, QList() << inheritedDecl); maxAllowedSteps = uint(-1); auto inheritedInheriters = DUChainUtils::getInheriters(inheritedDecl, maxAllowedSteps); QCOMPARE(inheritedInheriters.count(), 0); } void TestDUChain::testGlobalFunctionDeclaration() { TestFile file(QStringLiteral("void foo(int arg1, char arg2);\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); file.waitForParsed(); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); QCOMPARE(file.topContext()->childContexts().size(), 1); QVERIFY(!file.topContext()->childContexts().first()->inSymbolTable()); } void TestDUChain::testFunctionDefinitionVsDeclaration() { TestFile file(QStringLiteral("void func(); void func() {}\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto funcDecl = file.topContext()->localDeclarations()[0]; QVERIFY(!funcDecl->isDefinition()); QVERIFY(!dynamic_cast(funcDecl)); auto funcDef = file.topContext()->localDeclarations()[1]; QVERIFY(dynamic_cast(funcDef)); QVERIFY(funcDef->isDefinition()); } void TestDUChain::testEnsureNoDoubleVisit() { // On some language construct, we may up visiting the same cursor multiple times // Example: "struct SomeStruct {} s;" // decl: "SomeStruct SomeStruct " of kind StructDecl (2) in main.cpp@[(1,1),(1,17)] // decl: "struct SomeStruct s " of kind VarDecl (9) in main.cpp@[(1,1),(1,19)] // decl: "SomeStruct SomeStruct " of kind StructDecl (2) in main.cpp@[(1,1),(1,17)] // // => We end up visiting the StructDecl twice (or more) // That's because we use clang_visitChildren not just on the translation unit cursor. // Apparently just "recursing" vs. "visiting children explicitly" // results in a different AST traversal TestFile file(QStringLiteral("struct SomeStruct {} s;\n"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); // there should only be one declaration for "SomeStruct" auto candidates = top->findDeclarations(QualifiedIdentifier(QStringLiteral("SomeStruct"))); QCOMPARE(candidates.size(), 1); } void TestDUChain::testParsingEnvironment() { const TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses; IndexedTopDUContext indexed; ClangParsingEnvironment lastEnv; { TestFile file(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); auto astFeatures = static_cast(features | TopDUContext::AST); file.parse(astFeatures); file.setKeepDUChainData(true); QVERIFY(file.waitForParsed()); DUChainWriteLocker lock; auto top = file.topContext(); QVERIFY(top); auto sessionData = ParseSessionData::Ptr(dynamic_cast(top->ast().data())); lock.unlock(); ParseSession session(sessionData); lock.lock(); QVERIFY(session.data()); QVERIFY(top); auto envFile = QExplicitlySharedDataPointer( dynamic_cast(file.topContext()->parsingEnvironmentFile().data())); QCOMPARE(envFile->features(), astFeatures); QVERIFY(envFile->featuresSatisfied(astFeatures)); QCOMPARE(envFile->environmentQuality(), ClangParsingEnvironment::Source); // if no environment is given, no update should be triggered QVERIFY(!envFile->needsUpdate()); // same env should also not trigger a reparse ClangParsingEnvironment env = session.environment(); QCOMPARE(env.quality(), ClangParsingEnvironment::Source); QVERIFY(!envFile->needsUpdate(&env)); // but changing the environment should trigger an update env.addIncludes(Path::List() << Path(QStringLiteral("/foo/bar/baz"))); QVERIFY(envFile->needsUpdate(&env)); envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); // setting the environment quality higher should require an update env.setQuality(ClangParsingEnvironment::BuildSystem); QVERIFY(envFile->needsUpdate(&env)); envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); // changing defines requires an update env.addDefines(QHash{ { "foo", "bar" } }); QVERIFY(envFile->needsUpdate(&env)); // but only when changing the defines for the envFile's TU const auto barTU = IndexedString("bar.cpp"); const auto oldTU = env.translationUnitUrl(); env.setTranslationUnitUrl(barTU); QCOMPARE(env.translationUnitUrl(), barTU); QVERIFY(!envFile->needsUpdate(&env)); env.setTranslationUnitUrl(oldTU); QVERIFY(envFile->needsUpdate(&env)); // update it again envFile->setEnvironment(env); QVERIFY(!envFile->needsUpdate(&env)); lastEnv = env; // now compare against a lower quality environment // in such a case, we do not want to trigger an update env.setQuality(ClangParsingEnvironment::Unknown); env.setTranslationUnitUrl(barTU); QVERIFY(!envFile->needsUpdate(&env)); // even when the environment changes env.addIncludes(Path::List() << Path(QStringLiteral("/lalalala"))); QVERIFY(!envFile->needsUpdate(&env)); indexed = top->indexed(); } DUChain::self()->storeToDisk(); { DUChainWriteLocker lock; QVERIFY(!DUChain::self()->isInMemory(indexed.index())); QVERIFY(indexed.data()); QVERIFY(DUChain::self()->environmentFileForDocument(indexed)); auto envFile = QExplicitlySharedDataPointer( dynamic_cast(DUChain::self()->environmentFileForDocument(indexed).data())); QVERIFY(envFile); QCOMPARE(envFile->features(), features); QVERIFY(envFile->featuresSatisfied(features)); QVERIFY(!envFile->needsUpdate(&lastEnv)); DUChain::self()->removeDocumentChain(indexed.data()); } } void TestDUChain::testActiveDocumentHasASTAttached() { const TopDUContext::Features features = TopDUContext::AllDeclarationsContextsAndUses; IndexedTopDUContext indexed; ClangParsingEnvironment lastEnv; { TestFile file(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); auto astFeatures = static_cast(features | TopDUContext::AST); file.parse(astFeatures); file.setKeepDUChainData(true); QVERIFY(file.waitForParsed()); DUChainWriteLocker lock; auto top = file.topContext(); QVERIFY(top); auto sessionData = ParseSessionData::Ptr(dynamic_cast(top->ast().data())); lock.unlock(); ParseSession session(sessionData); lock.lock(); QVERIFY(session.data()); QVERIFY(top); QVERIFY(top->ast()); indexed = top->indexed(); } DUChain::self()->storeToDisk(); { DUChainWriteLocker lock; QVERIFY(!DUChain::self()->isInMemory(indexed.index())); QVERIFY(indexed.data()); } QUrl url; { DUChainReadLocker lock; auto ctx = indexed.data(); QVERIFY(ctx); QVERIFY(!ctx->ast()); url = ctx->url().toUrl(); } QVERIFY(!QFileInfo::exists(url.toLocalFile())); QFile file(url.toLocalFile()); file.open(QIODevice::WriteOnly); Q_ASSERT(file.isOpen()); auto document = ICore::self()->documentController()->openDocument(url); QVERIFY(document); ICore::self()->documentController()->activateDocument(document); QApplication::processEvents(); ICore::self()->languageController()->backgroundParser()->parseDocuments(); QThread::sleep(1); document->close(KDevelop::IDocument::Discard); { DUChainReadLocker lock; auto ctx = indexed.data(); QVERIFY(ctx); QVERIFY(ctx->ast()); } DUChainWriteLocker lock; DUChain::self()->removeDocumentChain(indexed.data()); } void TestDUChain::testActiveDocumentsGetBestPriority() { // note: this test would make more sense in kdevplatform, but we don't have a language plugin available there // (required for background parsing) // TODO: Create a fake-language plugin in kdevplatform for testing purposes, use that. TestFile file1(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); TestFile file2(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); TestFile file3(QStringLiteral("int main() {}\n"), QStringLiteral("cpp")); DUChain::self()->storeToDisk(); auto backgroundParser = ICore::self()->languageController()->backgroundParser(); QVERIFY(!backgroundParser->isQueued(file1.url())); auto documentController = ICore::self()->documentController(); // open first document (no activation) auto doc = documentController->openDocument(file1.url().toUrl(), KTextEditor::Range::invalid(), {IDocumentController::DoNotActivate}); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file1.url())); QCOMPARE(backgroundParser->priorityForDocument(file1.url()), (int)BackgroundParser::NormalPriority); // open second document, activate doc = documentController->openDocument(file2.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file2.url())); QCOMPARE(backgroundParser->priorityForDocument(file2.url()), (int)BackgroundParser::BestPriority); // open third document, activate, too doc = documentController->openDocument(file3.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file3.url())); QCOMPARE(backgroundParser->priorityForDocument(file3.url()), (int)BackgroundParser::BestPriority); } void TestDUChain::testSystemIncludes() { ClangParsingEnvironment env; Path::List projectIncludes = { Path("/projects/1"), Path("/projects/1/sub"), Path("/projects/2"), Path("/projects/2/sub") }; env.addIncludes(projectIncludes); auto includes = env.includes(); // no project paths set, so everything is considered a system include QCOMPARE(includes.system, projectIncludes); QVERIFY(includes.project.isEmpty()); Path::List systemIncludes = { Path("/sys"), Path("/sys/sub") }; env.addIncludes(systemIncludes); includes = env.includes(); QCOMPARE(includes.system, projectIncludes + systemIncludes); QVERIFY(includes.project.isEmpty()); Path::List projects = { Path("/projects/1"), Path("/projects/2") }; env.setProjectPaths(projects); // now the list should be properly separated QCOMPARE(env.projectPaths(), projects); includes = env.includes(); QCOMPARE(includes.system, systemIncludes); QCOMPARE(includes.project, projectIncludes); } void TestDUChain::testReparseWithAllDeclarationsContextsAndUses() { TestFile file(QStringLiteral("int foo() { return 0; } int main() { return foo(); }"), QStringLiteral("cpp")); file.parse(TopDUContext::VisibleDeclarationsAndContexts); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto dec = file.topContext()->localDeclarations().at(0); QEXPECT_FAIL("", "Skipping of function bodies is disabled for now", Continue); QVERIFY(dec->uses().isEmpty()); } file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(500)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->childContexts().size(), 2); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto mainDecl = file.topContext()->localDeclarations()[1]; QVERIFY(mainDecl->uses().isEmpty()); auto foo = file.topContext()->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); } } void TestDUChain::testReparseOnDocumentActivated() { TestFile file(QStringLiteral("int foo() { return 0; } int main() { return foo(); }"), QStringLiteral("cpp")); file.parse(TopDUContext::VisibleDeclarationsAndContexts); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; auto ctx = file.topContext(); QVERIFY(ctx); QCOMPARE(ctx->childContexts().size(), 2); QCOMPARE(ctx->localDeclarations().size(), 2); auto dec = ctx->localDeclarations().at(0); QEXPECT_FAIL("", "Skipping of function bodies was disabled for now", Continue); QVERIFY(dec->uses().isEmpty()); QVERIFY(!ctx->ast()); } auto backgroundParser = ICore::self()->languageController()->backgroundParser(); QVERIFY(!backgroundParser->isQueued(file.url())); auto doc = ICore::self()->documentController()->openDocument(file.url().toUrl()); QVERIFY(doc); QVERIFY(backgroundParser->isQueued(file.url())); QSignalSpy spy(backgroundParser, &BackgroundParser::parseJobFinished); spy.wait(); doc->close(KDevelop::IDocument::Discard); { DUChainReadLocker lock; auto ctx = file.topContext(); QCOMPARE(ctx->features() & TopDUContext::AllDeclarationsContextsAndUses, static_cast(TopDUContext::AllDeclarationsContextsAndUses)); QVERIFY(ctx->topContext()->ast()); } } void TestDUChain::testReparseInclude() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); // Use TopDUContext::AST to imitate that document is opened in the editor, so that ClangParseJob can store translation unit, that'll be used for reparsing. impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsAndContexts|TopDUContext::AST)); QVERIFY(impl.waitForParsed(5000)); { DUChainReadLocker lock; auto implCtx = impl.topContext(); QVERIFY(implCtx); QCOMPARE(implCtx->importedParentContexts().size(), 1); } impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(impl.waitForParsed(5000)); DUChainReadLocker lock; auto implCtx = impl.topContext(); QVERIFY(implCtx); QCOMPARE(implCtx->localDeclarations().size(), 1); QCOMPARE(implCtx->importedParentContexts().size(), 1); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(!headerCtx->parsingEnvironmentFile()->needsUpdate()); QCOMPARE(headerCtx->localDeclarations().size(), 1); QVERIFY(implCtx->imports(headerCtx, CursorInRevision(0, 10))); Declaration* foo = headerCtx->localDeclarations().first(); QCOMPARE(foo->uses().size(), 1); QCOMPARE(foo->uses().begin().key(), impl.url()); QCOMPARE(foo->uses().begin()->size(), 1); QCOMPARE(foo->uses().begin()->first(), RangeInRevision(1, 20, 1, 23)); QCOMPARE(DUChain::self()->allEnvironmentFiles(header.url()).size(), 1); QCOMPARE(DUChain::self()->allEnvironmentFiles(impl.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(header.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(impl.url()).size(), 1); } void TestDUChain::testReparseChangeEnvironment() { TestFile header(QStringLiteral("int foo() { return 42; }\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "int main() { return foo(); }", QStringLiteral("cpp"), &header); uint hashes[3] = {0, 0, 0}; for (int i = 0; i < 3; ++i) { impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(impl.topContext()); auto env = dynamic_cast(impl.topContext()->parsingEnvironmentFile().data()); QVERIFY(env); QCOMPARE(env->environmentQuality(), ClangParsingEnvironment::Source); hashes[i] = env->environmentHash(); QVERIFY(hashes[i]); // we should never end up with multiple env files or chains in memory for these files QCOMPARE(DUChain::self()->allEnvironmentFiles(impl.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(impl.url()).size(), 1); QCOMPARE(DUChain::self()->allEnvironmentFiles(header.url()).size(), 1); QCOMPARE(DUChain::self()->chainsForDocument(header.url()).size(), 1); } // in every run, we expect the environment to have changed for (int j = 0; j < i; ++j) { QVERIFY(hashes[i] != hashes[j]); } if (i == 0) { // 1) change defines m_provider->defines.insert(QStringLiteral("foooooooo"), QStringLiteral("baaar!")); } else if (i == 1) { // 2) change includes m_provider->includes.append(Path(QStringLiteral("/foo/bar/asdf/lalala"))); } // 3) stop } } void TestDUChain::testMacroDependentHeader() { TestFile header(QStringLiteral("struct MY_CLASS { class Q{Q(); int m;}; int m; };\n"), QStringLiteral("h")); TestFile impl("#define MY_CLASS A\n" "#include \"" + header.url().str() + "\"\n" "#undef MY_CLASS\n" "#define MY_CLASS B\n" "#include \"" + header.url().str() + "\"\n" "#undef MY_CLASS\n" "A a;\n" "const A::Q aq;\n" "B b;\n" "const B::Q bq;\n" "int am = a.m;\n" "int aqm = aq.m;\n" "int bm = b.m;\n" "int bqm = bq.m;\n" , QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 10); // 2x macro, then a, aq, b, bq QCOMPARE(top->importedParentContexts().size(), 1); AbstractType::Ptr type = top->localDeclarations()[2]->abstractType(); StructureType* sType = dynamic_cast(type.data()); QVERIFY(sType); QCOMPARE(sType->toString(), QString("A")); Declaration* decl = sType->declaration(top); QVERIFY(decl); AbstractType::Ptr type2 = top->localDeclarations()[4]->abstractType(); StructureType* sType2 = dynamic_cast(type2.data()); QVERIFY(sType2); QCOMPARE(sType2->toString(), QString("B")); Declaration* decl2 = sType2->declaration(top); QVERIFY(decl2); TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[0].context(top)); QVERIFY(top2); QCOMPARE(top2->localDeclarations().size(), 2); QCOMPARE(top2->localDeclarations()[0], decl); QCOMPARE(top2->localDeclarations()[1], decl2); qDebug() << "DECL RANGE:" << top2->localDeclarations()[0]->range().castToSimpleRange(); qDebug() << "CTX RANGE:" << top2->localDeclarations()[0]->internalContext()->range().castToSimpleRange(); // validate uses: QCOMPARE(top->usesCount(), 14); QCOMPARE(top->uses()[0].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A")); QCOMPARE(top->uses()[1].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A")); QCOMPARE(top->uses()[2].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::Q")); QCOMPARE(top->uses()[3].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B")); QCOMPARE(top->uses()[4].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B")); QCOMPARE(top->uses()[5].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::Q")); QCOMPARE(top->uses()[6].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("a")); QCOMPARE(top->uses()[7].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::m")); QCOMPARE(top->uses()[8].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("aq")); QCOMPARE(top->uses()[9].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("A::Q::m")); QCOMPARE(top->uses()[10].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("b")); QCOMPARE(top->uses()[11].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::m")); QCOMPARE(top->uses()[12].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("bq")); QCOMPARE(top->uses()[13].usedDeclaration(top)->qualifiedIdentifier(), QualifiedIdentifier("B::Q::m")); } void TestDUChain::testHeaderParsingOrder1() { TestFile header(QStringLiteral("typedef const A B;\n"), QStringLiteral("h")); TestFile impl("template class A{};\n" "#include \"" + header.url().str() + "\"\n" "B c;", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 2); QCOMPARE(top->importedParentContexts().size(), 1); AbstractType::Ptr type = top->localDeclarations()[1]->abstractType(); TypeAliasType* aType = dynamic_cast(type.data()); QVERIFY(aType); AbstractType::Ptr targetType = aType->type(); QVERIFY(targetType); IdentifiedType *idType = dynamic_cast(targetType.data()); QVERIFY(idType); // this declaration could be resolved, because it was created with an // indirect DeclarationId that is resolved from the perspective of 'top' Declaration* decl = idType->declaration(top); // NOTE: the decl. doesn't know (yet) about the template insantiation QVERIFY(decl); QCOMPARE(decl, top->localDeclarations()[0]); // now ensure that a use was build for 'A' in header1 TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[0].context(top)); QVERIFY(top2); QEXPECT_FAIL("", "the use could not be created because the corresponding declaration didn't exist yet", Continue); QCOMPARE(top2->usesCount(), 1); // Declaration* decl2 = top2->uses()[0].usedDeclaration(top2); // QVERIFY(decl2); // QCOMPARE(decl, decl2); } void TestDUChain::testHeaderParsingOrder2() { TestFile header(QStringLiteral("template class A{};\n"), QStringLiteral("h")); TestFile header2(QStringLiteral("typedef const A B;\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n" "#include \"" + header2.url().str() + "\"\n" "B c;", QStringLiteral("cpp"), &header); impl.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(impl.waitForParsed(500000)); DUChainReadLocker lock; TopDUContext* top = impl.topContext().data(); QVERIFY(top); QCOMPARE(top->localDeclarations().size(), 1); QCOMPARE(top->importedParentContexts().size(), 2); AbstractType::Ptr type = top->localDeclarations()[0]->abstractType(); TypeAliasType* aType = dynamic_cast(type.data()); QVERIFY(aType); AbstractType::Ptr targetType = aType->type(); QVERIFY(targetType); IdentifiedType *idType = dynamic_cast(targetType.data()); QVERIFY(idType); Declaration* decl = idType->declaration(top); // NOTE: the decl. doesn't know (yet) about the template insantiation QVERIFY(decl); // now ensure that a use was build for 'A' in header2 TopDUContext* top2 = dynamic_cast(top->importedParentContexts()[1].context(top)); QVERIFY(top2); QCOMPARE(top2->usesCount(), 1); Declaration* decl2 = top2->uses()[0].usedDeclaration(top2); QCOMPARE(decl, decl2); } void TestDUChain::testMacrosRanges() { TestFile file(QStringLiteral("#define FUNC_MACROS(x) struct str##x{};\nFUNC_MACROS(x);"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,19)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,11)); } void TestDUChain::testMacroUses() { TestFile file(QStringLiteral("#define USER(x) x\n#define USED\nUSER(USED)"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition1 = file.topContext()->localDeclarations()[0]; auto macroDefinition2 = file.topContext()->localDeclarations()[1]; QCOMPARE(macroDefinition1->uses().size(), 1); QCOMPARE(macroDefinition1->uses().begin()->first(), RangeInRevision(2,0,2,4)); #if CINDEX_VERSION_MINOR < 32 QEXPECT_FAIL("", "This appears to be a clang bug, the AST doesn't contain the macro use", Continue); #endif QCOMPARE(macroDefinition2->uses().size(), 1); if (macroDefinition2->uses().size()) { QCOMPARE(macroDefinition2->uses().begin()->first(), RangeInRevision(2,5,2,9)); } } void TestDUChain::testMultiLineMacroRanges() { TestFile file(QStringLiteral("#define FUNC_MACROS(x) struct str##x{};\nFUNC_MACROS(x\n);"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,19)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,11)); } void TestDUChain::testNestedMacroRanges() { TestFile file(QStringLiteral("#define INNER int var; var = 0;\n#define MACRO() INNER\nint main(){MACRO(\n);}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto main = file.topContext()->localDeclarations()[2]; QVERIFY(main); auto mainCtx = main->internalContext()->childContexts().first(); QVERIFY(mainCtx); QCOMPARE(mainCtx->localDeclarations().size(), 1); auto var = mainCtx->localDeclarations().first(); QVERIFY(var); QCOMPARE(var->range(), RangeInRevision(2,11,2,11)); QCOMPARE(var->uses().size(), 1); QCOMPARE(var->uses().begin()->first(), RangeInRevision(2,11,2,11)); } void TestDUChain::testNestedImports() { TestFile B(QStringLiteral("#pragma once\nint B();\n"), QStringLiteral("h")); TestFile C("#pragma once\n#include \"" + B.url().str() + "\"\nint C();\n", QStringLiteral("h")); TestFile A("#include \"" + B.url().str() + "\"\n" + "#include \"" + C.url().str() + "\"\nint A();\n", QStringLiteral("cpp")); A.parse(); QVERIFY(A.waitForParsed(5000)); DUChainReadLocker lock; auto BCtx = DUChain::self()->chainForDocument(B.url().toUrl()); QVERIFY(BCtx); QVERIFY(BCtx->importedParentContexts().isEmpty()); auto CCtx = DUChain::self()->chainForDocument(C.url().toUrl()); QVERIFY(CCtx); QCOMPARE(CCtx->importedParentContexts().size(), 1); QVERIFY(CCtx->imports(BCtx, CursorInRevision(1, 10))); auto ACtx = A.topContext(); QVERIFY(ACtx); QCOMPARE(ACtx->importedParentContexts().size(), 2); QVERIFY(ACtx->imports(BCtx, CursorInRevision(0, 10))); QVERIFY(ACtx->imports(CCtx, CursorInRevision(1, 10))); } void TestDUChain::testEnvironmentWithDifferentOrderOfElements() { TestFile file(QStringLiteral("int main();\n"), QStringLiteral("cpp")); m_provider->includes.clear(); m_provider->includes.append(Path(QStringLiteral("/path1"))); m_provider->includes.append(Path(QStringLiteral("/path2"))); m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); uint previousHash = 0; for (int i: {0, 1, 2, 3}) { file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); auto env = dynamic_cast(file.topContext()->parsingEnvironmentFile().data()); QVERIFY(env); QCOMPARE(env->environmentQuality(), ClangParsingEnvironment::Source); if (previousHash) { if (i == 3) { QVERIFY(previousHash != env->environmentHash()); } else { QCOMPARE(previousHash, env->environmentHash()); } } previousHash = env->environmentHash(); QVERIFY(previousHash); } if (i == 0) { //Change order of defines. Hash of the environment should stay the same. m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); } else if (i == 1) { //Add the same macros twice. Hash of the environment should stay the same. m_provider->defines.clear(); m_provider->defines.insert(QStringLiteral("key2"), QStringLiteral("value2")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key3"), QStringLiteral("value3")); m_provider->defines.insert(QStringLiteral("key1"), QStringLiteral("value1")); } else if (i == 2) { //OTOH order of includes should change hash of the environment. m_provider->includes.clear(); m_provider->includes.append(Path(QStringLiteral("/path2"))); m_provider->includes.append(Path(QStringLiteral("/path1"))); } } } void TestDUChain::testReparseMacro() { TestFile file(QStringLiteral("#define DECLARE(a) typedef struct a##_ {} *a;\nDECLARE(D);\nD d;"), QStringLiteral("cpp")); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 5); auto macroDefinition = file.topContext()->localDeclarations()[0]; QVERIFY(macroDefinition); QCOMPARE(macroDefinition->range(), RangeInRevision(0,8,0,15)); QCOMPARE(macroDefinition->uses().size(), 1); QCOMPARE(macroDefinition->uses().begin()->first(), RangeInRevision(1,0,1,7)); auto structDeclaration = file.topContext()->localDeclarations()[1]; QVERIFY(structDeclaration); QCOMPARE(structDeclaration->range(), RangeInRevision(1,0,1,0)); auto structTypedef = file.topContext()->localDeclarations()[3]; QVERIFY(structTypedef); QCOMPARE(structTypedef->range(), RangeInRevision(1,8,1,9)); QCOMPARE(structTypedef->uses().size(), 1); QCOMPARE(structTypedef->uses().begin()->first(), RangeInRevision(2,0,2,1)); } void TestDUChain::testGotoStatement() { TestFile file(QStringLiteral("int main() {\ngoto label;\ngoto label;\nlabel: return 0;}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); auto main = file.topContext()->localDeclarations()[0]; QVERIFY(main); auto mainCtx = main->internalContext()->childContexts().first(); QVERIFY(mainCtx); QCOMPARE(mainCtx->localDeclarations().size(), 1); auto label = mainCtx->localDeclarations().first(); QVERIFY(label); QCOMPARE(label->range(), RangeInRevision(3,0,3,5)); QCOMPARE(label->uses().size(), 1); QCOMPARE(label->uses().begin()->first(), RangeInRevision(1,5,1,10)); QCOMPARE(label->uses().begin()->last(), RangeInRevision(2,5,2,10)); } void TestDUChain::testRangesOfOperatorsInsideMacro() { TestFile file(QStringLiteral("class Test{public: Test& operator++(int);};\n#define MACRO(var) var++;\nint main(){\nTest tst; MACRO(tst)}"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto testClass = file.topContext()->localDeclarations()[0]; QVERIFY(testClass); auto operatorPlusPlus = testClass->internalContext()->localDeclarations().first(); QVERIFY(operatorPlusPlus); QCOMPARE(operatorPlusPlus->uses().size(), 1); QCOMPARE(operatorPlusPlus->uses().begin()->first(), RangeInRevision(3,10,3,10)); } void TestDUChain::testUsesCreatedForDeclarations() { auto code = R"(template void functionTemplate(T); template void functionTemplate(U) {} namespace NS { class Class{}; } using NS::Class; Class function(); NS::Class function() { return {}; } int main () { functionTemplate(int()); function(); } )"; TestFile file(code, QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; QVERIFY(file.topContext()); auto functionTemplate = file.topContext()->findDeclarations(QualifiedIdentifier(QStringLiteral("functionTemplate"))); QVERIFY(!functionTemplate.isEmpty()); auto functionTemplateDeclaration = DUChainUtils::declarationForDefinition(functionTemplate.first()); QVERIFY(!functionTemplateDeclaration->isDefinition()); #if CINDEX_VERSION_MINOR < 29 QEXPECT_FAIL("", "No API in LibClang to determine function template type", Continue); #endif QCOMPARE(functionTemplateDeclaration->uses().count(), 1); auto function = file.topContext()->findDeclarations(QualifiedIdentifier(QStringLiteral("function"))); QVERIFY(!function.isEmpty()); auto functionDeclaration = DUChainUtils::declarationForDefinition(function.first()); QVERIFY(!functionDeclaration->isDefinition()); QCOMPARE(functionDeclaration->uses().count(), 1); } void TestDUChain::testReparseIncludeGuard() { TestFile header(QStringLiteral("#ifndef GUARD\n#define GUARD\nint something;\n#endif\n"), QStringLiteral("h")); TestFile impl("#include \"" + header.url().str() + "\"\n", QStringLiteral("cpp"), &header); QVERIFY(impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST ))); { DUChainReadLocker lock; QCOMPARE(static_cast(impl.topContext()-> importedParentContexts().first().context(impl.topContext()))->problems().size(), 0); } QVERIFY(impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive))); { DUChainReadLocker lock; QCOMPARE(static_cast(impl.topContext()-> importedParentContexts().first().context(impl.topContext()))->problems().size(), 0); } } void TestDUChain::testExternC() { auto code = R"(extern "C" { void foo(); })"; TestFile file(code, QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed()); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(!top->findDeclarations(QualifiedIdentifier("foo")).isEmpty()); } void TestDUChain::testReparseUnchanged_data() { QTest::addColumn("headerCode"); QTest::addColumn("implCode"); QTest::newRow("include-guards") << R"( #ifndef GUARD #define GUARD int something; #endif )" << R"( #include "%1" )"; QTest::newRow("template-default-parameters") << R"( #ifndef TEST_H #define TEST_H template class dummy; template class dummy { int field[T]; }; #endif )" << R"( #include "%1" int main(int, char **) { dummy<> x; (void)x; } )"; } void TestDUChain::testReparseUnchanged() { QFETCH(QString, headerCode); QFETCH(QString, implCode); TestFile header(headerCode, QStringLiteral("h")); TestFile impl(implCode.arg(header.url().str()), QStringLiteral("cpp"), &header); auto checkProblems = [&] (bool reparsed) { DUChainReadLocker lock; auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); QVERIFY(headerCtx->problems().isEmpty()); auto implCtx = DUChain::self()->chainForDocument(impl.url()); QVERIFY(implCtx); if (reparsed && CINDEX_VERSION_MINOR > 29 && CINDEX_VERSION_MINOR < 33) { QEXPECT_FAIL("template-default-parameters", "the precompiled preamble messes the default template parameters up in clang 3.7", Continue); } QVERIFY(implCtx->problems().isEmpty()); }; impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST )); checkProblems(false); impl.parseAndWait(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdateRecursive)); checkProblems(true); } void TestDUChain::testTypeAliasTemplate() { TestFile file(QStringLiteral("template using Alias = T; using Foo = Alias;"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto templateAlias = file.topContext()->localDeclarations().first(); QVERIFY(templateAlias); #if CINDEX_VERSION_MINOR < 31 QEXPECT_FAIL("", "TypeAliasTemplate is not exposed via LibClang", Abort); #endif QVERIFY(templateAlias->isTypeAlias()); QVERIFY(templateAlias->abstractType()); QCOMPARE(templateAlias->abstractType()->toString(), QStringLiteral("Alias")); QCOMPARE(templateAlias->uses().size(), 1); QCOMPARE(templateAlias->uses().first().size(), 1); QCOMPARE(templateAlias->uses().first().first(), RangeInRevision(0, 51, 0, 56)); } void TestDUChain::testDeclarationsInsideMacroExpansion() { TestFile header(QStringLiteral("#define DECLARE(a) typedef struct a##__ {int var;} *a\nDECLARE(D);\n"), QStringLiteral("h")); TestFile file("#include \"" + header.url().str() + "\"\nint main(){\nD d; d->var;}\n", QStringLiteral("cpp")); file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST)); QVERIFY(file.waitForParsed(5000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); } file.parse(TopDUContext::Features(TopDUContext::AllDeclarationsContextsAndUses|TopDUContext::AST|TopDUContext::ForceUpdate)); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); auto context = file.topContext()->childContexts().first()->childContexts().first(); QVERIFY(context); QCOMPARE(context->localDeclarations().size(), 1); QCOMPARE(context->usesCount(), 3); QCOMPARE(context->uses()[0].m_range, RangeInRevision({2, 0}, {2, 1})); QCOMPARE(context->uses()[1].m_range, RangeInRevision({2, 5}, {2, 6})); QCOMPARE(context->uses()[2].m_range, RangeInRevision({2, 8}, {2, 11})); } // see also: https://bugs.kde.org/show_bug.cgi?id=368067 void TestDUChain::testForwardTemplateTypeParameterContext() { TestFile file(QStringLiteral(R"( template class Foo; class MatchingName { void bar(); }; void MatchingName::bar() { } )"), QStringLiteral("cpp")); file.parse(); QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; const auto top = file.topContext(); QVERIFY(top); DUChainDumper dumper(DUChainDumper::Features(DUChainDumper::DumpContext | DUChainDumper::DumpProblems)); dumper.dump(top); auto declarations = top->localDeclarations(); QCOMPARE(declarations.size(), 2); } // see also: https://bugs.kde.org/show_bug.cgi?id=368460 void TestDUChain::testTemplateFunctionParameterName() { TestFile file(QStringLiteral(R"( template void foo(int name); void bar(int name); )"), QStringLiteral("cpp")); file.parse(); QVERIFY(file.waitForParsed(500)); DUChainReadLocker lock; const auto top = file.topContext(); QVERIFY(top); DUChainDumper dumper(DUChainDumper::Features(DUChainDumper::DumpContext | DUChainDumper::DumpProblems)); dumper.dump(top); auto declarations = top->localDeclarations(); QCOMPARE(declarations.size(), 2); for (auto decl : declarations) { auto ctx = DUChainUtils::getArgumentContext(decl); QVERIFY(ctx); auto args = ctx->localDeclarations(); if (decl == declarations.first()) QEXPECT_FAIL("", "We get two declarations, for both template and args :(", Continue); QCOMPARE(args.size(), 1); if (decl == declarations.first()) QEXPECT_FAIL("", "see above, this then triggers T T here", Continue); QCOMPARE(args.first()->toString(), QStringLiteral("int name")); } } static bool containsErrors(const QList& problems) { auto it = std::find_if(problems.begin(), problems.end(), [] (const Problem::Ptr& problem) { return problem->severity() == Problem::Error; }); return it != problems.end(); } static bool expectedXmmintrinErrors(const QList& problems) { for (const auto& problem : problems) { if (problem->severity() == Problem::Error && !problem->description().contains(QLatin1String("Cannot initialize a parameter of type"))) { return false; } } return true; } static void verifyNoErrors(TopDUContext* top, QSet& checked) { const auto problems = top->problems(); if (containsErrors(problems)) { qDebug() << top->url() << top->problems(); if (top->url().str().endsWith(QLatin1String("xmmintrin.h")) && expectedXmmintrinErrors(problems)) { QEXPECT_FAIL("", "there are still some errors in xmmintrin.h b/c some clang provided intrinsincs are more strict than the GCC ones.", Continue); QVERIFY(false); } else { QFAIL("parse error detected"); } } const auto imports = top->importedParentContexts(); for (const auto& import : imports) { auto ctx = import.context(top); QVERIFY(ctx); auto importedTop = ctx->topContext(); if (checked.contains(importedTop)) { continue; } checked.insert(importedTop); verifyNoErrors(importedTop, checked); } } void TestDUChain::testFriendDeclaration() { TestFile file(QStringLiteral(R"( struct FriendFoo { friend class FriendBar; }; class FriendBar{}; FriendBar friendBar; )"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 3); auto friendBar = file.topContext()->localDeclarations()[1]; if (CINDEX_VERSION_MINOR < 37) { QEXPECT_FAIL("", "Your clang version is too old", Abort); } QCOMPARE(friendBar->uses().size(), 1); QCOMPARE(friendBar->uses().begin()->first(), RangeInRevision(3,25,3,34)); QCOMPARE(friendBar->uses().begin()->last(), RangeInRevision(8,8,8,17)); } } void TestDUChain::testVariadicTemplateArguments() { TestFile file(QStringLiteral(R"( template class VariadicTemplate {}; VariadicTemplate variadic; )"), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 2); auto decl = file.topContext()->localDeclarations()[1]; QVERIFY(decl); if (CINDEX_VERSION_MINOR < 37) { QEXPECT_FAIL("", "Your clang version is too old", Abort); } QCOMPARE(decl->toString(), QStringLiteral("VariadicTemplate< int, double, bool > variadic")); QVERIFY(decl->abstractType()); QCOMPARE(decl->abstractType()->toString(), QStringLiteral("VariadicTemplate< int, double, bool >")); } } void TestDUChain::testGccCompatibility() { // TODO: make it easier to change the compiler provider for testing purposes QTemporaryDir dir; auto project = new TestProject(Path(dir.path()), this); auto definesAndIncludesConfig = project->projectConfiguration()->group("CustomDefinesAndIncludes"); auto pathConfig = definesAndIncludesConfig.group("ProjectPath0"); pathConfig.writeEntry("Path", "."); pathConfig.group("Compiler").writeEntry("Name", "GCC"); m_projectController->addProject(project); { // TODO: Also test in C mode. Currently it doesn't work (some intrinsics missing?) TestFile file(QStringLiteral(R"( #include int main() { return 0; } )"), QStringLiteral("cpp"), project, dir.path()); file.parse(); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; QSet checked; verifyNoErrors(file.topContext(), checked); } m_projectController->closeAllProjects(); } void TestDUChain::testLambda() { TestFile file(QStringLiteral("auto lambda = [](int p1, int p2, int p3) { int var1, var2; };"), QStringLiteral("cpp")); QVERIFY(file.parseAndWait()); { DUChainReadLocker lock; QVERIFY(file.topContext()); QCOMPARE(file.topContext()->localDeclarations().size(), 1); QCOMPARE(file.topContext()->childContexts().size(), 1); auto lambdaContext = file.topContext()->childContexts().first(); QCOMPARE(lambdaContext->type(), DUContext::Function); QCOMPARE(lambdaContext->localDeclarations().size(), 3); QCOMPARE(lambdaContext->childContexts().size(), 1); QCOMPARE(lambdaContext->childContexts().first()->type(), DUContext::Other); QCOMPARE(lambdaContext->childContexts().first()->localDeclarations().size(), 2); } } void TestDUChain::testQtIntegration() { QTemporaryDir includeDir; { QDir dir(includeDir.path()); dir.mkdir(QStringLiteral("QtCore")); // create the file but don't put anything in it QFile header(includeDir.path() + "/QtCore/qobjectdefs.h"); QVERIFY(header.open(QIODevice::WriteOnly | QIODevice::Text)); } QTemporaryDir dir; auto project = new TestProject(Path(dir.path()), this); m_provider->defines.clear(); m_provider->includes = {Path(includeDir.path() + "/QtCore")}; m_projectController->addProject(project); { TestFile file(QStringLiteral(R"( #define slots #define signals #define Q_SLOTS #define Q_SIGNALS #include struct MyObject { public: void other1(); public slots: void slot1(); signals: void signal1(); private Q_SLOTS: void slot2(); Q_SIGNALS: void signal2(); public: void other2(); }; )"), QStringLiteral("cpp"), project, dir.path()); file.parse(); QVERIFY(file.waitForParsed(5000)); DUChainReadLocker lock; auto top = file.topContext(); QVERIFY(top); QVERIFY(top->problems().isEmpty()); const auto methods = top->childContexts().last()->localDeclarations(); QCOMPARE(methods.size(), 6); for (auto method : methods) { auto classFunction = dynamic_cast(method); QVERIFY(classFunction); auto id = classFunction->identifier().toString(); QCOMPARE(classFunction->isSignal(), id.startsWith(QLatin1String("signal"))); QCOMPARE(classFunction->isSlot(), id.startsWith(QLatin1String("slot"))); } } m_projectController->closeAllProjects(); } void TestDUChain::testHasInclude() { TestFile header(QStringLiteral(R"( #pragma once #if __has_include_next() // good #else #error broken c++11 setup (__has_include_next) #endif )"), QStringLiteral("h")); // NOTE: header is _not_ explicitly being parsed, instead the impl job does that TestFile file(QStringLiteral(R"( #if __has_include() // good #else #error broken c++11 setup (__has_include) #endif #include "%1" )").arg(header.url().str()), QStringLiteral("cpp")); file.parse(TopDUContext::AllDeclarationsContextsAndUses); QVERIFY(file.waitForParsed(1000)); { DUChainDumper dumper{DUChainDumper::DumpProblems}; DUChainReadLocker lock; QVERIFY(file.topContext()); dumper.dump(file.topContext()); QVERIFY(file.topContext()->problems().isEmpty()); auto headerCtx = DUChain::self()->chainForDocument(header.url()); QVERIFY(headerCtx); dumper.dump(headerCtx); QVERIFY(headerCtx->problems().count() <= 1); - for (const auto& problem : headerCtx->problems()) { + const auto& headerProblems = headerCtx->problems(); + for (const auto& problem : headerProblems) { // ignore the following error: "#include_next with absolute path [-Winclude-next-absolute-path]" "" [ (2, 12) -> (2, 30) ] QVERIFY(problem->description().contains(QLatin1String("-Winclude-next-absolute-path"))); } } } diff --git a/plugins/cmake/cmakemanager.cpp b/plugins/cmake/cmakemanager.cpp index f5d57497f1..3e07ab9de9 100644 --- a/plugins/cmake/cmakemanager.cpp +++ b/plugins/cmake/cmakemanager.cpp @@ -1,986 +1,987 @@ /* KDevelop CMake Support * * Copyright 2006 Matt Rogers * Copyright 2007-2013 Aleix Pol * * 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 "cmakemanager.h" #include "cmakeedit.h" #include "cmakeutils.h" #include "cmakeprojectdata.h" #include "duchain/cmakeparsejob.h" #include "cmakeimportjsonjob.h" #include "debug.h" #include "settings/cmakepreferences.h" #include "cmakecodecompletionmodel.h" #include "cmakenavigationwidget.h" #include "icmakedocumentation.h" #include "cmakemodelitems.h" #include "testing/ctestutils.h" #include "cmakeserverimportjob.h" #include "cmakeserver.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 Q_DECLARE_METATYPE(KDevelop::IProject*) using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(CMakeSupportFactory, "kdevcmakemanager.json", registerPlugin(); ) const QString DIALOG_CAPTION = i18n("KDevelop - CMake Support"); CMakeManager::CMakeManager( QObject* parent, const QVariantList& ) : KDevelop::AbstractFileManagerPlugin( QStringLiteral("kdevcmakemanager"), parent ) , m_filter( new ProjectFilterManager( this ) ) { if (CMake::findExecutable().isEmpty()) { setErrorDescription(i18n("Unable to find a CMake executable. Is one installed on the system?")); m_highlight = nullptr; return; } m_highlight = new KDevelop::CodeHighlighting(this); new CodeCompletion(this, new CMakeCodeCompletionModel(this), name()); connect(ICore::self()->projectController(), &IProjectController::projectClosing, this, &CMakeManager::projectClosing); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &CMakeManager::reloadProjects); connect(this, &KDevelop::AbstractFileManagerPlugin::folderAdded, this, &CMakeManager::folderAdded); // m_fileSystemChangeTimer = new QTimer(this); // m_fileSystemChangeTimer->setSingleShot(true); // m_fileSystemChangeTimer->setInterval(100); // connect(m_fileSystemChangeTimer,SIGNAL(timeout()),SLOT(filesystemBuffererTimeout())); } CMakeManager::~CMakeManager() { parseLock()->lockForWrite(); // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state parseLock()->unlock(); } bool CMakeManager::hasBuildInfo(ProjectBaseItem* item) const { return m_projects[item->project()].compilationData.files.contains(item->path()); } Path CMakeManager::buildDirectory(KDevelop::ProjectBaseItem *item) const { // CMakeFolderItem *fi=dynamic_cast(item); // Path ret; // ProjectBaseItem* parent = fi ? fi->formerParent() : item->parent(); // if (parent) // ret=buildDirectory(parent); // else // ret=Path(CMake::currentBuildDir(item->project())); // // if(fi) // ret.addPath(fi->buildDir()); // return ret; return Path(CMake::currentBuildDir(item->project())); } KDevelop::ProjectFolderItem* CMakeManager::import( KDevelop::IProject *project ) { CMake::checkForNeedingConfigure(project); return AbstractFileManagerPlugin::import(project); } class ChooseCMakeInterfaceJob : public ExecuteCompositeJob { Q_OBJECT public: ChooseCMakeInterfaceJob(IProject* project, CMakeManager* manager) : ExecuteCompositeJob(manager, {}) , project(project) , manager(manager) { } void start() override { server = new CMakeServer(project); connect(server, &CMakeServer::connected, this, &ChooseCMakeInterfaceJob::successfulConnection); connect(server, &CMakeServer::finished, this, &ChooseCMakeInterfaceJob::failedConnection); } private: void successfulConnection() { auto job = new CMakeServerImportJob(project, server, this); connect(job, &CMakeServerImportJob::result, this, [this, job](){ if (job->error() == 0) { manager->integrateData(job->projectData(), job->project()); } }); addSubjob(job); ExecuteCompositeJob::start(); } void failedConnection(int code) { Q_ASSERT(code > 0); Q_ASSERT(!server->isServerAvailable()); server->deleteLater(); server = nullptr; // parse the JSON file CMakeImportJsonJob* job = new CMakeImportJsonJob(project, this); // create the JSON file if it doesn't exist auto commandsFile = CMake::commandsFile(project); if (!QFileInfo::exists(commandsFile.toLocalFile())) { qCDebug(CMAKE) << "couldn't find commands file:" << commandsFile << "- now trying to reconfigure"; addSubjob(manager->builder()->configure(project)); } connect(job, &CMakeImportJsonJob::result, this, [this, job]() { if (job->error() == 0) { manager->integrateData(job->projectData(), job->project()); } }); addSubjob(job); ExecuteCompositeJob::start(); } CMakeServer* server = nullptr; IProject* const project; CMakeManager* const manager; }; KJob* CMakeManager::createImportJob(ProjectFolderItem* item) { auto project = item->project(); auto job = new ChooseCMakeInterfaceJob(project, this); connect(job, &KJob::result, this, [this, job, project](){ if (job->error() != 0) { qCWarning(CMAKE) << "couldn't load project successfully" << project->name(); m_projects.remove(project); } }); const QList jobs = { job, KDevelop::AbstractFileManagerPlugin::createImportJob(item) // generate the file system listing }; Q_ASSERT(!jobs.contains(nullptr)); ExecuteCompositeJob* composite = new ExecuteCompositeJob(this, jobs); // even if the cmake call failed, we want to load the project so that the project can be worked on composite->setAbortOnError(false); return composite; } // QList CMakeManager::parse(ProjectFolderItem*) // { return QList< ProjectFolderItem* >(); } // // QList CMakeManager::targets() const { QList ret; foreach(IProject* p, m_projects.keys()) { ret+=p->projectItem()->targetList(); } return ret; } CMakeFile CMakeManager::fileInformation(KDevelop::ProjectBaseItem* item) const { const auto & data = m_projects[item->project()].compilationData; QHash::const_iterator it = data.files.constFind(item->path()); if (it == data.files.constEnd()) { // if the item path contains a symlink, then we will not find it in the lookup table // as that only only stores canonicalized paths. Thus, we fallback to // to the canonicalized path and see if that brings up any matches const auto canonicalized = Path(QFileInfo(item->path().toLocalFile()).canonicalFilePath()); it = data.files.constFind(canonicalized); } if (it != data.files.constEnd()) { return *it; } else { // otherwise look for siblings and use the include paths of any we find const Path folder = item->folder() ? item->path() : item->path().parent(); for( it = data.files.constBegin(); it != data.files.constEnd(); ++it) { if (folder.isDirectParentOf(it.key())) { return *it; } } } // last-resort fallback: bubble up the parent chain, and keep looking for include paths if (auto parent = item->parent()) { return fileInformation(parent); } return {}; } Path::List CMakeManager::includeDirectories(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).includes; } Path::List CMakeManager::frameworkDirectories(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).frameworkDirectories; } QHash CMakeManager::defines(KDevelop::ProjectBaseItem *item ) const { return fileInformation(item).defines; } QString CMakeManager::extraArguments(KDevelop::ProjectBaseItem *item) const { return fileInformation(item).compileFlags; } KDevelop::IProjectBuilder * CMakeManager::builder() const { IPlugin* i = core()->pluginController()->pluginForExtension( QStringLiteral("org.kdevelop.IProjectBuilder"), QStringLiteral("KDevCMakeBuilder")); Q_ASSERT(i); KDevelop::IProjectBuilder* _builder = i->extension(); Q_ASSERT(_builder ); return _builder ; } bool CMakeManager::reload(KDevelop::ProjectFolderItem* folder) { qCDebug(CMAKE) << "reloading" << folder->path(); IProject* project = folder->project(); if (!project->isReady()) return false; KJob *job = createImportJob(folder); project->setReloadJob(job); ICore::self()->runController()->registerJob( job ); if (folder == project->projectItem()) { connect(job, &KJob::finished, this, [project](KJob* job) { if (job->error()) return; KDevelop::ICore::self()->projectController()->reparseProject(project, true); }); } return true; } static void populateTargets(ProjectFolderItem* folder, const QHash>& targets) { static QSet standardTargets = { QStringLiteral("edit_cache"), QStringLiteral("rebuild_cache"), QStringLiteral("list_install_components"), QStringLiteral("test"), //not really standard, but applicable for make and ninja QStringLiteral("install") }; QList dirTargets = kFilter>(targets[folder->path()], [](const CMakeTarget& target) -> bool { return target.type != CMakeTarget::Custom || (!target.name.endsWith(QLatin1String("_automoc")) && !target.name.endsWith(QLatin1String("_autogen")) && !standardTargets.contains(target.name) && !target.name.startsWith(QLatin1String("install/")) ); }); const auto tl = folder->targetList(); for (ProjectTargetItem* item : tl) { const auto idx = kIndexOf(dirTargets, [item](const CMakeTarget& target) { return target.name == item->text(); }); if (idx < 0) { delete item; } else { dirTargets.removeAt(idx); } } foreach (const auto& target, dirTargets) { switch(target.type) { case CMakeTarget::Executable: new CMakeTargetItem(folder, target.name, target.artifacts.value(0)); break; case CMakeTarget::Library: new ProjectLibraryTargetItem(folder->project(), target.name, folder); break; case CMakeTarget::Custom: new ProjectTargetItem(folder->project(), target.name, folder); break; } } foreach (ProjectFolderItem* children, folder->folderList()) { populateTargets(children, targets); } } void CMakeManager::integrateData(const CMakeProjectData &data, KDevelop::IProject* project) { if (data.m_server) { connect(data.m_server.data(), &CMakeServer::response, project, [this, project](const QJsonObject& response) { serverResponse(project, response); }); } else { connect(data.watcher.data(), &QFileSystemWatcher::fileChanged, this, &CMakeManager::dirtyFile); connect(data.watcher.data(), &QFileSystemWatcher::directoryChanged, this, &CMakeManager::dirtyFile); } m_projects[project] = data; populateTargets(project->projectItem(), data.targets); CTestUtils::createTestSuites(data.m_testSuites, data.targets, project); } void CMakeManager::serverResponse(KDevelop::IProject* project, const QJsonObject& response) { if (response[QStringLiteral("type")] == QLatin1String("signal")) { if (response[QStringLiteral("name")] == QLatin1String("dirty")) { m_projects[project].m_server->configure({}); } else qCDebug(CMAKE) << "unhandled signal response..." << project << response; } else if (response[QStringLiteral("type")] == QLatin1String("reply")) { const auto inReplyTo = response[QStringLiteral("inReplyTo")]; if (inReplyTo == QLatin1String("configure")) { m_projects[project].m_server->compute(); } else if (inReplyTo == QLatin1String("compute")) { m_projects[project].m_server->codemodel(); } else if(inReplyTo == QLatin1String("codemodel")) { auto &data = m_projects[project]; CMakeServerImportJob::processCodeModel(response, data); populateTargets(project->projectItem(), data.targets); } else { qCDebug(CMAKE) << "unhandled reply response..." << project << response; } } else { qCDebug(CMAKE) << "unhandled response..." << project << response; } } // void CMakeManager::deletedWatchedDirectory(IProject* p, const QUrl &dir) // { // if(p->folder().equals(dir, QUrl::CompareWithoutTrailingSlash)) { // ICore::self()->projectController()->closeProject(p); // } else { // if(dir.fileName()=="CMakeLists.txt") { // QList folders = p->foldersForUrl(dir.upUrl()); // foreach(ProjectFolderItem* folder, folders) // reload(folder); // } else { // qDeleteAll(p->itemsForUrl(dir)); // } // } // } // void CMakeManager::directoryChanged(const QString& dir) // { // m_fileSystemChangedBuffer << dir; // m_fileSystemChangeTimer->start(); // } // void CMakeManager::filesystemBuffererTimeout() // { // Q_FOREACH(const QString& file, m_fileSystemChangedBuffer) { // realDirectoryChanged(file); // } // m_fileSystemChangedBuffer.clear(); // } // void CMakeManager::realDirectoryChanged(const QString& dir) // { // QUrl path(dir); // IProject* p=ICore::self()->projectController()->findProjectForUrl(dir); // if(!p || !p->isReady()) { // if(p) { // m_fileSystemChangedBuffer << dir; // m_fileSystemChangeTimer->start(); // } // return; // } // // if(!QFile::exists(dir)) { // path.adjustPath(QUrl::AddTrailingSlash); // deletedWatchedDirectory(p, path); // } else // dirtyFile(dir); // } QList< KDevelop::ProjectTargetItem * > CMakeManager::targets(KDevelop::ProjectFolderItem * folder) const { return folder->targetList(); } QString CMakeManager::name() const { return languageName().str(); } IndexedString CMakeManager::languageName() { static IndexedString name("CMake"); return name; } KDevelop::ParseJob * CMakeManager::createParseJob(const IndexedString &url) { return new CMakeParseJob(url, this); } KDevelop::ICodeHighlighting* CMakeManager::codeHighlighting() const { return m_highlight; } // ContextMenuExtension CMakeManager::contextMenuExtension( KDevelop::Context* context ) // { // if( context->type() != KDevelop::Context::ProjectItemContext ) // return IPlugin::contextMenuExtension( context ); // // KDevelop::ProjectItemContext* ctx = dynamic_cast( context ); // QList items = ctx->items(); // // if( items.isEmpty() ) // return IPlugin::contextMenuExtension( context ); // // m_clickedItems = items; // ContextMenuExtension menuExt; // if(items.count()==1 && dynamic_cast(items.first())) // { // QAction * action = new QAction( i18n( "Jump to Target Definition" ), this ); // connect( action, SIGNAL(triggered()), this, SLOT(jumpToDeclaration()) ); // menuExt.addAction( ContextMenuExtension::ProjectGroup, action ); // } // // return menuExt; // } // // void CMakeManager::jumpToDeclaration() // { // DUChainAttatched* du=dynamic_cast(m_clickedItems.first()); // if(du) // { // KTextEditor::Cursor c; // QUrl url; // { // KDevelop::DUChainReadLocker lock; // Declaration* decl = du->declaration().data(); // if(!decl) // return; // c = decl->rangeInCurrentRevision().start(); // url = decl->url().toUrl(); // } // // ICore::self()->documentController()->openDocument(url, c); // } // } // // // TODO: Port to Path API // bool CMakeManager::moveFilesAndFolders(const QList< ProjectBaseItem* > &items, ProjectFolderItem* toFolder) // { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Move files and folders within CMakeLists as follows:")); // // bool cmakeSuccessful = true; // CMakeFolderItem *nearestCMakeFolderItem = nearestCMakeFolder(toFolder); // IProject* project=toFolder->project(); // // QList movedUrls; // QList oldUrls; // foreach(ProjectBaseItem *movedItem, items) // { // QList dirtyItems = cmakeListedItemsAffectedByUrlChange(project, movedItem->url()); // QUrl movedItemNewUrl = toFolder->url(); // movedItemNewUrl.addPath(movedItem->baseName()); // if (movedItem->folder()) // movedItemNewUrl.adjustPath(QUrl::AddTrailingSlash); // foreach(ProjectBaseItem* dirtyItem, dirtyItems) // { // QUrl dirtyItemNewUrl = afterMoveUrl(dirtyItem->url(), movedItem->url(), movedItemNewUrl); // if (CMakeFolderItem* folder = dynamic_cast(dirtyItem)) // { // cmakeSuccessful &= changesWidgetRemoveCMakeFolder(folder, &changesWidget); // cmakeSuccessful &= changesWidgetAddFolder(dirtyItemNewUrl, nearestCMakeFolderItem, &changesWidget); // } // else if (dirtyItem->parent()->target()) // { // cmakeSuccessful &= changesWidgetMoveTargetFile(dirtyItem, dirtyItemNewUrl, &changesWidget); // } // } // // oldUrls += movedItem->url(); // movedUrls += movedItemNewUrl; // } // // if (changesWidget.hasDocuments() && cmakeSuccessful) // cmakeSuccessful &= changesWidget.exec() && changesWidget.applyAllChanges(); // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort move?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // QList::const_iterator it1=oldUrls.constBegin(), it1End=oldUrls.constEnd(); // QList::const_iterator it2=movedUrls.constBegin(); // Q_ASSERT(oldUrls.size()==movedUrls.size()); // for(; it1!=it1End; ++it1, ++it2) // { // if (!KDevelop::renameUrl(project, *it1, *it2)) // return false; // // QList renamedItems = project->itemsForUrl(*it2); // bool dir = QFileInfo(it2->toLocalFile()).isDir(); // foreach(ProjectBaseItem* item, renamedItems) { // if(dir) // emit folderRenamed(Path(*it1), item->folder()); // else // emit fileRenamed(Path(*it1), item->file()); // } // } // // return true; // } // // bool CMakeManager::copyFilesAndFolders(const KDevelop::Path::List &items, KDevelop::ProjectFolderItem* toFolder) // { // IProject* project = toFolder->project(); // foreach(const Path& path, items) { // if (!KDevelop::copyUrl(project, path.toUrl(), toFolder->url())) // return false; // } // // return true; // } // // bool CMakeManager::removeFilesAndFolders(const QList &items) // { // using namespace CMakeEdit; // // IProject* p = 0; // QList urls; // foreach(ProjectBaseItem* item, items) // { // Q_ASSERT(item->folder() || item->file()); // // urls += item->url(); // if(!p) // p = item->project(); // } // // //First do CMakeLists changes // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Remove files and folders from CMakeLists as follows:")); // // bool cmakeSuccessful = changesWidgetRemoveItems(cmakeListedItemsAffectedByItemsChanged(items).toSet(), &changesWidget); // // if (changesWidget.hasDocuments() && cmakeSuccessful) // cmakeSuccessful &= changesWidget.exec() && changesWidget.applyAllChanges(); // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort deletion?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // bool ret = true; // //Then delete the files/folders // foreach(const QUrl& file, urls) // { // ret &= KDevelop::removeUrl(p, file, QDir(file.toLocalFile()).exists()); // } // // return ret; // } bool CMakeManager::removeFilesFromTargets(const QList &/*files*/) { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Modify project targets as follows:")); // // if (!files.isEmpty() && // changesWidgetRemoveFilesFromTargets(files, &changesWidget) && // changesWidget.exec() && // changesWidget.applyAllChanges()) { // return true; // } return false; } // ProjectFolderItem* CMakeManager::addFolder(const Path& folder, ProjectFolderItem* parent) // { // using namespace CMakeEdit; // // CMakeFolderItem *cmakeParent = nearestCMakeFolder(parent); // if(!cmakeParent) // return 0; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Create folder '%1':", folder.lastPathSegment())); // // ///FIXME: use path in changes widget // changesWidgetAddFolder(folder.toUrl(), cmakeParent, &changesWidget); // // if(changesWidget.exec() && changesWidget.applyAllChanges()) // { // if(KDevelop::createFolder(folder.toUrl())) { //If saved we create the folder then the CMakeLists.txt file // Path newCMakeLists(folder, "CMakeLists.txt"); // KDevelop::createFile( newCMakeLists.toUrl() ); // } else // KMessageBox::error(0, i18n("Could not save the change."), // DIALOG_CAPTION); // } // // return 0; // } // // KDevelop::ProjectFileItem* CMakeManager::addFile( const Path& file, KDevelop::ProjectFolderItem* parent) // { // KDevelop::ProjectFileItem* created = 0; // if ( KDevelop::createFile(file.toUrl()) ) { // QList< ProjectFileItem* > files = parent->project()->filesForPath(IndexedString(file.pathOrUrl())); // if(!files.isEmpty()) // created = files.first(); // else // created = new KDevelop::ProjectFileItem( parent->project(), file, parent ); // } // return created; // } bool CMakeManager::addFilesToTarget(const QList< ProjectFileItem* > &/*_files*/, ProjectTargetItem* /*target*/) { return false; // using namespace CMakeEdit; // // const QSet headerExt = QSet() << ".h" << ".hpp" << ".hxx"; // QList< ProjectFileItem* > files = _files; // for (int i = files.count() - 1; i >= 0; --i) // { // QString fileName = files[i]->fileName(); // QString fileExt = fileName.mid(fileName.lastIndexOf('.')); // QList sameUrlItems = files[i]->project()->itemsForUrl(files[i]->url()); // if (headerExt.contains(fileExt)) // files.removeAt(i); // else foreach(ProjectBaseItem* item, sameUrlItems) // { // if (item->parent() == target) // { // files.removeAt(i); // break; // } // } // } // // if(files.isEmpty()) // return true; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Modify target '%1' as follows:", target->baseName())); // // bool success = changesWidgetAddFilesToTarget(files, target, &changesWidget) && // changesWidget.exec() && // changesWidget.applyAllChanges(); // // if(!success) // KMessageBox::error(0, i18n("CMakeLists changes failed."), DIALOG_CAPTION); // // return success; } // bool CMakeManager::renameFileOrFolder(ProjectBaseItem *item, const Path &newPath) // { // using namespace CMakeEdit; // // ApplyChangesWidget changesWidget; // changesWidget.setCaption(DIALOG_CAPTION); // changesWidget.setInformation(i18n("Rename '%1' to '%2':", item->text(), // newPath.lastPathSegment())); // // bool cmakeSuccessful = true, changedCMakeLists=false; // IProject* project=item->project(); // const Path oldPath=item->path(); // QUrl oldUrl=oldPath.toUrl(); // if (item->file()) // { // QList targetFiles = cmakeListedItemsAffectedByUrlChange(project, oldUrl); // foreach(ProjectBaseItem* targetFile, targetFiles) // ///FIXME: use path in changes widget // cmakeSuccessful &= changesWidgetMoveTargetFile(targetFile, newPath.toUrl(), &changesWidget); // } // else if (CMakeFolderItem *folder = dynamic_cast(item)) // ///FIXME: use path in changes widget // cmakeSuccessful &= changesWidgetRenameFolder(folder, newPath.toUrl(), &changesWidget); // // item->setPath(newPath); // if (changesWidget.hasDocuments() && cmakeSuccessful) { // changedCMakeLists = changesWidget.exec() && changesWidget.applyAllChanges(); // cmakeSuccessful &= changedCMakeLists; // } // // if (!cmakeSuccessful) // { // if (KMessageBox::questionYesNo( QApplication::activeWindow(), // i18n("Changes to CMakeLists failed, abort rename?"), // DIALOG_CAPTION ) == KMessageBox::Yes) // return false; // } // // bool ret = KDevelop::renameUrl(project, oldUrl, newPath.toUrl()); // if(!ret) { // item->setPath(oldPath); // } // return ret; // } // // bool CMakeManager::renameFile(ProjectFileItem *item, const Path &newPath) // { // return renameFileOrFolder(item, newPath); // } // // bool CMakeManager::renameFolder(ProjectFolderItem* item, const Path &newPath) // { // return renameFileOrFolder(item, newPath); // } QWidget* CMakeManager::specialLanguageObjectNavigationWidget(const QUrl &url, const KTextEditor::Cursor& position) { KDevelop::TopDUContextPointer top= TopDUContextPointer(KDevelop::DUChain::self()->chainForDocument(url)); Declaration *decl=nullptr; if(top) { int useAt=top->findUseAt(top->transformToLocalRevision(position)); if(useAt>=0) { Use u=top->uses()[useAt]; decl=u.usedDeclaration(top->topContext()); } } CMakeNavigationWidget* doc=nullptr; if(decl) { doc=new CMakeNavigationWidget(top, decl); } else { const IDocument* d=ICore::self()->documentController()->documentForUrl(url); const KTextEditor::Document* e=d->textDocument(); KTextEditor::Cursor start=position, end=position, step(0,1); for(QChar i=e->characterAt(start); i.isLetter() || i=='_'; i=e->characterAt(start-=step)) {} start+=step; for(QChar i=e->characterAt(end); i.isLetter() || i=='_'; i=e->characterAt(end+=step)) {} QString id=e->text(KTextEditor::Range(start, end)); ICMakeDocumentation* docu=CMake::cmakeDocumentation(); if( docu ) { IDocumentation::Ptr desc=docu->description(id, url); if(desc) { doc=new CMakeNavigationWidget(top, desc); } } } return doc; } QPair CMakeManager::cacheValue(KDevelop::IProject* /*project*/, const QString& /*id*/) const { return QPair(); } // { // QPair ret; // if(project==0 && !m_projectsData.isEmpty()) // { // project=m_projectsData.keys().first(); // } // // // qCDebug(CMAKE) << "cache value " << id << project << (m_projectsData.contains(project) && m_projectsData[project].cache.contains(id)); // CMakeProjectData* data = m_projectsData[project]; // if(data && data->cache.contains(id)) // { // const CacheEntry& e=data->cache.value(id); // ret.first=e.value; // ret.second=e.doc; // } // return ret; // }Add // void CMakeManager::projectClosing(IProject* p) { m_projects.remove(p); // delete m_projectsData.take(p); // delete m_watchers.take(p); // // m_filter->remove(p); // // qCDebug(CMAKE) << "Project closed" << p; } // // QStringList CMakeManager::processGeneratorExpression(const QStringList& expr, IProject* project, ProjectTargetItem* target) const // { // QStringList ret; // const CMakeProjectData* data = m_projectsData[project]; // GenerationExpressionSolver exec(data->properties, data->targetAlias); // if(target) // exec.setTargetName(target->text()); // // exec.defineVariable("INSTALL_PREFIX", data->vm.value("CMAKE_INSTALL_PREFIX").join(QString())); // for(QStringList::const_iterator it = expr.constBegin(), itEnd = expr.constEnd(); it!=itEnd; ++it) { // QStringList val = exec.run(*it).split(';'); // ret += val; // } // return ret; // } /* void CMakeManager::addPending(const Path& path, CMakeFolderItem* folder) { m_pending.insert(path, folder); } CMakeFolderItem* CMakeManager::takePending(const Path& path) { return m_pending.take(path); } void CMakeManager::addWatcher(IProject* p, const QString& path) { if (QFileSystemWatcher* watcher = m_watchers.value(p)) { watcher->addPath(path); } else { qCWarning(CMAKE) << "Could not find a watcher for project" << p << p->name() << ", path " << path; Q_ASSERT(false); } }*/ // CMakeProjectData CMakeManager::projectData(IProject* project) // { // Q_ASSERT(QThread::currentThread() == project->thread()); // CMakeProjectData* data = m_projectsData[project]; // if(!data) { // data = new CMakeProjectData; // m_projectsData[project] = data; // } // return *data; // } ProjectFilterManager* CMakeManager::filterManager() const { return m_filter; } void CMakeManager::dirtyFile(const QString& path) { qCDebug(CMAKE) << "dirty!" << path; //we initialize again hte project that sent the signal for(QHash::const_iterator it = m_projects.constBegin(), itEnd = m_projects.constEnd(); it!=itEnd; ++it) { if(it->watcher == sender()) { reload(it.key()->projectItem()); break; } } } void CMakeManager::folderAdded(KDevelop::ProjectFolderItem* folder) { populateTargets(folder, m_projects[folder->project()].targets); } ProjectFolderItem* CMakeManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent) { // TODO: when we have data about targets, use folders with targets or similar if (QFile::exists(path.toLocalFile()+"/CMakeLists.txt")) return new KDevelop::ProjectBuildFolderItem( project, path, parent ); else return KDevelop::AbstractFileManagerPlugin::createFolderItem(project, path, parent); } int CMakeManager::perProjectConfigPages() const { return 1; } ConfigPage* CMakeManager::perProjectConfigPage(int number, const ProjectConfigOptions& options, QWidget* parent) { if (number == 0) { return new CMakePreferences(this, options, parent); } return nullptr; } void CMakeManager::reloadProjects() { - for(IProject* project: m_projects.keys()) { + const auto& projects = m_projects.keys(); + for (IProject* project : projects) { CMake::checkForNeedingConfigure(project); reload(project->projectItem()); } } #include "cmakemanager.moc" diff --git a/plugins/cmake/testing/ctestfindjob.cpp b/plugins/cmake/testing/ctestfindjob.cpp index 035ae1e451..e9fe7077ee 100644 --- a/plugins/cmake/testing/ctestfindjob.cpp +++ b/plugins/cmake/testing/ctestfindjob.cpp @@ -1,96 +1,96 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestfindjob.h" #include "ctestsuite.h" #include #include #include #include #include #include #include CTestFindJob::CTestFindJob(CTestSuite* suite, QObject* parent) : KJob(parent) , m_suite(suite) { qCDebug(CMAKE) << "Created a CTestFindJob"; setObjectName(i18n("Parse test suite %1", suite->name())); setCapabilities(Killable); } void CTestFindJob::start() { qCDebug(CMAKE); QMetaObject::invokeMethod(this, "findTestCases", Qt::QueuedConnection); } void CTestFindJob::findTestCases() { if (!m_suite->arguments().isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); return; } m_pendingFiles.clear(); - for (const auto& file : m_suite->sourceFiles()) - { + const auto& sourceFiles = m_suite->sourceFiles(); + for (const auto& file : sourceFiles) { if (!file.isEmpty()) { m_pendingFiles << file; } } qCDebug(CMAKE) << "Source files to update:" << m_pendingFiles; if (m_pendingFiles.isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); return; } foreach (const KDevelop::Path &file, m_pendingFiles) { KDevelop::DUChain::self()->updateContextForUrl(KDevelop::IndexedString(file.toUrl()), KDevelop::TopDUContext::AllDeclarationsAndContexts, this); } } void CTestFindJob::updateReady(const KDevelop::IndexedString& document, const KDevelop::ReferencedTopDUContext& context) { qCDebug(CMAKE) << "context update ready" << m_pendingFiles << document.str(); m_suite->loadDeclarations(document, context); m_pendingFiles.removeAll(KDevelop::Path(document.toUrl())); if (m_pendingFiles.isEmpty()) { KDevelop::ICore::self()->testController()->addTestSuite(m_suite); emitResult(); } } bool CTestFindJob::doKill() { KDevelop::ICore::self()->languageController()->backgroundParser()->revertAllRequests(this); return true; } diff --git a/plugins/cmake/testing/ctestutils.cpp b/plugins/cmake/testing/ctestutils.cpp index 804bba93f5..31b6753359 100644 --- a/plugins/cmake/testing/ctestutils.cpp +++ b/plugins/cmake/testing/ctestutils.cpp @@ -1,85 +1,85 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula CTestTestfile.cmake parsing uses code from the xUnit plugin Copyright 2008 Manuel Breugelmans Copyright 2010 Daniel Calviño Sánchez 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ctestutils.h" #include "ctestsuite.h" #include "ctestfindjob.h" #include #include #include #include #include #include #include #include using namespace KDevelop; static CMakeTarget targetByName(const QHash< KDevelop::Path, QVector>& targets, const QString& name) { - for (const auto &subdir: targets.values()) { + for (const auto& subdir: targets) { for (const auto &target: subdir) { if (target.name == name) return target; } } return {}; } static CMakeTarget targetByExe(const QHash< KDevelop::Path, QVector>& targets, const KDevelop::Path& exe) { - for (const auto &subdir: targets.values()) { + for (const auto& subdir: targets) { for (const auto &target: subdir) { if (target.artifacts.contains(exe)) return target; } } return {}; } void CTestUtils::createTestSuites(const QVector& testSuites, const QHash< KDevelop::Path, QVector>& targets, KDevelop::IProject* project) { for (const Test& test : testSuites) { KDevelop::Path executablePath; CMakeTarget target; if (QDir::isAbsolutePath(test.executable)) { executablePath = KDevelop::Path(test.executable); target = targetByExe(targets, executablePath); } else { target = targetByName(targets, test.executable); if (target.artifacts.isEmpty()) { continue; } executablePath = target.artifacts.first(); } qCDebug(CMAKE) << "looking for tests in test" << test.name << "target" << target.name << "with sources" << target.sources; CTestSuite* suite = new CTestSuite(test.name, executablePath, target.sources.toList(), project, test.arguments, test.properties); ICore::self()->runController()->registerJob(new CTestFindJob(suite)); } } diff --git a/plugins/contextbrowser/contextbrowserview.cpp b/plugins/contextbrowser/contextbrowserview.cpp index d92f227de1..eed341b958 100644 --- a/plugins/contextbrowser/contextbrowserview.cpp +++ b/plugins/contextbrowser/contextbrowserview.cpp @@ -1,390 +1,393 @@ /* * This file is part of KDevelop * * Copyright 2008 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "contextbrowserview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "contextbrowser.h" #include "debug.h" #include #include #include #include "browsemanager.h" #include #include #include #include #include #include using namespace KDevelop; namespace { enum Direction { NextUse, PreviousUse }; void selectUse(ContextBrowserView* view, Direction direction) { auto abstractNaviWidget = dynamic_cast(view->navigationWidget()); if (!abstractNaviWidget) { return; } auto usesWidget = dynamic_cast(abstractNaviWidget->context()->widget()); if (!usesWidget) { return; } OneUseWidget* first = nullptr, *previous = nullptr, *current = nullptr; - for (auto item : usesWidget->items()) { + const auto& usesWidgetItems = usesWidget->items(); + for (auto item : usesWidgetItems) { auto topContext = dynamic_cast(item); if (!topContext) { continue; } - for (auto item : topContext->items()) { + const auto& topContextItems = topContext->items(); + for (auto item : topContextItems) { auto navigationList = dynamic_cast(item); if (!navigationList) { continue; } - for (auto item : navigationList->items()) { + const auto& navigationListItems = navigationList->items(); + for (auto item : navigationListItems) { auto use = dynamic_cast(item); if (!use) { continue; } if (!first) { first = use; } current = use; if (direction == PreviousUse && current->isHighlighted() && previous) { previous->setHighlighted(true); previous->activateLink(); current->setHighlighted(false); return; } if (direction == NextUse && previous && previous->isHighlighted()) { current->setHighlighted(true); current->activateLink(); previous->setHighlighted(false); return; } previous = current; } } } if (direction == NextUse && first) { first->setHighlighted(true); first->activateLink(); if (current && current->isHighlighted()) current->setHighlighted(false); return; } if (direction == PreviousUse && current) { current->setHighlighted(true); current->activateLink(); if (first && first->isHighlighted()) { first->setHighlighted(false); } } } } QWidget* ContextBrowserView::createWidget(KDevelop::DUContext* context) { m_context = IndexedDUContext(context); if(m_context.data()) { return m_context.data()->createNavigationWidget(nullptr, nullptr, {}, {}, AbstractNavigationWidget::EmbeddableWidget); } return nullptr; } KDevelop::IndexedDeclaration ContextBrowserView::declaration() const { return m_declaration; } QWidget* ContextBrowserView::createWidget(Declaration* decl, TopDUContext* topContext) { m_declaration = IndexedDeclaration(decl); return decl->context()->createNavigationWidget(decl, topContext, {}, {}, AbstractNavigationWidget::EmbeddableWidget); } void ContextBrowserView::resetWidget() { if (m_navigationWidget) { delete m_navigationWidget; m_navigationWidget = nullptr; } } void ContextBrowserView::declarationMenu() { DUChainReadLocker lock(DUChain::lock()); AbstractNavigationWidget* navigationWidget = dynamic_cast(m_navigationWidget.data()); if(navigationWidget) { AbstractDeclarationNavigationContext* navigationContext = dynamic_cast(navigationWidget->context().data()); if(navigationContext && navigationContext->declaration().data()) { KDevelop::DeclarationContext* c = new KDevelop::DeclarationContext(navigationContext->declaration().data()); lock.unlock(); QMenu menu(this); QList extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(c, &menu); ContextMenuExtension::populateMenu(&menu, extensions); menu.exec(QCursor::pos()); } } } ContextBrowserView::ContextBrowserView( ContextBrowserPlugin* plugin, QWidget* parent ) : QWidget(parent), m_plugin(plugin), m_navigationWidget(new QTextBrowser()), m_autoLocked(false) { setWindowTitle(i18n("Code Browser")); setWindowIcon( QIcon::fromTheme(QStringLiteral("code-context"), windowIcon()) ); m_allowLockedUpdate = false; m_declarationMenuAction = new QAction(QIcon::fromTheme(QStringLiteral("code-class")), QString(), this); m_declarationMenuAction->setToolTip(i18n("Show declaration menu")); // expose the declaration menu via the context menu; allows hiding the toolbar to save some space // (this will not make it behave like a submenu though) m_declarationMenuAction->setText(i18n("Declaration Menu")); connect(m_declarationMenuAction, &QAction::triggered, this, &ContextBrowserView::declarationMenu); addAction(m_declarationMenuAction); m_lockAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("object-unlocked")), i18n("Lock Current View"), this); m_lockAction->setToolTip(i18n("Lock current view")); m_lockAction->setCheckedState(KGuiItem(i18n("Unlock Current View"), QIcon::fromTheme(QStringLiteral("object-locked")), i18n("Unlock current view"))); m_lockAction->setChecked(false); addAction(m_lockAction); m_layout = new QVBoxLayout; m_layout->setSpacing(0); m_layout->setMargin(0); m_layout->addWidget(m_navigationWidget); //m_layout->addStretch(); setLayout(m_layout); m_plugin->registerToolView(this); } ContextBrowserView::~ContextBrowserView() { m_plugin->unRegisterToolView(this); } void ContextBrowserView::focusInEvent(QFocusEvent* event) { //Indicate that we have focus qCDebug(PLUGIN_CONTEXTBROWSER) << "got focus"; // parentWidget()->setBackgroundRole(QPalette::ToolTipBase); /* m_layout->removeItem(m_buttons);*/ return QWidget::focusInEvent(event); } void ContextBrowserView::focusOutEvent(QFocusEvent* event) { qCDebug(PLUGIN_CONTEXTBROWSER) << "lost focus"; // parentWidget()->setBackgroundRole(QPalette::Background); /* m_layout->insertLayout(0, m_buttons); for(int a = 0; a < m_buttons->count(); ++a) { QWidgetItem* item = dynamic_cast(m_buttons->itemAt(a)); }*/ QWidget::focusOutEvent(event); } bool ContextBrowserView::event(QEvent* event) { QKeyEvent* keyEvent = dynamic_cast(event); if(hasFocus() && keyEvent) { AbstractNavigationWidget* navigationWidget = dynamic_cast(m_navigationWidget.data()); if(navigationWidget && event->type() == QEvent::KeyPress) { int key = keyEvent->key(); if(key == Qt::Key_Left) navigationWidget->previous(); if(key == Qt::Key_Right) navigationWidget->next(); if(key == Qt::Key_Up) navigationWidget->up(); if(key == Qt::Key_Down) navigationWidget->down(); if(key == Qt::Key_Return || key == Qt::Key_Enter) navigationWidget->accept(); if(key == Qt::Key_L) m_lockAction->toggle(); } } return QWidget::event(event); } void ContextBrowserView::showEvent(QShowEvent* event) { DUChainReadLocker lock(DUChain::lock(), 200); if (!lock.locked()) { QWidget::showEvent(event); return; } TopDUContext* top = m_lastUsedTopContext.data(); if(top && m_navigationWidgetDeclaration.isValid()) { //Update the navigation-widget Declaration* decl = m_navigationWidgetDeclaration.getDeclaration(top); if(decl) setDeclaration(decl, top, true); } QWidget::showEvent(event); } bool ContextBrowserView::isLocked() const { bool isLocked; if (m_allowLockedUpdate) { isLocked = false; } else { isLocked = m_lockAction->isChecked(); } return isLocked; } void ContextBrowserView::updateMainWidget(QWidget* widget) { if (widget) { setUpdatesEnabled(false); qCDebug(PLUGIN_CONTEXTBROWSER) << ""; resetWidget(); m_navigationWidget = widget; m_layout->insertWidget(1, widget, 1); m_allowLockedUpdate = false; setUpdatesEnabled(true); if (widget->metaObject()->indexOfSignal(QMetaObject::normalizedSignature("contextChanged(bool,bool)")) != -1) { connect(widget, SIGNAL(contextChanged(bool,bool)), this, SLOT(navigationContextChanged(bool,bool))); } } } void ContextBrowserView::navigationContextChanged(bool wasInitial, bool isInitial) { if(wasInitial && !isInitial && !m_lockAction->isChecked()) { m_autoLocked = true; m_lockAction->setChecked(true); }else if(!wasInitial && isInitial && m_autoLocked) { m_autoLocked = false; m_lockAction->setChecked(false); }else if(isInitial) { m_autoLocked = false; } } void ContextBrowserView::selectNextItem() { selectUse(this, NextUse); } void ContextBrowserView::selectPreviousItem() { selectUse(this, PreviousUse); } void ContextBrowserView::setDeclaration(KDevelop::Declaration* decl, KDevelop::TopDUContext* topContext, bool force) { m_lastUsedTopContext = IndexedTopDUContext(topContext); if(isLocked() && (!m_navigationWidget.data() || !isVisible())) { // Automatically remove the locked state if the view is not visible or the widget was deleted, // because the locked state has side-effects on other navigation functionality. m_autoLocked = false; m_lockAction->setChecked(false); } if(m_navigationWidgetDeclaration == decl->id() && !force) return; m_navigationWidgetDeclaration = decl->id(); if (!isLocked() && (isVisible() || force)) { // NO-OP if tool view is hidden, for performance reasons QWidget* w = createWidget(decl, topContext); updateMainWidget(w); } } KDevelop::IndexedDeclaration ContextBrowserView::lockedDeclaration() const { if(m_lockAction->isChecked()) return declaration(); else return KDevelop::IndexedDeclaration(); } void ContextBrowserView::allowLockedUpdate() { m_allowLockedUpdate = true; } void ContextBrowserView::setNavigationWidget(QWidget* widget) { updateMainWidget(widget); } void ContextBrowserView::setContext(KDevelop::DUContext* context) { if(!context) return; m_lastUsedTopContext = IndexedTopDUContext(context->topContext()); if(context->owner()) { if(context->owner()->id() == m_navigationWidgetDeclaration) return; m_navigationWidgetDeclaration = context->owner()->id(); }else{ m_navigationWidgetDeclaration = DeclarationId(); } if (!isLocked() && isVisible()) { // NO-OP if tool view is hidden, for performance reasons QWidget* w = createWidget(context); updateMainWidget(w); } } void ContextBrowserView::setSpecialNavigationWidget(QWidget* widget) { if (!isLocked() && isVisible()) { Q_ASSERT(widget); updateMainWidget(widget); } else if(widget) { widget->deleteLater(); } } diff --git a/plugins/cppcheck/problemmodel.cpp b/plugins/cppcheck/problemmodel.cpp index 0551781942..b5428ae022 100644 --- a/plugins/cppcheck/problemmodel.cpp +++ b/plugins/cppcheck/problemmodel.cpp @@ -1,162 +1,164 @@ /* This file is part of KDevelop Copyright 2017 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "problemmodel.h" #include "plugin.h" #include "utils.h" #include #include #include #include #include +#include #include namespace cppcheck { inline KDevelop::ProblemModelSet* problemModelSet() { return KDevelop::ICore::self()->languageController()->problemModelSet(); } namespace Strings { QString problemModelId() { return QStringLiteral("Cppcheck"); } } ProblemModel::ProblemModel(Plugin* plugin) : KDevelop::ProblemModel(plugin) , m_plugin(plugin) , m_project(nullptr) { setFeatures(CanDoFullUpdate | ScopeFilter | SeverityFilter | Grouping | CanByPassScopeFilter); reset(); problemModelSet()->addModel(Strings::problemModelId(), i18n("Cppcheck"), this); } ProblemModel::~ProblemModel() { problemModelSet()->removeModel(Strings::problemModelId()); } KDevelop::IProject* ProblemModel::project() const { return m_project; } void ProblemModel::fixProblemFinalLocation(KDevelop::IProblem::Ptr problem) { // Fix problems with incorrect range, which produced by cppcheck's errors // without element. In this case location automatically gets "/". // To avoid this we set project's root path as problem location. Q_ASSERT(m_project); auto range = problem->finalLocation(); if (range.document.isEmpty()) { range.document = KDevelop::IndexedString(m_project->path().toLocalFile()); problem->setFinalLocation(range); } - for (auto diagnostic : problem->diagnostics()) { + const auto& diagnostics = problem->diagnostics(); + for (auto& diagnostic : diagnostics) { fixProblemFinalLocation(diagnostic); } } bool ProblemModel::problemExists(KDevelop::IProblem::Ptr newProblem) { - for (auto problem : m_problems) { + for (auto problem : qAsConst(m_problems)) { if (newProblem->source() == problem->source() && newProblem->severity() == problem->severity() && newProblem->finalLocation() == problem->finalLocation() && newProblem->description() == problem->description() && newProblem->explanation() == problem->explanation()) return true; } return false; } void ProblemModel::addProblems(const QVector& problems) { static int maxLength = 0; if (m_problems.isEmpty()) { maxLength = 0; } for (auto problem : problems) { fixProblemFinalLocation(problem); if (problemExists(problem)) { continue; } m_problems.append(problem); addProblem(problem); // This performs adjusting of columns width in the ProblemsView if (maxLength < problem->description().length()) { maxLength = problem->description().length(); setProblems(m_problems); } } } void ProblemModel::setProblems() { setProblems(m_problems); } void ProblemModel::reset() { reset(nullptr, QString()); } void ProblemModel::reset(KDevelop::IProject* project, const QString& path) { m_project = project; m_path = path; clearProblems(); m_problems.clear(); QString tooltip = i18nc("@info:tooltip", "Re-Run Last Cppcheck Analysis"); if (m_project) { tooltip += QStringLiteral(" (%1)").arg(prettyPathName(m_path)); } setFullUpdateTooltip(tooltip); } void ProblemModel::show() { problemModelSet()->showModel(Strings::problemModelId()); } void ProblemModel::forceFullUpdate() { if (m_project && !m_plugin->isRunning()) { m_plugin->runCppcheck(m_project, m_path); } } } diff --git a/plugins/custom-buildsystem/tests/test_custombuildsystemplugin.cpp b/plugins/custom-buildsystem/tests/test_custombuildsystemplugin.cpp index 94ba0d8b72..fab04d4549 100644 --- a/plugins/custom-buildsystem/tests/test_custombuildsystemplugin.cpp +++ b/plugins/custom-buildsystem/tests/test_custombuildsystemplugin.cpp @@ -1,112 +1,114 @@ /************************************************************************ * KDevelop4 Custom Buildsystem Support * * * * Copyright 2010 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 or version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ************************************************************************/ #include "test_custombuildsystemplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "testconfig.h" using KDevelop::Core; using KDevelop::ICore; using KDevelop::IProject; using KDevelop::TestCore; using KDevelop::AutoTestShell; using KDevelop::KDevSignalSpy; using KDevelop::Path; QTEST_MAIN(TestCustomBuildSystemPlugin) void TestCustomBuildSystemPlugin::cleanupTestCase() { TestCore::shutdown(); } void TestCustomBuildSystemPlugin::initTestCase() { AutoTestShell::init({"KDevCustomBuildSystem", "KDevStandardOutputView"}); TestCore::initialize(); } void TestCustomBuildSystemPlugin::loadSimpleProject() { QUrl projecturl = QUrl::fromLocalFile( PROJECTS_SOURCE_DIR"/simpleproject/simpleproject.kdev4" ); KDevSignalSpy* projectSpy = new KDevSignalSpy( ICore::self()->projectController(), SIGNAL(projectOpened(KDevelop::IProject*)) ); ICore::self()->projectController()->openProject( projecturl ); // Wait for the project to be opened QVERIFY(projectSpy->wait(10000)); IProject* project = ICore::self()->projectController()->findProjectByName( QStringLiteral("SimpleProject") ); QVERIFY( project ); QCOMPARE( project->buildSystemManager()->buildDirectory( project->projectItem() ), Path( "file:///home/andreas/projects/testcustom/build/" ) ); } void TestCustomBuildSystemPlugin::buildDirProject() { QUrl projecturl = QUrl::fromLocalFile( PROJECTS_SOURCE_DIR"/builddirproject/builddirproject.kdev4" ); KDevSignalSpy* projectSpy = new KDevSignalSpy( ICore::self()->projectController(), SIGNAL(projectOpened(KDevelop::IProject*)) ); ICore::self()->projectController()->openProject( projecturl ); // Wait for the project to be opened QVERIFY(projectSpy->wait(10000)); IProject* project = ICore::self()->projectController()->findProjectByName( QStringLiteral("BuilddirProject") ); QVERIFY( project ); Path currentBuilddir = project->buildSystemManager()->buildDirectory( project->projectItem() ); QCOMPARE( currentBuilddir, Path( projecturl ).parent() ); } void TestCustomBuildSystemPlugin::loadMultiPathProject() { QUrl projecturl = QUrl::fromLocalFile( PROJECTS_SOURCE_DIR"/multipathproject/multipathproject.kdev4" ); KDevSignalSpy* projectSpy = new KDevSignalSpy( ICore::self()->projectController(), SIGNAL(projectOpened(KDevelop::IProject*)) ); ICore::self()->projectController()->openProject( projecturl ); // Wait for the project to be opened QVERIFY(projectSpy->wait(10000)); IProject* project = ICore::self()->projectController()->findProjectByName( QStringLiteral("MultiPathProject") ); QVERIFY( project ); KDevelop::ProjectBaseItem* mainfile = nullptr; - for (const auto& file: project->fileSet() ) { - for (auto i: project->filesForPath(file)) { + const auto& files = project->fileSet(); + for (const auto& file : files) { + const auto& filesForPath = project->filesForPath(file); + for (auto i: filesForPath) { if( i->text() == QLatin1String("main.cpp") ) { mainfile = i; break; } } } QVERIFY(mainfile); QCOMPARE( project->buildSystemManager()->buildDirectory( mainfile ), Path( "file:///home/andreas/projects/testcustom/build2/src" ) ); } diff --git a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp index dcc8fef11c..d286f350ce 100644 --- a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp @@ -1,282 +1,282 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "compilerprovider.h" #include "debug.h" #include "qtcompat_p.h" #include "compilerfactories.h" #include "settingsmanager.h" #include #include #include #include #include #include #include using namespace KDevelop; namespace { class NoCompiler : public ICompiler { public: NoCompiler(): ICompiler(i18n("None"), QString(), QString(), false) {} QHash< QString, QString > defines(Utils::LanguageType, const QString&) const override { return {}; } Path::List includes(Utils::LanguageType, const QString&) const override { return {}; } }; static CompilerPointer createDummyCompiler() { static CompilerPointer compiler(new NoCompiler()); return compiler; } ConfigEntry configForItem(KDevelop::ProjectBaseItem* item) { if(!item){ return ConfigEntry(); } const Path itemPath = item->path(); const Path rootDirectory = item->project()->path(); auto paths = SettingsManager::globalInstance()->readPaths(item->project()->projectConfiguration().data()); ConfigEntry config; Path closestPath; // find config entry closest to the requested item for (const auto& entry : paths) { auto configEntry = entry; Path targetDirectory = rootDirectory; targetDirectory.addPath(entry.path); if (targetDirectory == itemPath) { return configEntry; } if (targetDirectory.isParentOf(itemPath)) { if (config.path.isEmpty() || targetDirectory.segments().size() > closestPath.segments().size()) { config = configEntry; closestPath = targetDirectory; } } } return config; } } CompilerProvider::CompilerProvider( SettingsManager* settings, QObject* parent ) : QObject( parent ) , m_settings(settings) { m_factories = { CompilerFactoryPointer(new GccFactory()), CompilerFactoryPointer(new ClangFactory()), #ifdef _WIN32 CompilerFactoryPointer(new MsvcFactory()), #endif }; if (!QStandardPaths::findExecutable( QStringLiteral("clang") ).isEmpty()) { m_factories[1]->registerDefaultCompilers(this); } if (!QStandardPaths::findExecutable( QStringLiteral("gcc") ).isEmpty()) { m_factories[0]->registerDefaultCompilers(this); } #ifdef _WIN32 if (!QStandardPaths::findExecutable("cl.exe").isEmpty()) { m_factories[2]->registerDefaultCompilers(this); } #endif registerCompiler(createDummyCompiler()); retrieveUserDefinedCompilers(); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, [this]() { m_defaultProvider.clear(); }); } CompilerProvider::~CompilerProvider() = default; QHash CompilerProvider::defines( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } QHash CompilerProvider::defines( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::frameworkDirectories( const QString& /* path */ ) const { return {}; } Path::List CompilerProvider::frameworkDirectories( ProjectBaseItem* /* item */ ) const { return {}; } IDefinesAndIncludesManager::Type CompilerProvider::type() const { return IDefinesAndIncludesManager::CompilerSpecific; } CompilerPointer CompilerProvider::defaultCompiler() const { if (m_defaultProvider) return m_defaultProvider; auto rt = ICore::self()->runtimeController()->currentRuntime(); const auto path = QFile::decodeName(rt->getenv("PATH")).split(QtCompat::listSeparator()); for ( const CompilerPointer& compiler : m_compilers ) { const bool absolutePath = QDir::isAbsolutePath(compiler->path()); if ((absolutePath && QFileInfo::exists(rt->pathInHost(Path(compiler->path())).toLocalFile())) || QStandardPaths::findExecutable( compiler->path(), path).isEmpty() ) { continue; } m_defaultProvider = compiler; break; } if (!m_defaultProvider) m_defaultProvider = createDummyCompiler(); qCDebug(DEFINESANDINCLUDES) << "new default compiler" << rt->name() << m_defaultProvider->name() << m_defaultProvider->path(); return m_defaultProvider; } QVector< CompilerPointer > CompilerProvider::compilers() const { return m_compilers; } CompilerPointer CompilerProvider::compilerForItem( KDevelop::ProjectBaseItem* item ) const { auto compiler = configForItem(item).compiler; Q_ASSERT(compiler); return compiler; } bool CompilerProvider::registerCompiler(const CompilerPointer& compiler) { if (!compiler) { return false; } - for(auto c: m_compilers){ + for (auto& c : qAsConst(m_compilers)) { if (c->name() == compiler->name()) { return false; } } m_compilers.append(compiler); return true; } void CompilerProvider::unregisterCompiler(const CompilerPointer& compiler) { if (!compiler->editable()) { return; } for (int i = 0; i < m_compilers.count(); i++) { if (m_compilers[i]->name() == compiler->name()) { m_compilers.remove(i); break; } } } QVector< CompilerFactoryPointer > CompilerProvider::compilerFactories() const { return m_factories; } void CompilerProvider::retrieveUserDefinedCompilers() { - auto compilers = m_settings->userDefinedCompilers(); - for (auto c : compilers) { + const auto compilers = m_settings->userDefinedCompilers(); + for (auto& c : compilers) { registerCompiler(c); } } diff --git a/plugins/custom-definesandincludes/compilerprovider/settingsmanager.cpp b/plugins/custom-definesandincludes/compilerprovider/settingsmanager.cpp index 504d2b6a99..7e107c28dd 100644 --- a/plugins/custom-definesandincludes/compilerprovider/settingsmanager.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/settingsmanager.cpp @@ -1,456 +1,459 @@ /* * This file is part of KDevelop * * Copyright 2010 Andreas Pakulat * Copyright 2014 Sergey Kalinichev * * 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 "settingsmanager.h" #include #include #include #include #include #include +#include #include #include #include #include #include "compilerprovider.h" using namespace KDevelop; namespace { constexpr Utils::LanguageType configurableLanguageTypes[] = { Utils::C, Utils::Cpp, Utils::OpenCl, Utils::Cuda }; namespace ConfigConstants { const QString configKey = QStringLiteral( "CustomDefinesAndIncludes" ); const QString definesKey = QStringLiteral( "Defines" ); const QString includesKey = QStringLiteral( "Includes" ); const QString projectPathPrefix = QStringLiteral( "ProjectPath" ); const QString projectPathKey = QStringLiteral( "Path" ); const QString customBuildSystemGroup = QStringLiteral( "CustomBuildSystem" ); const QString definesAndIncludesGroup = QStringLiteral( "Defines And Includes" ); const QString compilersGroup = QStringLiteral( "Compilers" ); const QString compilerNameKey = QStringLiteral( "Name" ); const QString compilerPathKey = QStringLiteral( "Path" ); const QString compilerTypeKey = QStringLiteral( "Type" ); QString parserArgumentsKey(Utils::LanguageType languageType) { switch (languageType) { case Utils::C: return QStringLiteral("parserArgumentsC"); case Utils::Cpp: return QStringLiteral("parserArguments"); case Utils::OpenCl: return QStringLiteral("parserArgumentsOpenCL"); case Utils::Cuda: return QStringLiteral("parserArgumentsCuda"); case Utils::ObjC: case Utils::Other: break; } Q_UNREACHABLE(); } QString parseAmbiguousAsCPP() { return QStringLiteral("parseAmbiguousAsCPP"); } } // the grouplist is randomly sorted b/c it uses QSet internally // we sort the keys here, as the structure is properly defined for us QStringList sorted(QStringList list) { std::sort(list.begin(), list.end()); return list; } ParserArguments createDefaultArguments() { ParserArguments arguments; arguments[Utils::C] = QStringLiteral("-ferror-limit=100 -fspell-checking -Wdocumentation -Wunused-parameter -Wunreachable-code -Wall -std=c99"); arguments[Utils::Cpp] = QStringLiteral("-ferror-limit=100 -fspell-checking -Wdocumentation -Wunused-parameter -Wunreachable-code -Wall -std=c++11"); arguments[Utils::OpenCl] = QStringLiteral("-ferror-limit=100 -fspell-checking -Wdocumentation -Wunused-parameter -Wunreachable-code -Wall -cl-std=CL1.1"); arguments[Utils::Cuda] = QStringLiteral("-ferror-limit=100 -fspell-checking -Wdocumentation -Wunused-parameter -Wunreachable-code -Wall -std=c++11"); arguments[Utils::ObjC] = QStringLiteral("-ferror-limit=100 -fspell-checking -Wdocumentation -Wunused-parameter -Wunreachable-code -Wall -std=c99"); arguments.parseAmbiguousAsCPP = true; return arguments; } const ParserArguments& defaultArguments() { static ParserArguments arguments = createDefaultArguments(); return arguments; } CompilerPointer createCompilerFromConfig(KConfigGroup& cfg) { auto grp = cfg.group("Compiler"); auto name = grp.readEntry( ConfigConstants::compilerNameKey, QString() ); if (name.isEmpty()) { return SettingsManager::globalInstance()->provider()->defaultCompiler(); } - for (auto c : SettingsManager::globalInstance()->provider()->compilers()) { + const auto& compilers = SettingsManager::globalInstance()->provider()->compilers(); + for (auto& c : compilers) { if (c->name() == name) { return c; } } // Otherwise we have no such compiler registered (broken config file), return default one return SettingsManager::globalInstance()->provider()->defaultCompiler(); } void writeCompilerToConfig(KConfigGroup& cfg, const CompilerPointer& compiler) { Q_ASSERT(compiler); auto grp = cfg.group("Compiler"); // Store only compiler name, path and type retrieved from registered compilers grp.writeEntry(ConfigConstants::compilerNameKey, compiler->name()); } void doWriteSettings( KConfigGroup grp, const QVector& paths ) { int pathIndex = 0; for ( const auto& path : paths ) { KConfigGroup pathgrp = grp.group( ConfigConstants::projectPathPrefix + QString::number( pathIndex++ ) ); pathgrp.writeEntry(ConfigConstants::projectPathKey, path.path); for (auto type : configurableLanguageTypes) { pathgrp.writeEntry(ConfigConstants::parserArgumentsKey(type), path.parserArguments[type]); } pathgrp.writeEntry(ConfigConstants::parseAmbiguousAsCPP(), path.parserArguments.parseAmbiguousAsCPP); { int index = 0; KConfigGroup includes(pathgrp.group(ConfigConstants::includesKey)); for( auto it = path.includes.begin() ; it != path.includes.end(); ++it){ includes.writeEntry(QString::number(++index), *it); } } { KConfigGroup defines(pathgrp.group(ConfigConstants::definesKey)); for (auto it = path.defines.begin(); it != path.defines.end(); ++it) { defines.writeEntry(it.key(), it.value()); } } writeCompilerToConfig(pathgrp, path.compiler); } } /// @param remove if true all read entries will be removed from the config file QVector doReadSettings( KConfigGroup grp, bool remove = false ) { QVector paths; - for( const QString &grpName : sorted(grp.groupList()) ) { + const auto& sortedGroupNames = sorted(grp.groupList()); + for (const QString& grpName : sortedGroupNames) { if ( !grpName.startsWith( ConfigConstants::projectPathPrefix ) ) { continue; } KConfigGroup pathgrp = grp.group( grpName ); ConfigEntry path; path.path = pathgrp.readEntry( ConfigConstants::projectPathKey, "" ); for (auto type : configurableLanguageTypes) { path.parserArguments[type] = pathgrp.readEntry(ConfigConstants::parserArgumentsKey(type), defaultArguments()[type]); } path.parserArguments.parseAmbiguousAsCPP = pathgrp.readEntry(ConfigConstants::parseAmbiguousAsCPP(), defaultArguments().parseAmbiguousAsCPP); for (auto type : configurableLanguageTypes) { if (path.parserArguments[type].isEmpty()) { path.parserArguments[type] = defaultArguments()[type]; } } { // defines // Backwards compatibility with old config style if(pathgrp.hasKey(ConfigConstants::definesKey)) { QByteArray tmp = pathgrp.readEntry( ConfigConstants::definesKey, QByteArray() ); QDataStream s( tmp ); s.setVersion( QDataStream::Qt_4_5 ); // backwards compatible reading QHash defines; s >> defines; path.setDefines(defines); } else { KConfigGroup defines(pathgrp.group(ConfigConstants::definesKey)); QMap defMap = defines.entryMap(); path.defines.reserve(defMap.size()); for(auto it = defMap.constBegin(); it != defMap.constEnd(); ++it) { QString key = it.key(); if(key.isEmpty()) { // Toss out the invalid key and value since a valid // value needs a valid key continue; } else { path.defines.insert(key, it.value()); } } } } { // includes // Backwards compatibility with old config style if(pathgrp.hasKey(ConfigConstants::includesKey)){ QByteArray tmp = pathgrp.readEntry( ConfigConstants::includesKey, QByteArray() ); QDataStream s( tmp ); s.setVersion( QDataStream::Qt_4_5 ); s >> path.includes; } else { KConfigGroup includes(pathgrp.group(ConfigConstants::includesKey)); QMap incMap = includes.entryMap(); for(auto it = incMap.begin(); it != incMap.end(); ++it){ QString value = it.value(); if(value.isEmpty()){ continue; } path.includes += value; } } } path.compiler = createCompilerFromConfig(pathgrp); if ( remove ) { pathgrp.deleteGroup(); } Q_ASSERT(!path.parserArguments.isAnyEmpty()); paths << path; } return paths; } /** * Reads and converts paths from old (Custom Build System's) format to the current one. * @return all converted paths (if any) */ QVector convertedPaths( KConfig* cfg ) { KConfigGroup group = cfg->group( ConfigConstants::customBuildSystemGroup ); if ( !group.isValid() ) return {}; QVector paths; foreach( const QString &grpName, sorted(group.groupList()) ) { KConfigGroup subgroup = group.group( grpName ); if ( !subgroup.isValid() ) continue; paths += doReadSettings( subgroup, true ); } return paths; } } void ConfigEntry::setDefines(const QHash& newDefines) { defines.clear(); defines.reserve(newDefines.size()); for (auto it = newDefines.begin(); it != newDefines.end(); ++it) { defines[it.key()] = it.value().toString(); } } SettingsManager::SettingsManager() : m_provider(this) {} SettingsManager::~SettingsManager() {} SettingsManager* SettingsManager::globalInstance() { Q_ASSERT(QThread::currentThread() == qApp->thread()); static SettingsManager s_globalInstance; return &s_globalInstance; } CompilerProvider* SettingsManager::provider() { return &m_provider; } const CompilerProvider* SettingsManager::provider() const { return &m_provider; } void SettingsManager::writePaths( KConfig* cfg, const QVector< ConfigEntry >& paths ) { Q_ASSERT(QThread::currentThread() == qApp->thread()); KConfigGroup grp = cfg->group( ConfigConstants::configKey ); if ( !grp.isValid() ) return; grp.deleteGroup(); doWriteSettings( grp, paths ); } QVector SettingsManager::readPaths( KConfig* cfg ) const { Q_ASSERT(QThread::currentThread() == qApp->thread()); auto converted = convertedPaths( cfg ); if ( !converted.isEmpty() ) { const_cast(this)->writePaths( cfg, converted ); return converted; } KConfigGroup grp = cfg->group( ConfigConstants::configKey ); if ( !grp.isValid() ) { return {}; } return doReadSettings( grp ); } bool SettingsManager::needToReparseCurrentProject( KConfig* cfg ) const { auto grp = cfg->group( ConfigConstants::definesAndIncludesGroup ); return grp.readEntry( "reparse", true ); } void SettingsManager::writeUserDefinedCompilers(const QVector< CompilerPointer >& compilers) { QVector< CompilerPointer > editableCompilers; for (const auto& compiler : compilers) { if (!compiler->editable()) { continue; } editableCompilers.append(compiler); } KConfigGroup config = KSharedConfig::openConfig()->group(ConfigConstants::compilersGroup); config.deleteGroup(); config.writeEntry("number", editableCompilers.count()); int i = 0; for (const auto& compiler : editableCompilers) { KConfigGroup grp = config.group(QString::number(i)); ++i; grp.writeEntry(ConfigConstants::compilerNameKey, compiler->name()); grp.writeEntry(ConfigConstants::compilerPathKey, compiler->path()); grp.writeEntry(ConfigConstants::compilerTypeKey, compiler->factoryName()); } config.sync(); } QVector< CompilerPointer > SettingsManager::userDefinedCompilers() const { QVector< CompilerPointer > compilers; KConfigGroup config = KSharedConfig::openConfig()->group(ConfigConstants::compilersGroup); int count = config.readEntry("number", 0); for (int i = 0; i < count; i++) { KConfigGroup grp = config.group(QString::number(i)); auto name = grp.readEntry(ConfigConstants::compilerNameKey, QString()); auto path = grp.readEntry(ConfigConstants::compilerPathKey, QString()); auto type = grp.readEntry(ConfigConstants::compilerTypeKey, QString()); - auto cf = m_provider.compilerFactories(); - for (auto f : cf) { + const auto cf = m_provider.compilerFactories(); + for (auto& f : cf) { if (f->name() == type) { auto compiler = f->createCompiler(name, path); compilers.append(compiler); } } } return compilers; } ParserArguments SettingsManager::defaultParserArguments() const { return defaultArguments(); } ConfigEntry::ConfigEntry(const QString& path) : path(path) , compiler(SettingsManager::globalInstance()->provider()->defaultCompiler()) , parserArguments(defaultArguments()) {} namespace Utils { LanguageType languageType(const QString& path, bool treatAmbiguousAsCPP) { QMimeDatabase db; const auto mimeType = db.mimeTypeForFile(path).name(); if (mimeType == QStringLiteral("text/x-csrc") || mimeType == QStringLiteral("text/x-chdr") ) { if (treatAmbiguousAsCPP) { if (path.endsWith(QLatin1String(".h"), Qt::CaseInsensitive)) { return Cpp; } } // TODO: No proper mime type detection possible yet // cf. https://bugs.freedesktop.org/show_bug.cgi?id=26913 if (path.endsWith(QLatin1String(".cl"), Qt::CaseInsensitive)) { return OpenCl; } // TODO: No proper mime type detection possible yet // cf. https://bugs.freedesktop.org/show_bug.cgi?id=23700 if (path.endsWith(QLatin1String(".cu"), Qt::CaseInsensitive)) { return Cuda; } return C; } if (mimeType == QStringLiteral("text/x-c++src") || mimeType == QStringLiteral("text/x-c++hdr") ) { return Cpp; } if (mimeType == QStringLiteral("text/x-objcsrc")) { return ObjC; } if (mimeType == QStringLiteral("text/x-opencl-src")) { return OpenCl; } return Other; } } bool ParserArguments::isAnyEmpty() const { return std::any_of(std::begin(arguments), std::end(arguments), [](const QString& args) { return args.isEmpty(); } ); } diff --git a/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp b/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp index 99d6ea0a43..cd60a4a43b 100644 --- a/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/tests/test_compilerprovider.cpp @@ -1,255 +1,258 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "test_compilerprovider.h" #include #include #include #include #include #include #include #include #include #include #include "../compilerprovider.h" #include "../settingsmanager.h" #include "../tests/projectsgenerator.h" using namespace KDevelop; namespace { void testCompilerEntry(SettingsManager* settings, KConfig* config){ auto entries = settings->readPaths(config); auto entry = entries.first(); auto compilers = settings->provider()->compilers(); Q_ASSERT(!compilers.isEmpty()); bool gccCompilerInstalled = std::any_of(compilers.begin(), compilers.end(), [](const CompilerPointer& compiler){return compiler->name().contains(QLatin1String("gcc"), Qt::CaseInsensitive);}); if (gccCompilerInstalled) { QCOMPARE(entry.compiler->name(), QStringLiteral("GCC")); } } void testAddingEntry(SettingsManager* settings, KConfig* config){ auto entries = settings->readPaths(config); auto entry = entries.first(); auto compilers = settings->provider()->compilers(); ConfigEntry otherEntry; otherEntry.defines[QStringLiteral("TEST")] = QStringLiteral("lalal"); otherEntry.includes = QStringList() << QStringLiteral("/foo"); otherEntry.path = QStringLiteral("test"); otherEntry.compiler = compilers.first(); entries << otherEntry; settings->writePaths(config, entries); auto readWriteEntries = settings->readPaths(config); QCOMPARE(readWriteEntries.size(), 2); QCOMPARE(readWriteEntries.at(0).path, entry.path); QCOMPARE(readWriteEntries.at(0).defines, entry.defines); QCOMPARE(readWriteEntries.at(0).includes, entry.includes); QCOMPARE(readWriteEntries.at(0).compiler->name(), entry.compiler->name()); QCOMPARE(readWriteEntries.at(1).path, otherEntry.path); QCOMPARE(readWriteEntries.at(1).defines, otherEntry.defines); QCOMPARE(readWriteEntries.at(1).includes, otherEntry.includes); QCOMPARE(readWriteEntries.at(1).compiler->name(), otherEntry.compiler->name()); } } void TestCompilerProvider::initTestCase() { AutoTestShell::init({QStringLiteral("kdevdefinesandincludesmanager"), QStringLiteral("KDevCustomBuildSystem"), QStringLiteral("KDevStandardOutputView")}); TestCore::initialize(); } void TestCompilerProvider::cleanupTestCase() { TestCore::shutdown(); } void TestCompilerProvider::testRegisterCompiler() { auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); auto cf = provider->compilerFactories(); for (int i = 0 ; i < cf.size(); ++i) { auto compiler = cf[i]->createCompiler(QString::number(i), QString::number(i)); QVERIFY(provider->registerCompiler(compiler)); QVERIFY(!provider->registerCompiler(compiler)); QVERIFY(provider->compilers().contains(compiler)); } QVERIFY(!provider->registerCompiler({})); } void TestCompilerProvider::testCompilerIncludesAndDefines() { auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); - for (auto c : provider->compilers()) { + const auto& compilers = provider->compilers(); + for (auto& c : compilers) { if (!c->editable() && !c->path().isEmpty()) { QVERIFY(!c->defines(Utils::Cpp, {}).isEmpty()); QVERIFY(!c->includes(Utils::Cpp, {}).isEmpty()); } } QVERIFY(!provider->defines(nullptr).isEmpty()); QVERIFY(!provider->includes(nullptr).isEmpty()); auto compiler = provider->compilerForItem(nullptr); QVERIFY(compiler); QVERIFY(!compiler->defines(Utils::Cpp, QStringLiteral("-std=c++11")).isEmpty()); QVERIFY(!compiler->includes(Utils::Cpp, QStringLiteral("-std=c++11")).isEmpty()); } void TestCompilerProvider::testStorageBackwardsCompatible() { auto settings = SettingsManager::globalInstance(); QTemporaryFile file; QVERIFY(file.open()); QTextStream stream(&file); stream << "[Buildset]\n" << "BuildItems=@Variant(\\x00\\x00\\x00\\t\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x1a\\x00S\\x00i\\x00m\\x00p\\x00l\\x00e\\x00P\\x00r\\x00o\\x00j\\x00e\\x00c\\x00t)\n" << "[CustomBuildSystem]\n" << "CurrentConfiguration=BuildConfig0\n" << "[CustomDefinesAndIncludes][ProjectPath0]\n" << "Defines=\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00_\\x00D\\x00E\\x00B\\x00U\\x00G\\x00\\x00\\x00\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x00V\\x00A\\x00R\\x00I\\x00A\\x00B\\x00L\\x00E\\x00\\x00\\x00\\n\\x00\\x00\\x00\\x00\\n\\x00V\\x00A\\x00L\\x00U\\x00E\n" << "Includes=\\x00\\x00\\x00\\x01\\x00\\x00\\x00$\\x00/\\x00u\\x00s\\x00r\\x00/\\x00i\\x00n\\x00c\\x00l\\x00u\\x00d\\x00e\\x00/\\x00m\\x00y\\x00d\\x00i\\x00r\n" << "Path=/\n" << "[CustomDefinesAndIncludes][ProjectPath0][Compiler]\nName=GCC\nPath=gcc\nType=GCC\n"; file.close(); KConfig config(file.fileName()); auto entries = settings->readPaths(&config); QCOMPARE(entries.size(), 1); auto entry = entries.first(); Defines defines; defines[QStringLiteral("VARIABLE")] = QStringLiteral("VALUE"); defines[QStringLiteral("_DEBUG")] = QString(); QCOMPARE(entry.defines, defines); QStringList includes = QStringList() << QStringLiteral("/usr/include/mydir"); QCOMPARE(entry.includes, includes); QCOMPARE(entry.path, QString("/")); QVERIFY(entry.compiler); testCompilerEntry(settings, &config); testAddingEntry(settings, &config); } void TestCompilerProvider::testStorageNewSystem() { auto settings = SettingsManager::globalInstance(); QTemporaryFile file; QVERIFY(file.open()); QTextStream stream(&file); stream << "[Buildset]\n" << "BuildItems=@Variant(\\x00\\x00\\x00\\t\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x1a\\x00S\\x00i\\x00m\\x00p\\x00l\\x00e\\x00P\\x00r\\x00o\\x00j\\x00e\\x00c\\x00t)\n\n" << "[CustomBuildSystem]\n" << "CurrentConfiguration=BuildConfig0\n\n" << "[CustomDefinesAndIncludes][ProjectPath0]\n" << "Path=/\n\n" << "[CustomDefinesAndIncludes][ProjectPath0][Defines]\n" << "_DEBUG=\n" << "VARIABLE=VALUE\n" << "[CustomDefinesAndIncludes][ProjectPath0][Includes]\n" << "1=/usr/include/mydir\n" << "2=/usr/local/include/mydir\n" << "[CustomDefinesAndIncludes][ProjectPath0][Compiler]\nName=GCC\nPath=gcc\nType=GCC\n"; file.close(); KConfig config(file.fileName()); auto entries = settings->readPaths(&config); QCOMPARE(entries.size(), 1); auto entry = entries.first(); QCOMPARE(entry.path, QString("/")); Defines defines; defines[QStringLiteral("VARIABLE")] = QStringLiteral("VALUE"); defines[QStringLiteral("_DEBUG")] = QString(); QCOMPARE(entry.defines, defines); QMap includeMap; includeMap[QStringLiteral("1")] = QStringLiteral("/usr/include/mydir"); includeMap[QStringLiteral("2")] = QStringLiteral("/usr/local/include/mydir"); int i = 0; for(auto it = includeMap.begin(); it != includeMap.end(); it++) { QCOMPARE(entry.includes.at(i++), it.value()); } testCompilerEntry(settings, &config); testAddingEntry(settings, &config); } void TestCompilerProvider::testCompilerIncludesAndDefinesForProject() { auto project = ProjectsGenerator::GenerateMultiPathProject(); Q_ASSERT(project); auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); Q_ASSERT(!provider->compilerFactories().isEmpty()); auto compiler = provider->compilerFactories().first()->createCompiler(QStringLiteral("name"), QStringLiteral("path")); QVERIFY(provider->registerCompiler(compiler)); QVERIFY(provider->compilers().contains(compiler)); auto projectCompiler = provider->compilerForItem(project->projectItem()); QVERIFY(projectCompiler); QVERIFY(projectCompiler != compiler); ProjectBaseItem* mainfile = nullptr; - for (const auto& file: project->fileSet() ) { - for (auto i: project->filesForPath(file)) { + const auto& fileSet = project->fileSet(); + for (const auto& file: fileSet) { + const auto& files = project->filesForPath(file); + for (auto i: files) { if( i->text() == QLatin1String("main.cpp") ) { mainfile = i; break; } } } QVERIFY(mainfile); auto mainCompiler = provider->compilerForItem(mainfile); QVERIFY(mainCompiler); QVERIFY(mainCompiler->name() == projectCompiler->name()); ConfigEntry entry; entry.path = QStringLiteral("src/main.cpp"); entry.compiler = compiler; auto entries = settings->readPaths(project->projectConfiguration().data()); entries.append(entry); settings->writePaths(project->projectConfiguration().data(), entries); QVERIFY(provider->compilers().contains(compiler)); mainCompiler = provider->compilerForItem(mainfile); QVERIFY(mainCompiler); QVERIFY(mainCompiler->name() == compiler->name()); ICore::self()->projectController()->closeProject(project); } QTEST_MAIN(TestCompilerProvider) diff --git a/plugins/custom-definesandincludes/compilerprovider/widget/compilersmodel.cpp b/plugins/custom-definesandincludes/compilerprovider/widget/compilersmodel.cpp index 7e7d119d96..9f3562f34e 100644 --- a/plugins/custom-definesandincludes/compilerprovider/widget/compilersmodel.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/widget/compilersmodel.cpp @@ -1,313 +1,314 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "compilersmodel.h" #include #include //Represents a single row in the table class TreeItem { public: explicit TreeItem(const QList &data, TreeItem *parent = nullptr) :m_itemData(data) ,m_parentItem(parent) {} virtual ~TreeItem() { removeChilds(); } void appendChild(TreeItem *item) { m_childItems.append(item); } void removeChild(int row) { m_childItems.removeAt(row); } TreeItem *child(int row) { return m_childItems.value(row); } int childCount() const { return m_childItems.count(); } int columnCount() const { return m_itemData.count(); } virtual QVariant data(int column) const { return m_itemData.value(column); } TreeItem *parent() { return m_parentItem; } int row() const { if (m_parentItem) { return m_parentItem->m_childItems.indexOf(const_cast(this)); } return 0; } void removeChilds() { qDeleteAll(m_childItems); m_childItems.clear(); } private: QList m_childItems; QList m_itemData; TreeItem *m_parentItem; }; class CompilerItem : public TreeItem { public: CompilerItem(const CompilerPointer& compiler, TreeItem* parent) : TreeItem(QList{compiler->name(), compiler->factoryName()}, parent) , m_compiler(compiler) {} CompilerPointer compiler() { return m_compiler; } QVariant data(int column) const override { return !column ? m_compiler->name() : m_compiler->factoryName(); } private: CompilerPointer m_compiler; }; namespace { TreeItem* autoDetectedRootItem(TreeItem* root) { return root->child(0); } TreeItem* manualRootItem(TreeItem* root) { return root->child(1); } } CompilersModel::CompilersModel(QObject* parent) : QAbstractItemModel(parent) , m_rootItem(new TreeItem({i18n("Name"), i18n("Type")})) { m_rootItem->appendChild(new TreeItem( QList{i18n("Auto-detected"), QString()}, m_rootItem)); m_rootItem->appendChild(new TreeItem( QList{i18n("Manual"), QString()}, m_rootItem)); } CompilersModel::~CompilersModel() { delete m_rootItem; } QVariant CompilersModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || (role != Qt::DisplayRole && role != CompilerDataRole)) { return QVariant(); } TreeItem *item = static_cast(index.internalPointer()); if (role == CompilerDataRole) { QVariant v; if (auto c = dynamic_cast(item)) { if (item->parent() == manualRootItem(m_rootItem)) { v.setValue(c->compiler()); } } return v; } return item->data(index.column()); } int CompilersModel::rowCount(const QModelIndex& parent) const { TreeItem *parentItem; if (parent.column() > 0) { return 0; } if (!parent.isValid()) { parentItem = m_rootItem; } else { parentItem = static_cast(parent.internalPointer()); } return parentItem->childCount(); } int CompilersModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) { return static_cast(parent.internalPointer())->columnCount(); } else { return m_rootItem->columnCount(); } } QVariant CompilersModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { return m_rootItem->data(section); } return QVariant(); } Qt::ItemFlags CompilersModel::flags(const QModelIndex& index) const { if (!index.isValid()) { return Qt::NoItemFlags; } return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } QModelIndex CompilersModel::index(int row, int column, const QModelIndex& parent) const { if (!hasIndex(row, column, parent)) { return QModelIndex(); } TreeItem *parentItem; if (!parent.isValid()) { parentItem = m_rootItem; } else { parentItem = static_cast(parent.internalPointer()); } TreeItem* childItem = parentItem->child(row); if (childItem) { return createIndex(row, column, childItem); } else { return QModelIndex(); } } QModelIndex CompilersModel::parent(const QModelIndex& index) const { if (!index.isValid()) { return QModelIndex(); } TreeItem *childItem = static_cast(index.internalPointer()); TreeItem *parentItem = childItem->parent(); if (parentItem == m_rootItem) { return QModelIndex(); } return createIndex(parentItem->row(), 0, parentItem); } QVector< CompilerPointer > CompilersModel::compilers() const { QVector compilers; for (int idx = 0; idx < 2; idx++) { for (int i = 0; i< m_rootItem->child(idx)->childCount(); i++) { auto compiler = static_cast(m_rootItem->child(idx)->child(i))->compiler(); if (!compiler->name().isEmpty() && !compiler->path().isEmpty()) { compilers.append(compiler); } } } return compilers; } void CompilersModel::setCompilers(const QVector< CompilerPointer >& compilers) { beginResetModel(); autoDetectedRootItem(m_rootItem)->removeChilds(); manualRootItem(m_rootItem)->removeChilds(); - for (auto compiler: compilers) { + for (auto& compiler: compilers) { if (compiler->factoryName().isEmpty()) { continue; } TreeItem* parent = autoDetectedRootItem(m_rootItem); if (compiler->editable()) { parent = manualRootItem(m_rootItem); } parent->appendChild(new CompilerItem(compiler, parent)); } endResetModel(); } QModelIndex CompilersModel::addCompiler(const CompilerPointer& compiler) { beginInsertRows(index(1, 0), manualRootItem(m_rootItem)->childCount(), manualRootItem(m_rootItem)->childCount()); Q_ASSERT(!compiler->factoryName().isEmpty()); manualRootItem(m_rootItem)->appendChild(new CompilerItem(compiler, manualRootItem(m_rootItem))); endInsertRows(); emit compilerChanged(); return index(manualRootItem(m_rootItem)->childCount()-1, 0, index(1, 0)); } bool CompilersModel::removeRows(int row, int count, const QModelIndex& parent) { if (row >= 0 && count > 0 && parent.isValid() && static_cast(parent.internalPointer()) == manualRootItem(m_rootItem)) { beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; ++i) { manualRootItem(m_rootItem)->removeChild(row); } endRemoveRows(); emit compilerChanged(); return true; } return false; } void CompilersModel::updateCompiler(const QItemSelection& compiler) { - for (const auto& idx: compiler.indexes()) { + const auto& indexes = compiler.indexes(); + for (const auto& idx : indexes) { emit dataChanged(idx, idx); } emit compilerChanged(); } diff --git a/plugins/custom-definesandincludes/compilerprovider/widget/compilerswidget.cpp b/plugins/custom-definesandincludes/compilerprovider/widget/compilerswidget.cpp index c020ab51d5..ff95294251 100644 --- a/plugins/custom-definesandincludes/compilerprovider/widget/compilerswidget.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/widget/compilerswidget.cpp @@ -1,257 +1,257 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "compilerswidget.h" #include #if KIO_VERSION < QT_VERSION_CHECK(5,21,0) #include #endif #include #include #include #include #include #include "ui_compilerswidget.h" #include "compilersmodel.h" #include "../compilerprovider/settingsmanager.h" #include "../compilerprovider/compilerprovider.h" #include using namespace KDevelop; CompilersWidget::CompilersWidget(QWidget* parent) : ConfigPage(nullptr, nullptr, parent) , m_ui(new Ui::CompilersWidget) , m_compilersModel(new CompilersModel(this)) { m_ui->setupUi(this); m_ui->compilers->setModel(m_compilersModel); m_ui->compilers->header()->setSectionResizeMode(QHeaderView::Stretch); m_addMenu = new QMenu(m_ui->addButton); m_addMenu->clear(); auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); foreach (const auto& factory, provider->compilerFactories()) { QAction* action = new QAction(m_addMenu); const QString fname = factory->name(); action->setText(fname); connect(action, &QAction::triggered, this, [this, fname]() { addCompiler(fname); }); m_addMenu->addAction(action); } m_ui->addButton->setMenu(m_addMenu); connect(m_ui->removeButton, &QPushButton::clicked, this, &CompilersWidget::deleteCompiler); auto delAction = new QAction( i18n("Delete compiler"), this ); delAction->setShortcut( QKeySequence( QStringLiteral("Del") ) ); delAction->setShortcutContext( Qt::WidgetWithChildrenShortcut ); m_ui->compilers->addAction( delAction ); connect( delAction, &QAction::triggered, this, &CompilersWidget::deleteCompiler ); connect(m_ui->compilers->selectionModel(), &QItemSelectionModel::currentChanged, this, &CompilersWidget::compilerSelected); connect(m_ui->compilerName, &QLineEdit::textEdited, this, &CompilersWidget::compilerEdited); #if KIO_VERSION < QT_VERSION_CHECK(5,21,0) // KUrlRequester::textEdited signal only added for 5.21 auto kUrlRequesterLineEdit = m_ui->compilerPath->lineEdit(); Q_ASSERT(kUrlRequesterLineEdit); connect(kUrlRequesterLineEdit, &QLineEdit::textEdited, this, &CompilersWidget::compilerEdited); #else connect(m_ui->compilerPath, &KUrlRequester::textEdited, this, &CompilersWidget::compilerEdited); #endif connect(m_compilersModel, &CompilersModel::compilerChanged, this, &CompilersWidget::compilerChanged); enableItems(false); } CompilersWidget::~CompilersWidget() { } void CompilersWidget::setCompilers(const QVector< CompilerPointer >& compilers) { m_compilersModel->setCompilers(compilers); m_ui->compilers->expandAll(); } void CompilersWidget::clear() { m_compilersModel->setCompilers({}); } void CompilersWidget::deleteCompiler() { qCDebug(DEFINESANDINCLUDES) << "Deleting compiler"; auto selectionModel = m_ui->compilers->selectionModel(); foreach (const QModelIndex& row, selectionModel->selectedIndexes()) { if (row.column() == 1) { //Don't remove the same compiler twice continue; } if(m_compilersModel->removeRows(row.row(), 1, row.parent())) { auto selectedCompiler = selectionModel->selectedIndexes(); compilerSelected(selectedCompiler.isEmpty() ? QModelIndex() : selectedCompiler.first()); } } emit changed(); } void CompilersWidget::addCompiler(const QString& factoryName) { auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); foreach (const auto& factory, provider->compilerFactories()) { if (factoryName == factory->name()) { //add compiler without any information, the user will fill the data in later auto compilerIndex = m_compilersModel->addCompiler(factory->createCompiler(QString(), QString())); m_ui->compilers->selectionModel()->select(compilerIndex, QItemSelectionModel::Clear | QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); compilerSelected(compilerIndex); m_ui->compilers->scrollTo(compilerIndex); m_ui->compilerName->setFocus(Qt::OtherFocusReason); break; } } emit changed(); } QVector< CompilerPointer > CompilersWidget::compilers() const { return m_compilersModel->compilers(); } void CompilersWidget::compilerSelected(const QModelIndex& index) { auto compiler = index.data(CompilersModel::CompilerDataRole); if (compiler.value()) { m_ui->compilerName->setText(compiler.value()->name()); //NOTE: there is a bug in kLineEdit, which causes textEdited signal to be // spuriously emitted on calling setText(). See bug report here: // https://bugs.kde.org/show_bug.cgi?id=388798 // The resulting spurious call of compilerEdited then fails with an assert. //Work around this bug until it is fixed upstream by disabling signals here const QSignalBlocker blocker(m_ui->compilerPath); m_ui->compilerPath->setText(compiler.value()->path()); enableItems(true); } else { enableItems(false); } } void CompilersWidget::compilerEdited() { auto indexes = m_ui->compilers->selectionModel()->selectedIndexes(); Q_ASSERT(!indexes.isEmpty()); auto compiler = indexes.first().data(CompilersModel::CompilerDataRole); if (!compiler.value()) { return; } compiler.value()->setName(m_ui->compilerName->text()); compiler.value()->setPath(m_ui->compilerPath->text()); m_compilersModel->updateCompiler(m_ui->compilers->selectionModel()->selection()); emit changed(); } void CompilersWidget::enableItems(bool enable) { m_ui->compilerName->setEnabled(enable); m_ui->compilerPath->setEnabled(enable); if(!enable) { m_ui->compilerName->clear(); //NOTE: this is to work around the //spurious signal bug in kLineEdit const QSignalBlocker blocker(m_ui->compilerPath); m_ui->compilerPath->clear(); } } void CompilersWidget::reset() { auto settings = SettingsManager::globalInstance(); setCompilers(settings->provider()->compilers()); } void CompilersWidget::apply() { auto settings = SettingsManager::globalInstance(); auto provider = settings->provider(); settings->writeUserDefinedCompilers(compilers()); const auto& providerCompilers = provider->compilers(); const auto& widgetCompilers = compilers(); - for (auto compiler: providerCompilers) { + for (auto& compiler: providerCompilers) { if (!widgetCompilers.contains(compiler)) { provider->unregisterCompiler(compiler); } } - for (auto compiler: widgetCompilers) { + for (auto& compiler: widgetCompilers) { if (!providerCompilers.contains(compiler)) { provider->registerCompiler(compiler); } } } void CompilersWidget::defaults() { } QString CompilersWidget::name() const { return i18n("C/C++ Compilers"); } QString CompilersWidget::fullName() const { return i18n("Configure C/C++ Compilers"); } QIcon CompilersWidget::icon() const { return QIcon::fromTheme(QStringLiteral("kdevelop")); } KDevelop::ConfigPage::ConfigPageType CompilersWidget::configPageType() const { return ConfigPage::LanguageConfigPage; } diff --git a/plugins/custom-definesandincludes/includepathsconverter.cpp b/plugins/custom-definesandincludes/includepathsconverter.cpp index eab41b2f30..6ffac98ebd 100644 --- a/plugins/custom-definesandincludes/includepathsconverter.cpp +++ b/plugins/custom-definesandincludes/includepathsconverter.cpp @@ -1,245 +1,246 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "includepathsconverter.h" #include #include #include #include #include #include #include #include "settingsmanager.h" using namespace KDevelop; namespace { KSharedConfigPtr openConfigFile(const QString& configFile) { return KSharedConfig::openConfig(configFile, KConfig::SimpleConfig); } QString findconfigFile(const QString& projectDir) { QDirIterator dirIterator(projectDir + '/' + ".kdev4"); while (dirIterator.hasNext()) { dirIterator.next(); if (dirIterator.fileName().endsWith(QLatin1String(".kdev4"))) { return dirIterator.fileInfo().canonicalFilePath(); } } return {}; } QString findProject(const QString& subdirectory) { QDir project(subdirectory); do { if (project.exists(QStringLiteral(".kdev4"))) { return project.path(); } } while(project.cdUp()); return {}; } } IncludePathsConverter::IncludePathsConverter() { } bool IncludePathsConverter::addIncludePaths(const QStringList& includeDirectories, const QString& projectConfigFile, const QString& subdirectory) { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return false; } auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; ConfigEntry config; for (auto& entry: configEntries) { if (path == entry.path) { config = entry; config.includes += includeDirectories; config.includes.removeDuplicates(); entry = config; break; } } if (config.path.isEmpty()) { config.path = path; config.includes = includeDirectories; configEntries.append(config); } SettingsManager::globalInstance()->writePaths(configFile.data(), configEntries); return true; } bool IncludePathsConverter::removeIncludePaths(const QStringList& includeDirectories, const QString& projectConfigFile, const QString& subdirectory) { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return false; } auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; for (auto& entry: configEntries) { if (path == entry.path) { for(const auto& include: includeDirectories) { entry.includes.removeAll(include); } SettingsManager::globalInstance()->writePaths(configFile.data(), configEntries); return true; } } return true; } QStringList IncludePathsConverter::readIncludePaths(const QString& projectConfigFile, const QString& subdirectory) const { auto configFile = openConfigFile(projectConfigFile); if (!configFile) { return {}; } QString path = subdirectory.isEmpty() ? QStringLiteral(".") : subdirectory; auto configEntries = SettingsManager::globalInstance()->readPaths(configFile.data()); for (const auto& entry: configEntries) { if (path == entry.path) { return entry.includes; } } return {}; } int main(int argc, char** argv) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("kdev_includepathsconverter")); QCommandLineParser parser; parser.setApplicationDescription("\nAdds, removes or shows include directories of a project. Also it can be used as a tool to convert include directories from .kdev_include_paths file to the new format.\n\n" "Examples:\ncat /project/path/.kdev_include_paths | xargs -d '\\n' kdev_includepathsconverter -a /project/path/\n\n" "kdev_includepathsconverter -r /project/path/subdirectory/ \"/some/include/dir\" \"/another/include/dir\" \n\n" "kdev_includepathsconverter -l /project/path/another/subdirectory/"); parser.addHelpOption(); QCommandLineOption listOption(QStringLiteral("l"), QCoreApplication::translate("main", "Shows include directories used by the project"), QCoreApplication::translate("main", "project")); parser.addOption(listOption); QCommandLineOption addOption(QStringLiteral("a"), QCoreApplication::translate("main", "Adds include directories to the project"), QCoreApplication::translate("main", "project")); parser.addOption(addOption); QCommandLineOption removeOption(QStringLiteral("r"), QCoreApplication::translate("main", "Removes include directories from the project"), QCoreApplication::translate("main", "project")); parser.addOption(removeOption); parser.process(app); QString projectDir; QStringList includeDirectories(parser.positionalArguments()); std::transform(includeDirectories.begin(), includeDirectories.end(), includeDirectories.begin(), [](const QString& path) { return path.trimmed(); } ); includeDirectories.erase(std::remove_if(includeDirectories.begin(), includeDirectories.end(), [](const QString& path) { return path.isEmpty(); } ), includeDirectories.end()); bool show = parser.isSet(listOption); bool add = parser.isSet(addOption); bool remove = parser.isSet(removeOption); if (show) { projectDir = parser.value(listOption); } else if(add) { projectDir = parser.value(addOption); } else if(remove) { projectDir = parser.value(removeOption); } if (projectDir.isEmpty()) { parser.showHelp(-1); } QString subdirectory = projectDir; projectDir = findProject(projectDir); QString configFile = findconfigFile(projectDir); QTextStream out(stdout); if (configFile.isEmpty()) { out << QCoreApplication::translate("main", "No project found for: ") << subdirectory; return -1; } if (add || remove) { if (includeDirectories.isEmpty()) { parser.showHelp(-1); } } { auto subdirCanonical = QFileInfo(subdirectory).canonicalFilePath(); auto projectCanonical = QFileInfo(projectDir).canonicalFilePath(); if (subdirCanonical != projectCanonical) { subdirectory = subdirCanonical.mid(projectCanonical.size()); if (subdirectory.startsWith('/')) { subdirectory.remove(0,1); } } else { subdirectory.clear(); } } IncludePathsConverter converter; if (remove) { if (!converter.removeIncludePaths(includeDirectories, configFile, subdirectory)) { out << QCoreApplication::translate("main", "Can't remove include paths"); } } if (add) { if (!converter.addIncludePaths(includeDirectories, configFile, subdirectory)) { out << QCoreApplication::translate("main", "Can't add include paths"); } } if (show) { - for (const auto& include: converter.readIncludePaths(configFile, subdirectory)) { + const auto& includes = converter.readIncludePaths(configFile, subdirectory); + for (const auto& include : includes) { out << include << "\n"; } } } diff --git a/plugins/custom-definesandincludes/tests/test_definesandincludes.cpp b/plugins/custom-definesandincludes/tests/test_definesandincludes.cpp index 3f354c8f08..cfa7faecc6 100644 --- a/plugins/custom-definesandincludes/tests/test_definesandincludes.cpp +++ b/plugins/custom-definesandincludes/tests/test_definesandincludes.cpp @@ -1,150 +1,152 @@ /************************************************************************ * * * Copyright 2010 Andreas Pakulat * * Copyright 2014 Sergey Kalinichev * * * * 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 or version 3 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ************************************************************************/ #include "test_definesandincludes.h" #include "projectsgenerator.h" #include #include #include #include #include #include #include #include "idefinesandincludesmanager.h" using namespace KDevelop; static IProject* s_currentProject = nullptr; void TestDefinesAndIncludes::cleanupTestCase() { TestCore::shutdown(); } void TestDefinesAndIncludes::initTestCase() { AutoTestShell::init({QStringLiteral("kdevdefinesandincludesmanager"), QStringLiteral("KDevCustomBuildSystem"), QStringLiteral("KDevStandardOutputView")}); TestCore::initialize(); } void TestDefinesAndIncludes::cleanup() { ICore::self()->projectController()->closeProject( s_currentProject ); } void TestDefinesAndIncludes::loadSimpleProject() { s_currentProject = ProjectsGenerator::GenerateSimpleProject(); QVERIFY( s_currentProject ); auto manager = IDefinesAndIncludesManager::manager(); QVERIFY( manager ); const auto actualIncludes = manager->includes( s_currentProject->projectItem(), IDefinesAndIncludesManager::UserDefined ); const auto actualDefines = manager->defines( s_currentProject->projectItem(), IDefinesAndIncludesManager::UserDefined ); qDebug() << actualDefines << actualIncludes; QCOMPARE( actualIncludes, Path::List() << Path( "/usr/include/mydir") ); Defines defines; defines.insert( QStringLiteral("_DEBUG"), QString() ); defines.insert( QStringLiteral("VARIABLE"), QStringLiteral("VALUE") ); QCOMPARE( actualDefines, defines ); QVERIFY(!manager->parserArguments(s_currentProject->path().path() + "/src/main.cpp").isEmpty()); } void TestDefinesAndIncludes::loadMultiPathProject() { s_currentProject = ProjectsGenerator::GenerateMultiPathProject(); QVERIFY( s_currentProject ); auto manager = IDefinesAndIncludesManager::manager(); QVERIFY( manager ); Path::List includes = Path::List() << Path(QStringLiteral("/usr/include/otherdir")); QHash defines; defines.insert(QStringLiteral("SOURCE"), QStringLiteral("CONTENT")); defines.insert(QStringLiteral("_COPY"), QString()); QCOMPARE( manager->includes( s_currentProject->projectItem(), IDefinesAndIncludesManager::UserDefined ), includes ); QCOMPARE( manager->defines( s_currentProject->projectItem(), IDefinesAndIncludesManager::UserDefined ), defines ); ProjectBaseItem* mainfile = nullptr; - for (const auto& file: s_currentProject->fileSet() ) { - for (auto i: s_currentProject->filesForPath(file)) { + const auto& fileSet = s_currentProject->fileSet(); + for (const auto& file : fileSet) { + const auto& files = s_currentProject->filesForPath(file); + for (auto i: files) { if( i->text() == QLatin1String("main.cpp") ) { mainfile = i; break; } } } QVERIFY(mainfile); includes.prepend(Path(QStringLiteral("/usr/local/include/mydir"))); defines.insert(QStringLiteral("BUILD"), QStringLiteral("debug")); qDebug() << includes << "VS" << manager->includes( mainfile, IDefinesAndIncludesManager::UserDefined ); qDebug() << mainfile << mainfile->path(); QCOMPARE(manager->includes( mainfile, IDefinesAndIncludesManager::UserDefined ), includes); QCOMPARE(defines, manager->defines( mainfile, IDefinesAndIncludesManager::UserDefined )); QVERIFY(!manager->parserArguments(mainfile).isEmpty()); } void TestDefinesAndIncludes::testNoProjectIncludeDirectories() { s_currentProject = ProjectsGenerator::GenerateSimpleProjectWithOutOfProjectFiles(); QVERIFY(s_currentProject); auto manager = KDevelop::IDefinesAndIncludesManager::manager(); QVERIFY(manager); auto projectIncludes = manager->includes(s_currentProject->projectItem(), IDefinesAndIncludesManager::UserDefined); Path includePath1(s_currentProject->path().path() + QDir::separator() + "include1.h"); Path includePath2(s_currentProject->path().path() + QDir::separator() + "include2.h"); QVERIFY(!projectIncludes.contains(includePath1)); QVERIFY(!projectIncludes.contains(includePath2)); auto noProjectIncludes = manager->includes(s_currentProject->path().path() + "/src/main.cpp"); QVERIFY(noProjectIncludes.contains(includePath1)); QVERIFY(noProjectIncludes.contains(includePath2)); } void TestDefinesAndIncludes::testEmptyProject() { s_currentProject = ProjectsGenerator::GenerateEmptyProject(); QVERIFY(s_currentProject); auto manager = KDevelop::IDefinesAndIncludesManager::manager(); QVERIFY(manager); auto projectIncludes = manager->includes(s_currentProject->projectItem()); auto projectDefines = manager->defines(s_currentProject->projectItem()); auto parserArguments = manager->parserArguments(s_currentProject->projectItem()); QVERIFY(projectIncludes.isEmpty()); QVERIFY(projectDefines.isEmpty()); QVERIFY(parserArguments.isEmpty()); } QTEST_MAIN(TestDefinesAndIncludes) diff --git a/plugins/customscript/customscript_plugin.cpp b/plugins/customscript/customscript_plugin.cpp index d22f5556a9..061a640ada 100644 --- a/plugins/customscript/customscript_plugin.cpp +++ b/plugins/customscript/customscript_plugin.cpp @@ -1,560 +1,561 @@ /* This file is part of KDevelop Copyright (C) 2008 Cédric Pasteur Copyright (C) 2011 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. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "customscript_plugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static QPointer indentPluginSingleton; K_PLUGIN_FACTORY_WITH_JSON(CustomScriptFactory, "kdevcustomscript.json", registerPlugin(); ) // Replaces ${KEY} in command with variables[KEY] static QString replaceVariables(QString command, QMap variables) { while (command.contains(QLatin1String("${"))) { int pos = command.indexOf(QLatin1String("${")); int end = command.indexOf(QLatin1Char('}'), pos + 2); if (end == -1) { break; } QString key = command.mid(pos + 2, end - pos - 2); if (variables.contains(key)) { command.replace(pos, 1 + end - pos, variables[key]); } else { qCDebug(CUSTOMSCRIPT) << "found no variable while replacing in shell-command" << command << "key" << key << "available:" << variables; command.remove(pos, 1 + end - pos); } } return command; } CustomScriptPlugin::CustomScriptPlugin(QObject* parent, const QVariantList&) : IPlugin(QStringLiteral("kdevcustomscript"), parent) { m_currentStyle = predefinedStyles().at(0); indentPluginSingleton = this; } CustomScriptPlugin::~CustomScriptPlugin() { } QString CustomScriptPlugin::name() const { // This needs to match the X-KDE-PluginInfo-Name entry from the .desktop file! return QStringLiteral("kdevcustomscript"); } QString CustomScriptPlugin::caption() const { return QStringLiteral("Custom Script Formatter"); } QString CustomScriptPlugin::description() const { return i18n("Indent and Format Source Code.
" "This plugin allows using powerful external formatting tools " "that can be invoked through the command-line.
" "For example, the uncrustify, astyle or indent " "formatters can be used.
" "The advantage of command-line formatters is that formatting configurations " "can be easily shared by all team members, independent of their preferred IDE."); } QString CustomScriptPlugin::formatSourceWithStyle(SourceFormatterStyle style, const QString& text, const QUrl& url, const QMimeType& /*mime*/, const QString& leftContext, const QString& rightContext) const { KProcess proc; QTextStream ios(&proc); std::unique_ptr tmpFile; if (style.content().isEmpty()) { style = predefinedStyle(style.name()); if (style.content().isEmpty()) { qCWarning(CUSTOMSCRIPT) << "Empty contents for style" << style.name() << "for indent plugin"; return text; } } QString useText = text; useText = leftContext + useText + rightContext; QMap projectVariables; foreach (IProject* project, ICore::self()->projectController()->projects()) { projectVariables[project->name()] = project->path().toUrl().toLocalFile(); } QString command = style.content(); // Replace ${Project} with the project path command = replaceVariables(command, projectVariables); command.replace(QLatin1String("$FILE"), url.toLocalFile()); if (command.contains(QLatin1String("$TMPFILE"))) { tmpFile.reset(new QTemporaryFile(QDir::tempPath() + "/code")); tmpFile->setAutoRemove(false); if (tmpFile->open()) { qCDebug(CUSTOMSCRIPT) << "using temporary file" << tmpFile->fileName(); command.replace(QLatin1String("$TMPFILE"), tmpFile->fileName()); QByteArray useTextArray = useText.toLocal8Bit(); if (tmpFile->write(useTextArray) != useTextArray.size()) { qCWarning(CUSTOMSCRIPT) << "failed to write text to temporary file"; return text; } } else { qCWarning(CUSTOMSCRIPT) << "Failed to create a temporary file"; return text; } tmpFile->close(); } qCDebug(CUSTOMSCRIPT) << "using shell command for indentation: " << command; proc.setShellCommand(command); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); proc.start(); if (!proc.waitForStarted()) { qCDebug(CUSTOMSCRIPT) << "Unable to start indent" << endl; return text; } if (!tmpFile.get()) { proc.write(useText.toLocal8Bit()); } proc.closeWriteChannel(); if (!proc.waitForFinished()) { qCDebug(CUSTOMSCRIPT) << "Process doesn't finish" << endl; return text; } QString output; if (tmpFile.get()) { QFile f(tmpFile->fileName()); if (f.open(QIODevice::ReadOnly)) { output = QString::fromLocal8Bit(f.readAll()); } else { qCWarning(CUSTOMSCRIPT) << "Failed opening the temporary file for reading"; return text; } } else { output = ios.readAll(); } if (output.isEmpty()) { qCWarning(CUSTOMSCRIPT) << "indent returned empty text for style" << style.name() << style.content(); return text; } int tabWidth = 4; if ((!leftContext.isEmpty() || !rightContext.isEmpty()) && (text.contains(' ') || output.contains(' '))) { // If we have to do contex-matching with tabs, determine the correct tab-width so that the context // can be matched correctly Indentation indent = indentation(url); if (indent.indentationTabWidth > 0) { tabWidth = indent.indentationTabWidth; } } return KDevelop::extractFormattedTextFromContext(output, text, leftContext, rightContext, tabWidth); } QString CustomScriptPlugin::formatSource(const QString& text, const QUrl& url, const QMimeType& mime, const QString& leftContext, const QString& rightContext) const { auto style = KDevelop::ICore::self()->sourceFormatterController()->styleForUrl(url, mime); return formatSourceWithStyle(style, text, url, mime, leftContext, rightContext); } static QVector stylesFromLanguagePlugins() { QVector styles; foreach (auto lang, ICore::self()->languageController()->loadedLanguages()) { const SourceFormatterItemList& languageStyles = lang->sourceFormatterItems(); for (const SourceFormatterStyleItem& item: languageStyles) { if (item.engine == QLatin1String("customscript")) { styles << item.style; } } } return styles; } KDevelop::SourceFormatterStyle CustomScriptPlugin::predefinedStyle(const QString& name) const { - for (auto langStyle: stylesFromLanguagePlugins()) { + const auto& langStyles = stylesFromLanguagePlugins(); + for (auto& langStyle: langStyles) { qCDebug(CUSTOMSCRIPT) << "looking at style from language with custom sample" << langStyle.description() << langStyle.overrideSample(); if (langStyle.name() == name) { return langStyle; } } SourceFormatterStyle result(name); if (name == QLatin1String("GNU_indent_GNU")) { result.setCaption(i18n("Gnu Indent: GNU")); result.setContent(QStringLiteral("indent")); result.setUsePreview(true); } else if (name == QLatin1String("GNU_indent_KR")) { result.setCaption(i18n("Gnu Indent: Kernighan & Ritchie")); result.setContent(QStringLiteral("indent -kr")); result.setUsePreview(true); } else if (name == QLatin1String("GNU_indent_orig")) { result.setCaption(i18n("Gnu Indent: Original Berkeley indent style")); result.setContent(QStringLiteral("indent -orig")); result.setUsePreview(true); } else if (name == QLatin1String("kdev_format_source")) { result.setCaption(QStringLiteral("KDevelop: kdev_format_source")); result.setContent(QStringLiteral("kdev_format_source $FILE $TMPFILE")); result.setUsePreview(false); result.setDescription(i18n("Description:
" "kdev_format_source is a script bundled with KDevelop " "which allows using fine-grained formatting rules by placing " "meta-files called format_sources into the file-system.

" "Each line of the format_sources files defines a list of wildcards " "followed by a colon and the used formatting-command.

" "The formatting-command should use $TMPFILE to reference the " "temporary file to reformat.

" "Example:
" "*.cpp *.h : myformatter $TMPFILE
" "This will reformat all files ending with .cpp or .h using " "the custom formatting script myformatter.

" "Example:
" "subdir/* : uncrustify -l CPP -f $TMPFILE -c uncrustify.config -o $TMPFILE
" "This will reformat all files in subdirectory subdir using the uncrustify " "tool with the config-file uncrustify.config.")); } result.setMimeTypes({ {"text/x-c++src", "C++"}, {"text/x-chdr", "C"}, {"text/x-c++hdr", "C++"}, {"text/x-csrc", "C"}, {"text/x-java", "Java"}, {"text/x-csharp", "C#"} }); return result; } QVector CustomScriptPlugin::predefinedStyles() const { const QVector styles = stylesFromLanguagePlugins() + QVector{ predefinedStyle(QStringLiteral("kdev_format_source")), predefinedStyle(QStringLiteral("GNU_indent_GNU")), predefinedStyle(QStringLiteral("GNU_indent_KR")), predefinedStyle(QStringLiteral("GNU_indent_orig")), }; return styles; } KDevelop::SettingsWidget* CustomScriptPlugin::editStyleWidget(const QMimeType& mime) const { Q_UNUSED(mime); return new CustomScriptPreferences(); } static QString formattingSample() { return "// Formatting\n" "void func(){\n" "\tif(isFoo(a,b))\n" "\tbar(a,b);\n" "if(isFoo)\n" "\ta=bar((b-c)*a,*d--);\n" "if( isFoo( a,b ) )\n" "\tbar(a, b);\n" "if (isFoo) {isFoo=false;cat << isFoo <::const_iterator it = list.begin();\n" "}\n" "namespace A {\n" "namespace B {\n" "void foo() {\n" " if (true) {\n" " func();\n" " } else {\n" " // bla\n" " }\n" "}\n" "}\n" "}\n"; } static QString indentingSample() { return QLatin1String( "// Indentation\n" "#define foobar(A)\\\n" "{Foo();Bar();}\n" "#define anotherFoo(B)\\\n" "return Bar()\n" "\n" "namespace Bar\n" "{\n" "class Foo\n" "{public:\n" "Foo();\n" "virtual ~Foo();\n" "};\n" "void bar(int foo)\n" "{\n" "switch (foo)\n" "{\n" "case 1:\n" "a+=1;\n" "break;\n" "case 2:\n" "{\n" "a += 2;\n" " break;\n" "}\n" "}\n" "if (isFoo)\n" "{\n" "bar();\n" "}\n" "else\n" "{\n" "anotherBar();\n" "}\n" "}\n" "int foo()\n" "\twhile(isFoo)\n" "\t\t{\n" "\t\t\t// ...\n" "\t\t\tgoto error;\n" "\t\t/* .... */\n" "\t\terror:\n" "\t\t\t//...\n" "\t\t}\n" "\t}\n" "fooArray[]={ red,\n" "\tgreen,\n" "\tdarkblue};\n" "fooFunction(barArg1,\n" "\tbarArg2,\n" "\tbarArg3);\n" ); } QString CustomScriptPlugin::previewText(const SourceFormatterStyle& style, const QMimeType& /*mime*/) const { if (!style.overrideSample().isEmpty()) { return style.overrideSample(); } return formattingSample() + "\n\n" + indentingSample(); } QStringList CustomScriptPlugin::computeIndentationFromSample(const QUrl& url) const { QStringList ret; auto languages = ICore::self()->languageController()->languagesForUrl(url); if (languages.isEmpty()) { return ret; } QString sample = languages[0]->indentationSample(); QString formattedSample = formatSource(sample, url, QMimeDatabase().mimeTypeForUrl(url), QString(), QString()); const QStringList lines = formattedSample.split(QLatin1Char('\n')); for (const QString& line : lines) { if (!line.isEmpty() && line[0].isSpace()) { QString indent; for (const QChar c : line) { if (c.isSpace()) { indent.push_back(c); } else { break; } } if (!indent.isEmpty() && !ret.contains(indent)) { ret.push_back(indent); } } } return ret; } CustomScriptPlugin::Indentation CustomScriptPlugin::indentation(const QUrl& url) const { Indentation ret; QStringList indent = computeIndentationFromSample(url); if (indent.isEmpty()) { qCDebug(CUSTOMSCRIPT) << "failed extracting a valid indentation from sample for url" << url; return ret; // No valid indentation could be extracted } if (indent[0].contains(' ')) { ret.indentWidth = indent[0].count(' '); } if (!indent.join(QString()).contains(' ')) { ret.indentationTabWidth = -1; // Tabs are not used for indentation } if (indent[0] == QLatin1String(" ")) { // The script indents with tabs-only // The problem is that we don't know how // wide a tab is supposed to be. // // We need indentation-width=tab-width // to make the editor do tab-only formatting, // so choose a random with of 4. ret.indentWidth = 4; ret.indentationTabWidth = 4; } else if (ret.indentWidth) { // Tabs are used for indentation, alongside with spaces // Try finding out how many spaces one tab stands for. // Do it by assuming a uniform indentation-step with each level. for (int pos = 0; pos < indent.size(); ++pos) { if (indent[pos] == QLatin1String(" ")&& pos >= 1) { // This line consists of only a tab. int prevWidth = indent[pos - 1].length(); int prevPrevWidth = (pos >= 2) ? indent[pos - 2].length() : 0; int step = prevWidth - prevPrevWidth; qCDebug(CUSTOMSCRIPT) << "found in line " << pos << prevWidth << prevPrevWidth << step; if (step > 0 && step <= prevWidth) { qCDebug(CUSTOMSCRIPT) << "Done"; ret.indentationTabWidth = prevWidth + step; break; } } } } qCDebug(CUSTOMSCRIPT) << "indent-sample" << "\"" + indent.join(QLatin1Char('\n')) + "\"" << "extracted tab-width" << ret.indentationTabWidth << "extracted indentation width" << ret.indentWidth; return ret; } void CustomScriptPreferences::updateTimeout() { const QString& text = indentPluginSingleton.data()->previewText(m_style, QMimeType()); QString formatted = indentPluginSingleton.data()->formatSourceWithStyle(m_style, text, QUrl(), QMimeType()); emit previewTextChanged(formatted); } CustomScriptPreferences::CustomScriptPreferences() { m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect(m_updateTimer, &QTimer::timeout, this, &CustomScriptPreferences::updateTimeout); m_vLayout = new QVBoxLayout(this); m_vLayout->setMargin(0); m_captionLabel = new QLabel; m_vLayout->addWidget(m_captionLabel); m_vLayout->addSpacing(10); m_hLayout = new QHBoxLayout; m_vLayout->addLayout(m_hLayout); m_commandLabel = new QLabel; m_hLayout->addWidget(m_commandLabel); m_commandEdit = new QLineEdit; m_hLayout->addWidget(m_commandEdit); m_commandLabel->setText(i18n("Command:")); m_vLayout->addSpacing(10); m_bottomLabel = new QLabel; m_vLayout->addWidget(m_bottomLabel); m_bottomLabel->setTextFormat(Qt::RichText); m_bottomLabel->setText( i18n("You can enter an arbitrary shell command.
" "The unformatted source-code is reached to the command
" "through the standard input, and the
" "formatted result is read from the standard output.
" "
" "If you add $TMPFILE into the command, then
" "a temporary file is used for transferring the code.")); connect(m_commandEdit, &QLineEdit::textEdited, this, &CustomScriptPreferences::textEdited); m_vLayout->addSpacing(10); m_moreVariablesButton = new QPushButton(i18n("More Variables")); connect(m_moreVariablesButton, &QPushButton::clicked, this, &CustomScriptPreferences::moreVariablesClicked); m_vLayout->addWidget(m_moreVariablesButton); m_vLayout->addStretch(); } void CustomScriptPreferences::load(const KDevelop::SourceFormatterStyle& style) { m_style = style; m_commandEdit->setText(style.content()); m_captionLabel->setText(i18n("Style: %1", style.caption())); updateTimeout(); } QString CustomScriptPreferences::save() { return m_commandEdit->text(); } void CustomScriptPreferences::moreVariablesClicked(bool) { KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18n("$TMPFILE will be replaced with the path to a temporary file.
" "The code will be written into the file, the temporary
" "file will be substituted into that position, and the result
" "will be read out of that file.
" "
" "$FILE will be replaced with the path of the original file.
" "The contents of the file must not be modified, changes are allowed
" "only in $TMPFILE.
" "
" "${PROJECT_NAME} will be replaced by the path of
" "the currently open project with the matching name." ), i18n("Variable Replacements")); } #include "customscript_plugin.moc" // kate: indent-mode cstyle; space-indent off; tab-width 4; diff --git a/plugins/debuggercommon/mibreakpointcontroller.cpp b/plugins/debuggercommon/mibreakpointcontroller.cpp index 35876b91c1..9965c1031b 100644 --- a/plugins/debuggercommon/mibreakpointcontroller.cpp +++ b/plugins/debuggercommon/mibreakpointcontroller.cpp @@ -1,759 +1,760 @@ /* This file is part of the KDE project Copyright (C) 2002 Matthias Hoelzer-Kluepfel Copyright (C) 2002 John Firebaugh Copyright (C) 2006, 2008 Vladimir Prus Copyright (C) 2007 Hamish Rodda Copyright (C) 2009 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "mibreakpointcontroller.h" #include "debuglog.h" #include "midebugsession.h" #include "mi/micommand.h" #include "stringhelpers.h" #include #include +#include #include using namespace KDevMI; using namespace KDevMI::MI; using namespace KDevelop; struct MIBreakpointController::Handler : public MICommandHandler { Handler(MIBreakpointController* controller, const BreakpointDataPtr& b, BreakpointModel::ColumnFlags columns) : controller(controller) , breakpoint(b) , columns(columns) { breakpoint->sent |= columns; breakpoint->dirty &= ~columns; } void handle(const ResultRecord& r) override { breakpoint->sent &= ~columns; if (r.reason == QLatin1String("error")) { breakpoint->errors |= columns; int row = controller->breakpointRow(breakpoint); if (row >= 0) { controller->updateErrorText(row, r[QStringLiteral("msg")].literal()); qCWarning(DEBUGGERCOMMON) << r[QStringLiteral("msg")].literal(); } } else { if (breakpoint->errors & columns) { breakpoint->errors &= ~columns; if (breakpoint->errors) { // Since at least one error column cleared, it's possible that any remaining // error bits were collateral damage; try resending the corresponding columns // to see whether errors remain. breakpoint->dirty |= (breakpoint->errors & ~breakpoint->sent); } } } } bool handlesError() override { return true; } MIBreakpointController* controller; BreakpointDataPtr breakpoint; BreakpointModel::ColumnFlags columns; }; struct MIBreakpointController::UpdateHandler : public MIBreakpointController::Handler { UpdateHandler(MIBreakpointController* c, const BreakpointDataPtr& b, BreakpointModel::ColumnFlags columns) : Handler(c, b, columns) {} void handle(const ResultRecord &r) override { Handler::handle(r); int row = controller->breakpointRow(breakpoint); if (row >= 0) { // Note: send further updates even if we got an error; who knows: perhaps // these additional updates will "unstuck" the error condition. if (breakpoint->sent == 0 && breakpoint->dirty != 0) { controller->sendUpdates(row); } controller->recalculateState(row); } } }; struct MIBreakpointController::InsertedHandler : public MIBreakpointController::Handler { InsertedHandler(MIBreakpointController* c, const BreakpointDataPtr& b, BreakpointModel::ColumnFlags columns) : Handler(c, b, columns) {} void handle(const ResultRecord &r) override { Handler::handle(r); int row = controller->breakpointRow(breakpoint); if (r.reason != QLatin1String("error")) { QString bkptKind; for (auto kind : {"bkpt", "wpt", "hw-rwpt", "hw-awpt"}) { if (r.hasField(kind)) { bkptKind = kind; break; } } if (bkptKind.isEmpty()) { qCWarning(DEBUGGERCOMMON) << "Gdb sent unknown breakpoint kind"; return; } const Value& miBkpt = r[bkptKind]; breakpoint->debuggerId = miBkpt[QStringLiteral("number")].toInt(); if (row >= 0) { controller->updateFromDebugger(row, miBkpt); if (breakpoint->dirty != 0) controller->sendUpdates(row); } else { // breakpoint was deleted while insertion was in flight controller->debugSession()->addCommand(BreakDelete, QString::number(breakpoint->debuggerId), CmdImmediately); } } if (row >= 0) { controller->recalculateState(row); } } }; struct MIBreakpointController::DeleteHandler : MIBreakpointController::Handler { DeleteHandler(MIBreakpointController* c, const BreakpointDataPtr& b) : Handler(c, b, BreakpointModel::ColumnFlags()) {} void handle(const ResultRecord&) override { controller->m_pendingDeleted.removeAll(breakpoint); } }; struct MIBreakpointController::IgnoreChanges { explicit IgnoreChanges(MIBreakpointController& controller) : controller(controller) { ++controller.m_ignoreChanges; } ~IgnoreChanges() { --controller.m_ignoreChanges; } MIBreakpointController& controller; }; MIBreakpointController::MIBreakpointController(MIDebugSession * parent) : IBreakpointController(parent) { Q_ASSERT(parent); connect(parent, &MIDebugSession::inferiorStopped, this, &MIBreakpointController::programStopped); int numBreakpoints = breakpointModel()->breakpoints().size(); for (int row = 0; row < numBreakpoints; ++row) breakpointAdded(row); } MIDebugSession *MIBreakpointController::debugSession() const { Q_ASSERT(QObject::parent()); return static_cast(const_cast(QObject::parent())); } int MIBreakpointController::breakpointRow(const BreakpointDataPtr& breakpoint) { return m_breakpoints.indexOf(breakpoint); } void MIBreakpointController::setDeleteDuplicateBreakpoints(bool enable) { m_deleteDuplicateBreakpoints = enable; } void MIBreakpointController::initSendBreakpoints() { for (int row = 0; row < m_breakpoints.size(); ++row) { BreakpointDataPtr breakpoint = m_breakpoints[row]; if (breakpoint->debuggerId < 0 && breakpoint->sent == 0) { createBreakpoint(row); } } } void MIBreakpointController::breakpointAdded(int row) { if (m_ignoreChanges > 0) return; auto breakpoint = BreakpointDataPtr::create(); m_breakpoints.insert(row, breakpoint); const Breakpoint* modelBreakpoint = breakpointModel()->breakpoint(row); if (!modelBreakpoint->enabled()) breakpoint->dirty |= BreakpointModel::EnableColumnFlag; if (!modelBreakpoint->condition().isEmpty()) breakpoint->dirty |= BreakpointModel::ConditionColumnFlag; if (modelBreakpoint->ignoreHits() != 0) breakpoint->dirty |= BreakpointModel::IgnoreHitsColumnFlag; if (!modelBreakpoint->address().isEmpty()) breakpoint->dirty |= BreakpointModel::LocationColumnFlag; createBreakpoint(row); } void MIBreakpointController::breakpointModelChanged(int row, BreakpointModel::ColumnFlags columns) { if (m_ignoreChanges > 0) return; BreakpointDataPtr breakpoint = m_breakpoints.at(row); breakpoint->dirty |= columns & (BreakpointModel::EnableColumnFlag | BreakpointModel::LocationColumnFlag | BreakpointModel::ConditionColumnFlag | BreakpointModel::IgnoreHitsColumnFlag); if (breakpoint->sent != 0) { // Throttle the amount of commands we send to GDB; the response handler of currently // in-flight commands will take care of sending the update. // This also prevents us from sending updates while a break-create command is in-flight. return; } if (breakpoint->debuggerId < 0) { createBreakpoint(row); } else { sendUpdates(row); } } void MIBreakpointController::breakpointAboutToBeDeleted(int row) { if (m_ignoreChanges > 0) return; BreakpointDataPtr breakpoint = m_breakpoints.at(row); m_breakpoints.removeAt(row); if (breakpoint->debuggerId < 0) { // Two possibilities: // (1) Breakpoint has never been sent to GDB, so we're done // (2) Breakpoint has been sent to GDB, but we haven't received // the response yet; the response handler will delete the // breakpoint. return; } if (debugSession()->debuggerStateIsOn(s_dbgNotStarted)) return; debugSession()->addCommand( BreakDelete, QString::number(breakpoint->debuggerId), new DeleteHandler(this, breakpoint), CmdImmediately); m_pendingDeleted << breakpoint; } // Note: despite the name, this is in fact session state changed. void MIBreakpointController::debuggerStateChanged(IDebugSession::DebuggerState state) { IgnoreChanges ignoreChanges(*this); if (state == IDebugSession::EndedState || state == IDebugSession::NotStartedState) { for (int row = 0; row < m_breakpoints.size(); ++row) { updateState(row, Breakpoint::NotStartedState); } } else if (state == IDebugSession::StartingState) { for (int row = 0; row < m_breakpoints.size(); ++row) { updateState(row, Breakpoint::DirtyState); } } } void MIBreakpointController::createBreakpoint(int row) { if (debugSession()->debuggerStateIsOn(s_dbgNotStarted)) return; BreakpointDataPtr breakpoint = m_breakpoints.at(row); Breakpoint* modelBreakpoint = breakpointModel()->breakpoint(row); Q_ASSERT(breakpoint->debuggerId < 0 && breakpoint->sent == 0); if (modelBreakpoint->location().isEmpty()) return; if (modelBreakpoint->kind() == Breakpoint::CodeBreakpoint) { QString location; if (modelBreakpoint->line() != -1) { location = QStringLiteral("%0:%1") .arg(modelBreakpoint->url().url(QUrl::PreferLocalFile | QUrl::StripTrailingSlash)) .arg(modelBreakpoint->line() + 1); } else { location = modelBreakpoint->location(); } if (location == QLatin1String("catch throw")) { location = QStringLiteral("exception throw"); } // Note: We rely on '-f' to be automatically added by the MICommand logic QString arguments; if (!modelBreakpoint->enabled()) arguments += QLatin1String("-d "); if (!modelBreakpoint->condition().isEmpty()) arguments += QStringLiteral("-c %0 ").arg(Utils::quoteExpression(modelBreakpoint->condition())); if (modelBreakpoint->ignoreHits() != 0) arguments += QStringLiteral("-i %0 ").arg(modelBreakpoint->ignoreHits()); arguments += Utils::quoteExpression(location); BreakpointModel::ColumnFlags sent = BreakpointModel::EnableColumnFlag | BreakpointModel::ConditionColumnFlag | BreakpointModel::IgnoreHitsColumnFlag | BreakpointModel::LocationColumnFlag; debugSession()->addCommand(BreakInsert, arguments, new InsertedHandler(this, breakpoint, sent), CmdImmediately); } else { QString opt; if (modelBreakpoint->kind() == Breakpoint::ReadBreakpoint) opt = QStringLiteral("-r "); else if (modelBreakpoint->kind() == Breakpoint::AccessBreakpoint) opt = QStringLiteral("-a "); debugSession()->addCommand(BreakWatch, opt + Utils::quoteExpression(modelBreakpoint->location()), new InsertedHandler(this, breakpoint, BreakpointModel::LocationColumnFlag), CmdImmediately); } recalculateState(row); } void MIBreakpointController::sendUpdates(int row) { if (debugSession()->debuggerStateIsOn(s_dbgNotStarted)) return; BreakpointDataPtr breakpoint = m_breakpoints.at(row); Breakpoint* modelBreakpoint = breakpointModel()->breakpoint(row); Q_ASSERT(breakpoint->debuggerId >= 0 && breakpoint->sent == 0); if (breakpoint->dirty & BreakpointModel::LocationColumnFlag) { // Gdb considers locations as fixed, so delete and re-create the breakpoint debugSession()->addCommand(BreakDelete, QString::number(breakpoint->debuggerId), CmdImmediately); breakpoint->debuggerId = -1; createBreakpoint(row); return; } if (breakpoint->dirty & BreakpointModel::EnableColumnFlag) { debugSession()->addCommand(modelBreakpoint->enabled() ? BreakEnable : BreakDisable, QString::number(breakpoint->debuggerId), new UpdateHandler(this, breakpoint, BreakpointModel::EnableColumnFlag), CmdImmediately); } if (breakpoint->dirty & BreakpointModel::IgnoreHitsColumnFlag) { debugSession()->addCommand(BreakAfter, QStringLiteral("%0 %1").arg(breakpoint->debuggerId) .arg(modelBreakpoint->ignoreHits()), new UpdateHandler(this, breakpoint, BreakpointModel::IgnoreHitsColumnFlag), CmdImmediately); } if (breakpoint->dirty & BreakpointModel::ConditionColumnFlag) { debugSession()->addCommand(BreakCondition, QStringLiteral("%0 %1").arg(breakpoint->debuggerId) .arg(modelBreakpoint->condition()), new UpdateHandler(this, breakpoint, BreakpointModel::ConditionColumnFlag), CmdImmediately); } recalculateState(row); } void MIBreakpointController::recalculateState(int row) { BreakpointDataPtr breakpoint = m_breakpoints.at(row); if (breakpoint->errors == 0) updateErrorText(row, QString()); Breakpoint::BreakpointState newState = Breakpoint::NotStartedState; if (debugSession()->state() != IDebugSession::EndedState && debugSession()->state() != IDebugSession::NotStartedState) { if (!debugSession()->debuggerStateIsOn(s_dbgNotStarted)) { if (breakpoint->dirty == 0 && breakpoint->sent == 0) { if (breakpoint->pending) { newState = Breakpoint::PendingState; } else { newState = Breakpoint::CleanState; } } else { newState = Breakpoint::DirtyState; } } } updateState(row, newState); } int MIBreakpointController::rowFromDebuggerId(int gdbId) const { for (int row = 0; row < m_breakpoints.size(); ++row) { if (gdbId == m_breakpoints[row]->debuggerId) return row; } return -1; } void MIBreakpointController::notifyBreakpointCreated(const AsyncRecord& r) { const Value& miBkpt = r[QStringLiteral("bkpt")]; // Breakpoints with multiple locations are represented by a parent breakpoint (e.g. 1) // and multiple child breakpoints (e.g. 1.1, 1.2, 1.3, ...). // We ignore the child breakpoints here in the current implementation; this can lead to dubious // results in the UI when breakpoints are marked in document views (e.g. when a breakpoint // applies to multiple overloads of a C++ function simultaneously) and in disassembly // (e.g. when a breakpoint is set in an inlined functions). if (miBkpt[QStringLiteral("number")].literal().contains('.')) return; createFromDebugger(miBkpt); } void MIBreakpointController::notifyBreakpointModified(const AsyncRecord& r) { const Value& miBkpt = r[QStringLiteral("bkpt")]; const int gdbId = miBkpt[QStringLiteral("number")].toInt(); const int row = rowFromDebuggerId(gdbId); if (row < 0) { - for (const auto& breakpoint : m_pendingDeleted) { + for (const auto& breakpoint : qAsConst(m_pendingDeleted)) { if (breakpoint->debuggerId == gdbId) { // Received a modification of a breakpoint whose deletion is currently // in-flight; simply ignore it. return; } } qCWarning(DEBUGGERCOMMON) << "Received a modification of an unknown breakpoint"; createFromDebugger(miBkpt); } else { updateFromDebugger(row, miBkpt); } } void MIBreakpointController::notifyBreakpointDeleted(const AsyncRecord& r) { const int gdbId = r[QStringLiteral("id")].toInt(); const int row = rowFromDebuggerId(gdbId); if (row < 0) { // The user may also have deleted the breakpoint via the UI simultaneously return; } IgnoreChanges ignoreChanges(*this); breakpointModel()->removeRow(row); m_breakpoints.removeAt(row); } void MIBreakpointController::createFromDebugger(const Value& miBkpt) { const QString type = miBkpt[QStringLiteral("type")].literal(); Breakpoint::BreakpointKind gdbKind; if (type == QLatin1String("breakpoint")) { gdbKind = Breakpoint::CodeBreakpoint; } else if (type == QLatin1String("watchpoint") || type == QLatin1String("hw watchpoint")) { gdbKind = Breakpoint::WriteBreakpoint; } else if (type == QLatin1String("read watchpoint")) { gdbKind = Breakpoint::ReadBreakpoint; } else if (type == QLatin1String("acc watchpoint")) { gdbKind = Breakpoint::AccessBreakpoint; } else { qCWarning(DEBUGGERCOMMON) << "Unknown breakpoint type " << type; return; } // During debugger startup, we want to avoid creating duplicate breakpoints when the same breakpoint // appears both in our model and in a init file e.g. .gdbinit BreakpointModel* model = breakpointModel(); const int numRows = model->rowCount(); for (int row = 0; row < numRows; ++row) { BreakpointDataPtr breakpoint = m_breakpoints.at(row); const bool breakpointSent = breakpoint->debuggerId >= 0 || breakpoint->sent != 0; if (breakpointSent && !m_deleteDuplicateBreakpoints) continue; Breakpoint* modelBreakpoint = model->breakpoint(row); if (modelBreakpoint->kind() != gdbKind) continue; if (gdbKind == Breakpoint::CodeBreakpoint) { bool sameLocation = false; if (miBkpt.hasField(QStringLiteral("fullname")) && miBkpt.hasField(QStringLiteral("line"))) { const QString location = Utils::unquoteExpression(miBkpt[QStringLiteral("fullname")].literal()); const int line = miBkpt[QStringLiteral("line")].toInt() - 1; if (location == modelBreakpoint->url().url(QUrl::PreferLocalFile | QUrl::StripTrailingSlash) && line == modelBreakpoint->line()) { sameLocation = true; } } if (!sameLocation && miBkpt.hasField(QStringLiteral("original-location"))) { const QString location = miBkpt[QStringLiteral("original-location")].literal(); if (location == modelBreakpoint->location()) { sameLocation = true; } else { QRegExp rx("^(.+):(\\d+)$"); if (rx.indexIn(location) != -1 && Utils::unquoteExpression(rx.cap(1)) == modelBreakpoint->url().url(QUrl::PreferLocalFile | QUrl::StripTrailingSlash) && rx.cap(2).toInt() - 1 == modelBreakpoint->line()) { sameLocation = true; } } } if (!sameLocation && miBkpt.hasField(QStringLiteral("what")) && miBkpt[QStringLiteral("what")].literal() == QLatin1String("exception throw")) { if (modelBreakpoint->expression() == QLatin1String("catch throw") || modelBreakpoint->expression() == QLatin1String("exception throw")) { sameLocation = true; } } if (!sameLocation) continue; } else { if (Utils::unquoteExpression(miBkpt[QStringLiteral("original-location")].literal()) != modelBreakpoint->expression()) { continue; } } QString condition; if (miBkpt.hasField(QStringLiteral("cond"))) { condition = miBkpt[QStringLiteral("cond")].literal(); } if (condition != modelBreakpoint->condition()) continue; // Breakpoint is equivalent if (!breakpointSent) { breakpoint->debuggerId = miBkpt[QStringLiteral("number")].toInt(); // Reasonable people can probably have different opinions about what the "correct" behavior // should be for the "enabled" and "ignore hits" column. // Here, we let the status in KDevelop's UI take precedence, which we suspect to be // marginally more useful. Dirty data will be sent during the initial sending of the // breakpoint list. const bool gdbEnabled = miBkpt[QStringLiteral("enabled")].literal() == QLatin1String("y"); if (gdbEnabled != modelBreakpoint->enabled()) breakpoint->dirty |= BreakpointModel::EnableColumnFlag; int gdbIgnoreHits = 0; if (miBkpt.hasField(QStringLiteral("ignore"))) gdbIgnoreHits = miBkpt[QStringLiteral("ignore")].toInt(); if (gdbIgnoreHits != modelBreakpoint->ignoreHits()) breakpoint->dirty |= BreakpointModel::IgnoreHitsColumnFlag; updateFromDebugger(row, miBkpt, BreakpointModel::EnableColumnFlag | BreakpointModel::IgnoreHitsColumnFlag); return; } // Breakpoint from the model has already been sent, but we want to delete duplicates // It is not entirely clear _which_ breakpoint ought to be deleted, and reasonable people // may have different opinions. // We suspect that it is marginally more useful to delete the existing model breakpoint; // after all, this only happens when a user command creates a breakpoint, and perhaps the // user intends to modify the breakpoint they created manually. In any case, // this situation should only happen rarely (in particular, when a user sets a breakpoint // inside the remote run script). model->removeRows(row, 1); break; // fall through to pick up the manually created breakpoint in the model } // No equivalent breakpoint found, or we have one but want to be consistent with GDB's // behavior of allowing multiple equivalent breakpoint. IgnoreChanges ignoreChanges(*this); const int row = m_breakpoints.size(); Q_ASSERT(row == model->rowCount()); switch (gdbKind) { case Breakpoint::WriteBreakpoint: model->addWatchpoint(); break; case Breakpoint::ReadBreakpoint: model->addReadWatchpoint(); break; case Breakpoint::AccessBreakpoint: model->addAccessWatchpoint(); break; case Breakpoint::CodeBreakpoint: model->addCodeBreakpoint(); break; default: Q_ASSERT(false); return; } // Since we are in ignore-changes mode, we have to add the BreakpointData manually. auto breakpoint = BreakpointDataPtr::create(); m_breakpoints << breakpoint; breakpoint->debuggerId = miBkpt[QStringLiteral("number")].toInt(); updateFromDebugger(row, miBkpt); } // This method is required for the legacy interface which will be removed void MIBreakpointController::sendMaybe(KDevelop::Breakpoint*) { Q_ASSERT(false); } void MIBreakpointController::updateFromDebugger(int row, const Value& miBkpt, BreakpointModel::ColumnFlags lockedColumns) { IgnoreChanges ignoreChanges(*this); BreakpointDataPtr breakpoint = m_breakpoints[row]; Breakpoint* modelBreakpoint = breakpointModel()->breakpoint(row); // Commands that are currently in flight will overwrite the modification we have received, // so do not update the corresponding data lockedColumns |= breakpoint->sent | breakpoint->dirty; // TODO: // Gdb has a notion of "original-location", which is the "expression" or "location" used // to set the breakpoint, and notions of the actual location of the breakpoint (function name, // address, source file and line). The breakpoint model currently does not map well to this // (though it arguably should), and does not support multi-location breakpoints at all. // We try to do the best we can until the breakpoint model gets cleaned up. if (miBkpt.hasField(QStringLiteral("fullname")) && miBkpt.hasField(QStringLiteral("line"))) { modelBreakpoint->setLocation( QUrl::fromLocalFile(Utils::unquoteExpression(miBkpt[QStringLiteral("fullname")].literal())), miBkpt[QStringLiteral("line")].toInt() - 1); } else if (miBkpt.hasField(QStringLiteral("original-location"))) { QRegExp rx("^(.+):(\\d+)$"); QString location = miBkpt[QStringLiteral("original-location")].literal(); if (rx.indexIn(location) != -1) { modelBreakpoint->setLocation(QUrl::fromLocalFile(Utils::unquoteExpression(rx.cap(1))), rx.cap(2).toInt()-1); } else { modelBreakpoint->setData(Breakpoint::LocationColumn, Utils::unquoteExpression(location)); } } else if (miBkpt.hasField(QStringLiteral("what"))) { modelBreakpoint->setExpression(miBkpt[QStringLiteral("what")].literal()); } else { qCWarning(DEBUGGERCOMMON) << "Breakpoint doesn't contain required location/expression data"; } if (!(lockedColumns & BreakpointModel::EnableColumnFlag)) { bool enabled = true; if (miBkpt.hasField(QStringLiteral("enabled"))) { if (miBkpt[QStringLiteral("enabled")].literal() == QLatin1String("n")) enabled = false; } modelBreakpoint->setData(Breakpoint::EnableColumn, enabled ? Qt::Checked : Qt::Unchecked); breakpoint->dirty &= ~BreakpointModel::EnableColumnFlag; } if (!(lockedColumns & BreakpointModel::ConditionColumnFlag)) { QString condition; if (miBkpt.hasField(QStringLiteral("cond"))) { condition = miBkpt[QStringLiteral("cond")].literal(); } modelBreakpoint->setCondition(condition); breakpoint->dirty &= ~BreakpointModel::ConditionColumnFlag; } if (!(lockedColumns & BreakpointModel::IgnoreHitsColumnFlag)) { int ignoreHits = 0; if (miBkpt.hasField(QStringLiteral("ignore"))) { ignoreHits = miBkpt[QStringLiteral("ignore")].toInt(); } modelBreakpoint->setIgnoreHits(ignoreHits); breakpoint->dirty &= ~BreakpointModel::IgnoreHitsColumnFlag; } breakpoint->pending = false; if (miBkpt.hasField(QStringLiteral("addr")) && miBkpt[QStringLiteral("addr")].literal() == QLatin1String("")) { breakpoint->pending = true; } int hitCount = 0; if (miBkpt.hasField(QStringLiteral("times"))) { hitCount = miBkpt[QStringLiteral("times")].toInt(); } updateHitCount(row, hitCount); recalculateState(row); } void MIBreakpointController::programStopped(const AsyncRecord& r) { if (!r.hasField(QStringLiteral("reason"))) return; const QString reason = r[QStringLiteral("reason")].literal(); int debuggerId = -1; if (reason == QLatin1String("breakpoint-hit")) { debuggerId = r[QStringLiteral("bkptno")].toInt(); } else if (reason == QLatin1String("watchpoint-trigger")) { debuggerId = r[QStringLiteral("wpt")][QStringLiteral("number")].toInt(); } else if (reason == QLatin1String("read-watchpoint-trigger")) { debuggerId = r[QStringLiteral("hw-rwpt")][QStringLiteral("number")].toInt(); } else if (reason == QLatin1String("access-watchpoint-trigger")) { debuggerId = r[QStringLiteral("hw-awpt")][QStringLiteral("number")].toInt(); } if (debuggerId < 0) return; int row = rowFromDebuggerId(debuggerId); if (row < 0) return; QString msg; if (r.hasField(QStringLiteral("value"))) { if (r[QStringLiteral("value")].hasField(QStringLiteral("old"))) { msg += i18n("
Old value: %1", r["value"]["old"].literal()); } if (r[QStringLiteral("value")].hasField(QStringLiteral("new"))) { msg += i18n("
New value: %1", r["value"]["new"].literal()); } } notifyHit(row, msg); } diff --git a/plugins/debuggercommon/midebuggerplugin.cpp b/plugins/debuggercommon/midebuggerplugin.cpp index 47240ee7af..81e37cf224 100644 --- a/plugins/debuggercommon/midebuggerplugin.cpp +++ b/plugins/debuggercommon/midebuggerplugin.cpp @@ -1,310 +1,312 @@ /* * Common code for MI debugger support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2016 Aetf * * 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 "midebuggerplugin.h" #include "midebugjobs.h" #include "dialogs/processselection.h" #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; class KDevMI::DBusProxy : public QObject { Q_OBJECT public: DBusProxy(const QString& service, const QString& name, QObject* parent) : QObject(parent), m_dbusInterface(service, QStringLiteral("/debugger")), m_name(name), m_valid(true) {} ~DBusProxy() override { if (m_valid) { m_dbusInterface.call(QStringLiteral("debuggerClosed"), m_name); } } QDBusInterface* interface() { return &m_dbusInterface; } void Invalidate() { m_valid = false; } public Q_SLOTS: void debuggerAccepted(const QString& name) { if (name == m_name) { emit debugProcess(this); } } void debuggingFinished() { m_dbusInterface.call(QStringLiteral("debuggingFinished"), m_name); } Q_SIGNALS: void debugProcess(DBusProxy*); private: QDBusInterface m_dbusInterface; QString m_name; bool m_valid; }; const QString drkonqiservice = QLatin1String("org.kde.drkonqi"); MIDebuggerPlugin::MIDebuggerPlugin(const QString &componentName, const QString& displayName, QObject *parent) : KDevelop::IPlugin(componentName, parent), m_displayName(displayName) { core()->debugController()->initializeUi(); setupActions(); setupDBus(); } void MIDebuggerPlugin::setupActions() { KActionCollection* ac = actionCollection(); QAction * action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("core"))); action->setText(i18n("Examine Core File with %1", m_displayName)); action->setWhatsThis(i18n("Examine core file" "

This loads a core file, which is typically created " "after the application has crashed, e.g. with a " "segmentation fault. The core file contains an " "image of the program memory at the time it crashed, " "allowing you to do a post-mortem analysis.

")); connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotExamineCore); ac->addAction(QStringLiteral("debug_core"), action); #if KF5SysGuard_FOUND action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("connect_creating"))); action->setText(i18n("Attach to Process with %1", m_displayName)); action->setWhatsThis(i18n("Attach to process" "

Attaches the debugger to a running process.

")); connect(action, &QAction::triggered, this, &MIDebuggerPlugin::slotAttachProcess); ac->addAction(QStringLiteral("debug_attach"), action); #endif } void MIDebuggerPlugin::setupDBus() { QDBusConnectionInterface* dbusInterface = QDBusConnection::sessionBus().interface(); - for (const auto &service : dbusInterface->registeredServiceNames().value()) { + const auto& registeredServiceNames = dbusInterface->registeredServiceNames().value(); + for (const auto& service : registeredServiceNames) { slotDBusOwnerChanged(service, QString(), QString('n')); } connect(dbusInterface, &QDBusConnectionInterface::serviceOwnerChanged, this, &MIDebuggerPlugin::slotDBusOwnerChanged); } void MIDebuggerPlugin::unload() { unloadToolViews(); } MIDebuggerPlugin::~MIDebuggerPlugin() { } void MIDebuggerPlugin::slotDBusOwnerChanged(const QString& service, const QString& oldOwner, const QString& newOwner) { if (oldOwner.isEmpty() && service.startsWith(drkonqiservice)) { if (m_drkonqis.contains(service)) { return; } // New registration const QString name = i18n("KDevelop (%1) - %2", m_displayName, core()->activeSession()->name()); auto drkonqiProxy = new DBusProxy(service, name, this); m_drkonqis.insert(service, drkonqiProxy); connect(drkonqiProxy->interface(), SIGNAL(acceptDebuggingApplication(QString)), drkonqiProxy, SLOT(debuggerAccepted(QString))); connect(drkonqiProxy, &DBusProxy::debugProcess, this, &MIDebuggerPlugin::slotDebugExternalProcess); drkonqiProxy->interface()->call(QStringLiteral("registerDebuggingApplication"), name); } else if (newOwner.isEmpty() && service.startsWith(drkonqiservice)) { // Deregistration if (m_drkonqis.contains(service)) { auto proxy = m_drkonqis.take(service); proxy->Invalidate(); delete proxy; } } } void MIDebuggerPlugin::slotDebugExternalProcess(DBusProxy* proxy) { QDBusReply reply = proxy->interface()->call(QStringLiteral("pid")); if (reply.isValid()) { connect(attachProcess(reply.value()), &KJob::result, proxy, &DBusProxy::debuggingFinished); } core()->uiController()->activeMainWindow()->raise(); } ContextMenuExtension MIDebuggerPlugin::contextMenuExtension(Context* context, QWidget* parent) { ContextMenuExtension menuExt = IPlugin::contextMenuExtension(context, parent); if (context->type() != KDevelop::Context::EditorContext) return menuExt; EditorContext *econtext = dynamic_cast(context); if (!econtext) return menuExt; QString contextIdent = econtext->currentWord(); if (!contextIdent.isEmpty()) { QString squeezed = KStringHandler::csqueeze(contextIdent, 30); QAction* action = new QAction(parent); action->setText(i18n("Evaluate: %1", squeezed)); action->setWhatsThis(i18n("Evaluate expression" "

Shows the value of the expression under the cursor.

")); connect(action, &QAction::triggered, this, [this, contextIdent](){ emit addWatchVariable(contextIdent); }); menuExt.addAction(ContextMenuExtension::DebugGroup, action); action = new QAction(parent); action->setText(i18n("Watch: %1", squeezed)); action->setWhatsThis(i18n("Watch expression" "

Adds the expression under the cursor to the Variables/Watch list.

")); connect(action, &QAction::triggered, this, [this, contextIdent](){ emit evaluateExpression(contextIdent); }); menuExt.addAction(ContextMenuExtension::DebugGroup, action); } return menuExt; } void MIDebuggerPlugin::slotExamineCore() { showStatusMessage(i18n("Choose a core file to examine..."), 1000); if (core()->debugController()->currentSession() != nullptr) { KMessageBox::ButtonCode answer = KMessageBox::warningYesNo( core()->uiController()->activeMainWindow(), i18n("A program is already being debugged. Do you want to abort the " "currently running debug session and continue?")); if (answer == KMessageBox::No) return; } MIExamineCoreJob *job = new MIExamineCoreJob(this, core()->runController()); core()->runController()->registerJob(job); // job->start() is called in registerJob } #if KF5SysGuard_FOUND void MIDebuggerPlugin::slotAttachProcess() { showStatusMessage(i18n("Choose a process to attach to..."), 1000); if (core()->debugController()->currentSession() != nullptr) { KMessageBox::ButtonCode answer = KMessageBox::warningYesNo( core()->uiController()->activeMainWindow(), i18n("A program is already being debugged. Do you want to abort the " "currently running debug session and continue?")); if (answer == KMessageBox::No) return; } QPointer dlg = new ProcessSelectionDialog(core()->uiController()->activeMainWindow()); if (!dlg->exec() || !dlg->pidSelected()) { delete dlg; return; } // TODO: move check into process selection dialog int pid = dlg->pidSelected(); delete dlg; if (QApplication::applicationPid() == pid) KMessageBox::error(core()->uiController()->activeMainWindow(), i18n("Not attaching to process %1: cannot attach the debugger to itself.", pid)); else attachProcess(pid); } #endif MIAttachProcessJob* MIDebuggerPlugin::attachProcess(int pid) { MIAttachProcessJob *job = new MIAttachProcessJob(this, pid, core()->runController()); core()->runController()->registerJob(job); // job->start() is called in registerJob return job; } QString MIDebuggerPlugin::statusName() const { return i18n("Debugger"); } void MIDebuggerPlugin::showStatusMessage(const QString& msg, int timeout) { emit showMessage(this, msg, timeout); } #include "midebuggerplugin.moc" diff --git a/plugins/docker/dockerplugin.cpp b/plugins/docker/dockerplugin.cpp index b747648537..9a40e70490 100644 --- a/plugins/docker/dockerplugin.cpp +++ b/plugins/docker/dockerplugin.cpp @@ -1,169 +1,170 @@ /* Copyright 2017 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dockerplugin.h" #include "dockerruntime.h" #include "dockerpreferences.h" #include "dockerpreferencessettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KDevDockerFactory, "kdevdocker.json", registerPlugin();) using namespace KDevelop; DockerPlugin::DockerPlugin(QObject *parent, const QVariantList & /*args*/) : KDevelop::IPlugin( QStringLiteral("kdevdocker"), parent ) , m_settings(new DockerPreferencesSettings) { runtimeChanged(ICore::self()->runtimeController()->currentRuntime()); setXMLFile( QStringLiteral("kdevdockerplugin.rc") ); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &DockerPlugin::runtimeChanged); QProcess* process = new QProcess(this); connect(process, static_cast(&QProcess::finished), this, &DockerPlugin::imagesListFinished); process->start(QStringLiteral("docker"), {QStringLiteral("images"), QStringLiteral("--filter"), QStringLiteral("dangling=false"), QStringLiteral("--format"), QStringLiteral("{{.Repository}}:{{.Tag}}\t{{.ID}}")}, QIODevice::ReadOnly); DockerRuntime::s_settings = m_settings.data(); } DockerPlugin::~DockerPlugin() { DockerRuntime::s_settings = nullptr; } void DockerPlugin::imagesListFinished(int code) { if (code != 0) return; QProcess* process = qobject_cast(sender()); Q_ASSERT(process); QTextStream stream(process); while(!stream.atEnd()) { const QString line = stream.readLine(); const QStringList parts = line.split(QLatin1Char('\t')); const QString tag = parts[0] == QLatin1String("") ? parts[1] : parts[0]; ICore::self()->runtimeController()->addRuntimes(new DockerRuntime(tag)); } process->deleteLater(); Q_EMIT imagesListed(); } void DockerPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime) { const bool isDocker = qobject_cast(newRuntime); - for(auto action: actionCollection()->actions()) { + const auto& actions = actionCollection()->actions(); + for (auto action: actions) { action->setEnabled(isDocker); } } KDevelop::ContextMenuExtension DockerPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { QList urls; if ( context->type() == KDevelop::Context::FileContext ) { KDevelop::FileContext* filectx = static_cast(context); urls = filectx->urls(); } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { KDevelop::ProjectItemContext* projctx = static_cast(context); foreach( KDevelop::ProjectBaseItem* item, projctx->items() ) { if ( item->file() ) { urls << item->path().toUrl(); } } } for(auto it = urls.begin(); it != urls.end(); ) { if (it->isLocalFile() && it->fileName() == QLatin1String("Dockerfile")) { ++it; } else { it = urls.erase(it); } } if ( !urls.isEmpty() ) { KDevelop::ContextMenuExtension ext; foreach(const QUrl &url, urls) { const KDevelop::Path file(url); auto action = new QAction(QIcon::fromTheme("text-dockerfile"), i18n("docker build '%1'", file.path()), parent); connect(action, &QAction::triggered, this, [this, file]() { const auto dir = file.parent(); const QString name = QInputDialog::getText( ICore::self()->uiController()->activeMainWindow(), i18n("Choose tag name..."), i18n("Tag name for '%1'", file.path()), QLineEdit::Normal, dir.lastPathSegment() ); auto process = new OutputExecuteJob; process->setExecuteOnHost(true); process->setProperties(OutputExecuteJob::DisplayStdout | OutputExecuteJob::DisplayStderr); *process << QStringList{"docker", "build", "--tag", name, dir.toLocalFile()}; connect(process, &KJob::finished, this, [name] (KJob* job) { if (job->error() != 0) return; ICore::self()->runtimeController()->addRuntimes(new DockerRuntime(name)); }); process->start(); }); ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action); } return ext; } return KDevelop::IPlugin::contextMenuExtension(context, parent); } int DockerPlugin::configPages() const { return 1; } KDevelop::ConfigPage* DockerPlugin::configPage(int number, QWidget* parent) { if (number == 0) { return new DockerPreferences(this, m_settings.data(), parent); } return nullptr; } #include "dockerplugin.moc" diff --git a/plugins/docker/dockerruntime.cpp b/plugins/docker/dockerruntime.cpp index 8651689215..82485e172c 100644 --- a/plugins/docker/dockerruntime.cpp +++ b/plugins/docker/dockerruntime.cpp @@ -1,248 +1,250 @@ /* Copyright 2017 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dockerruntime.h" #include "dockerpreferencessettings.h" #include "debug_docker.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; DockerPreferencesSettings* DockerRuntime::s_settings = nullptr; DockerRuntime::DockerRuntime(const QString &tag) : KDevelop::IRuntime() , m_tag(tag) { setObjectName(tag); } void DockerRuntime::inspectContainer() { QProcess* process = new QProcess(this); connect(process, static_cast(&QProcess::finished), this, [process, this](int code, QProcess::ExitStatus status){ process->deleteLater(); qCDebug(DOCKER) << "inspect container" << code << status; if (code || status) { qCWarning(DOCKER) << "Could not figure out the container" << m_container; return; } const QJsonArray arr = QJsonDocument::fromJson(process->readAll()).array(); const QJsonObject obj = arr.constBegin()->toObject(); const QJsonObject objGraphDriverData = obj.value("GraphDriver").toObject().value("Data").toObject(); m_mergedDir = Path(objGraphDriverData.value("MergedDir").toString()); qCDebug(DOCKER) << "mergeddir:" << m_container << m_mergedDir; - for (auto entry : obj["Config"].toObject()["Env"].toArray()) { + const auto& entries = obj["Config"].toObject()["Env"].toArray(); + for (auto entry : entries) { const auto content = entry.toString().split('='); if (content.count() != 2) continue; m_envs.insert(content[0].toLocal8Bit(), content[1].toLocal8Bit()); } qCDebug(DOCKER) << "envs:" << m_container << m_envs; }); process->start("docker", {"container", "inspect", m_container}); process->waitForFinished(); qDebug() << "inspecting" << QStringList{"container", "inspect", m_container} << process->exitCode(); } DockerRuntime::~DockerRuntime() { } QByteArray DockerRuntime::getenv(const QByteArray& varname) const { return m_envs.value(varname); } void DockerRuntime::setEnabled(bool enable) { if (enable) { m_userMergedDir = KDevelop::Path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/docker-" + QString(m_tag).replace('/', '_')); QDir().mkpath(m_userMergedDir.toLocalFile()); QProcess pCreateContainer; pCreateContainer.start("docker", {"run", "-d", m_tag, "tail", "-f", "/dev/null"}); pCreateContainer.waitForFinished(); if (pCreateContainer.exitCode()) { qCWarning(DOCKER) << "could not create the container" << pCreateContainer.readAll(); } m_container = pCreateContainer.readAll().trimmed(); inspectContainer(); const QStringList cmd = {"pkexec", "bindfs", "--map=root/"+KUser().loginName(), m_mergedDir.toLocalFile(), m_userMergedDir.toLocalFile() }; QProcess p; p.start(cmd.first(), cmd.mid(1)); p.waitForFinished(); if (p.exitCode()) { qCDebug(DOCKER) << "bindfs returned" << cmd << p.exitCode() << p.readAll(); } } else { int codeContainer = QProcess::execute("docker", {"kill", m_container}); qCDebug(DOCKER) << "docker kill returned" << codeContainer; int code = QProcess::execute(QStringLiteral("pkexec"), {"umount", m_userMergedDir.toLocalFile()}); qCDebug(DOCKER) << "umount returned" << code; m_container.clear(); } } static QString ensureEndsSlash(const QString &string) { return string.endsWith('/') ? string : (string + QLatin1Char('/')); } static QStringList projectVolumes() { QStringList ret; const QString dir = ensureEndsSlash(DockerRuntime::s_settings->projectsVolume()); const QString buildDir = ensureEndsSlash(DockerRuntime::s_settings->buildDirsVolume()); - for (IProject* project: ICore::self()->projectController()->projects()) { + const auto& projects = ICore::self()->projectController()->projects(); + for (IProject* project : projects) { const Path path = project->path(); if (path.isLocalFile()) { ret << "--volume" << QStringLiteral("%1:%2").arg(path.toLocalFile(), dir + project->name()); } const auto ibsm = project->buildSystemManager(); if (ibsm) { ret << "--volume" << ibsm->buildDirectory(project->projectItem()).toLocalFile() + QLatin1Char(':') + buildDir + project->name(); } } return ret; } QStringList DockerRuntime::workingDirArgs(QProcess* process) const { const auto wd = process->workingDirectory(); return wd.isEmpty() ? QStringList{} : QStringList{"-w", pathInRuntime(KDevelop::Path(wd)).toLocalFile()}; } void DockerRuntime::startProcess(QProcess* process) const { auto program = process->program(); if (program.contains('/')) program = pathInRuntime(Path(program)).toLocalFile(); const QStringList args = QStringList{"run", "--rm"} << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << program << process->arguments(); process->setProgram("docker"); process->setArguments(args); qCDebug(DOCKER) << "starting qprocess" << process->program() << process->arguments(); process->start(); } void DockerRuntime::startProcess(KProcess* process) const { auto program = process->program(); if (program[0].contains('/')) program[0] = pathInRuntime(Path(program[0])).toLocalFile(); process->setProgram(QStringList{ "docker", "run", "--rm" } << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << program); qCDebug(DOCKER) << "starting kprocess" << process->program().join(' '); process->start(); } static Path projectRelPath(const KDevelop::Path & projectsDir, const KDevelop::Path& runtimePath, bool sourceDir) { const auto relPath = projectsDir.relativePath(runtimePath); const int index = relPath.indexOf(QLatin1Char('/')); auto project = ICore::self()->projectController()->findProjectByName(relPath.left(index)); if (!project) { qCWarning(DOCKER) << "No project for" << relPath; } else { const auto repPathProject = index < 0 ? QString() : relPath.mid(index+1); const auto rootPath = sourceDir ? project->path() : project->buildSystemManager()->buildDirectory(project->projectItem()); return Path(rootPath, repPathProject); } return {}; } KDevelop::Path DockerRuntime::pathInHost(const KDevelop::Path& runtimePath) const { Path ret; const Path projectsDir(DockerRuntime::s_settings->projectsVolume()); if (runtimePath==projectsDir || projectsDir.isParentOf(runtimePath)) { ret = projectRelPath(projectsDir, runtimePath, true); } else { const Path buildDirs(DockerRuntime::s_settings->buildDirsVolume()); if (runtimePath==buildDirs || buildDirs.isParentOf(runtimePath)) { ret = projectRelPath(buildDirs, runtimePath, false); } else ret = KDevelop::Path(m_userMergedDir, KDevelop::Path(QStringLiteral("/")).relativePath(runtimePath)); } qCDebug(DOCKER) << "pathInHost" << ret << runtimePath; return ret; } KDevelop::Path DockerRuntime::pathInRuntime(const KDevelop::Path& localPath) const { if (m_userMergedDir==localPath || m_userMergedDir.isParentOf(localPath)) { KDevelop::Path ret(KDevelop::Path("/"), m_userMergedDir.relativePath(localPath)); qCDebug(DOCKER) << "docker runtime pathInRuntime..." << ret << localPath; return ret; } else if (auto project = ICore::self()->projectController()->findProjectForUrl(localPath.toUrl())) { const Path projectsDir(DockerRuntime::s_settings->projectsVolume()); const QString relpath = project->path().relativePath(localPath); const KDevelop::Path ret(projectsDir, project->name() + QLatin1Char('/') + relpath); qCDebug(DOCKER) << "docker user pathInRuntime..." << ret << localPath; return ret; } else { const auto projects = ICore::self()->projectController()->projects(); for (auto project : projects) { auto ibsm = project->buildSystemManager(); if (ibsm) { const auto builddir = ibsm->buildDirectory(project->projectItem()); if (builddir != localPath && !builddir.isParentOf(localPath)) continue; const Path builddirs(DockerRuntime::s_settings->buildDirsVolume()); const QString relpath = builddir.relativePath(localPath); const KDevelop::Path ret(builddirs, project->name() + QLatin1Char('/') + relpath); qCDebug(DOCKER) << "docker build pathInRuntime..." << ret << localPath; return ret; } } qCWarning(DOCKER) << "only project files are accessible on the docker runtime" << localPath; } qCDebug(DOCKER) << "bypass..." << localPath; return localPath; } diff --git a/plugins/docker/tests/test_docker.cpp b/plugins/docker/tests/test_docker.cpp index 10a8c4a1e1..8dee9f16d7 100644 --- a/plugins/docker/tests/test_docker.cpp +++ b/plugins/docker/tests/test_docker.cpp @@ -1,152 +1,153 @@ /*************************************************************************** * This file was partly taken from KDevelop's cvs plugin * * Copyright 2017 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 #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static QString s_testedImage = QStringLiteral("ubuntu:17.04"); class DockerTest: public QObject { Q_OBJECT public: DockerTest() { QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\nkdevplatform.plugins.docker=true\n")); } IRuntime* m_initialRuntime = nullptr; private Q_SLOTS: void initTestCase() { auto ret = QProcess::execute("docker", {"pull", s_testedImage}); if (ret != 0) { QSKIP("Couldn't successfully call docker"); return; } AutoTestShell::init({QStringLiteral("kdevdocker"), QStringLiteral("KDevGenericManager")}); TestCore::initialize(); m_initialRuntime = ICore::self()->runtimeController()->currentRuntime(); auto plugin = ICore::self()->pluginController()->loadPlugin("kdevdocker"); QVERIFY(plugin); QSignalSpy spy(plugin, SIGNAL(imagesListed())); QVERIFY(spy.wait()); auto projectPath = QUrl::fromLocalFile(QFINDTESTDATA("testproject/test.kdev4")); TestCore::self()->projectController()->openProject(projectPath); QSignalSpy spy2(TestCore::self()->projectController(), &IProjectController::projectOpened); QVERIFY(spy2.wait()); } void init() { QVERIFY(ICore::self()->runtimeController()->currentRuntime() == m_initialRuntime); - for(IRuntime* runtime : ICore::self()->runtimeController()->availableRuntimes()) { + const auto& availableRuntimes = ICore::self()->runtimeController()->availableRuntimes(); + for (IRuntime* runtime : availableRuntimes) { if (s_testedImage == runtime->name()) { ICore::self()->runtimeController()->setCurrentRuntime(runtime); } } QVERIFY(ICore::self()->runtimeController()->currentRuntime() != m_initialRuntime); } void paths() { auto rt = ICore::self()->runtimeController()->currentRuntime(); QVERIFY(rt); const Path root("/"); const Path hostDir = rt->pathInHost(root); QCOMPARE(root, rt->pathInRuntime(hostDir)); } void projectPath() { auto rt = ICore::self()->runtimeController()->currentRuntime(); QVERIFY(rt); auto project = ICore::self()->projectController()->projects().first(); QVERIFY(project); const Path file = project->projectItem()->folder()->fileList().first()->path(); const Path fileRuntime = rt->pathInRuntime(file); QCOMPARE(fileRuntime, Path("/src/test/testfile.sh")); QCOMPARE(rt->pathInHost(fileRuntime), file); QCOMPARE(project->path(), rt->pathInHost(rt->pathInRuntime(project->path()))); } void projectDirectory() { auto rt = ICore::self()->runtimeController()->currentRuntime(); QVERIFY(rt); auto project = ICore::self()->projectController()->projects().first(); QVERIFY(project); const Path projectDir = project->path(); const Path dirRuntime = rt->pathInRuntime(projectDir); QCOMPARE(dirRuntime, Path("/src/test/")); QCOMPARE(rt->pathInHost(dirRuntime), projectDir); QCOMPARE(project->path(), rt->pathInHost(rt->pathInRuntime(project->path()))); } void envs() { auto rt = ICore::self()->runtimeController()->currentRuntime(); QVERIFY(rt); QCOMPARE(rt->getenv("PATH"), QByteArray("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")); } void runProcess() { auto rt = ICore::self()->runtimeController()->currentRuntime(); QVERIFY(rt); auto project = ICore::self()->projectController()->projects().first(); QVERIFY(project); const Path projectPath = rt->pathInRuntime(project->path()); QProcess process; process.setProgram("ls"); process.setArguments({projectPath.toLocalFile()}); rt->startProcess(&process); QVERIFY(process.waitForFinished()); QCOMPARE(process.exitCode(), 0); QCOMPARE(process.readAll(), QByteArray("test.kdev4\ntestfile.sh\n")); } void cleanup() { ICore::self()->runtimeController()->setCurrentRuntime(m_initialRuntime); } }; QTEST_MAIN( DockerTest ) #include "test_docker.moc" diff --git a/plugins/execute/nativeappjob.cpp b/plugins/execute/nativeappjob.cpp index 224f22d687..11fbc6dd1a 100644 --- a/plugins/execute/nativeappjob.cpp +++ b/plugins/execute/nativeappjob.cpp @@ -1,171 +1,172 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "nativeappjob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "executeplugin.h" #include "debug.h" using namespace KDevelop; NativeAppJob::NativeAppJob(QObject* parent, KDevelop::ILaunchConfiguration* cfg) : KDevelop::OutputExecuteJob( parent ) , m_name(cfg->name()) { { auto cfgGroup = cfg->config(); if (cfgGroup.readEntry(ExecutePlugin::isExecutableEntry, false)) { m_name = cfgGroup.readEntry(ExecutePlugin::executableEntry, cfg->name()).section('/', -1); } } setCapabilities(Killable); IExecutePlugin* iface = KDevelop::ICore::self()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute"))->extension(); Q_ASSERT(iface); const KDevelop::EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iface->environmentProfileName(cfg); QString err; QUrl executable = iface->executable( cfg, err ); if( !err.isEmpty() ) { setError( -1 ); setErrorText( err ); return; } if (envProfileName.isEmpty()) { qCWarning(PLUGIN_EXECUTE) << "Launch Configuration:" << cfg->name() << i18n("No environment profile specified, looks like a broken " "configuration, please check run configuration '%1'. " "Using default environment profile.", cfg->name() ); envProfileName = environmentProfiles.defaultProfileName(); } setEnvironmentProfile(envProfileName); QStringList arguments = iface->arguments( cfg, err ); if( !err.isEmpty() ) { setError( -2 ); setErrorText( err ); } if( error() != 0 ) { qCWarning(PLUGIN_EXECUTE) << "Launch Configuration:" << cfg->name() << "oops, problem" << errorText(); return; } setStandardToolView(KDevelop::IOutputView::RunView); setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll); setFilteringStrategy(OutputModel::NativeAppErrorFilter); setProperties(DisplayStdout | DisplayStderr); // Now setup the process parameters QUrl wc = iface->workingDirectory( cfg ); if( !wc.isValid() || wc.isEmpty() ) { wc = QUrl::fromLocalFile( QFileInfo( executable.toLocalFile() ).absolutePath() ); } setWorkingDirectory( wc ); qCDebug(PLUGIN_EXECUTE) << "setting app:" << executable << arguments; if (iface->useTerminal(cfg)) { QString terminalCommand = iface->terminal(cfg); terminalCommand.replace(QLatin1String("%exe"), KShell::quoteArg( executable.toLocalFile()) ); terminalCommand.replace(QLatin1String("%workdir"), KShell::quoteArg( wc.toLocalFile()) ); QStringList args = KShell::splitArgs(terminalCommand); args.append( arguments ); *this << args; } else { *this << executable.toLocalFile(); *this << arguments; } setJobName(m_name); } NativeAppJob* findNativeJob(KJob* j) { NativeAppJob* job = qobject_cast(j); if (!job) { const QList jobs = j->findChildren(); if (!jobs.isEmpty()) job = jobs.first(); } return job; } void NativeAppJob::start() { QVector > currentJobs; // collect running instances of the same type - for (auto j : ICore::self()->runController()->currentJobs()) { + const auto& allCurrentJobs = ICore::self()->runController()->currentJobs(); + for (auto j : allCurrentJobs) { NativeAppJob* njob = findNativeJob(j); if (njob && njob != this && njob->m_name == m_name) currentJobs << njob; } if (!currentJobs.isEmpty()) { QMessageBox msgBox(QMessageBox::Question, i18n("Job already running"), i18n("'%1' is already being executed.", m_name), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); msgBox.button(QMessageBox::No)->setText(i18n("Kill All Instances")); msgBox.button(QMessageBox::Yes)->setText(i18n("Start Another")); msgBox.setDefaultButton(QMessageBox::Cancel); switch (msgBox.exec()) { case QMessageBox::Yes: // simply start another job break; case QMessageBox::No: // kill the running instance for (auto & job : currentJobs) { if (job) job->kill(EmitResult); } break; default: // cancel starting a new job kill(); return; } } OutputExecuteJob::start(); } diff --git a/plugins/filetemplates/templateclassassistant.cpp b/plugins/filetemplates/templateclassassistant.cpp index 45863b6993..65d041df71 100644 --- a/plugins/filetemplates/templateclassassistant.cpp +++ b/plugins/filetemplates/templateclassassistant.cpp @@ -1,593 +1,594 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula 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 "templateclassassistant.h" #include "templateselectionpage.h" #include "templateoptionspage.h" #include "classmemberspage.h" #include "classidentifierpage.h" #include "overridespage.h" #include "licensepage.h" #include "outputpage.h" #include "testcasespage.h" #include "defaultcreateclasshelper.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 #define REMOVE_PAGE(name) \ if (d->name##Page) \ { \ removePage(d->name##Page); \ d->name##Page = nullptr; \ d->name##PageWidget = nullptr; \ } #define ZERO_PAGE(name) \ d->name##Page = nullptr; \ d->name##PageWidget = nullptr; using namespace KDevelop; class KDevelop::TemplateClassAssistantPrivate { public: explicit TemplateClassAssistantPrivate(const QUrl& baseUrl); ~TemplateClassAssistantPrivate(); void addFilesToTarget (const QHash& fileUrls); KPageWidgetItem* templateSelectionPage; KPageWidgetItem* classIdentifierPage; KPageWidgetItem* overridesPage; KPageWidgetItem* membersPage; KPageWidgetItem* testCasesPage; KPageWidgetItem* licensePage; KPageWidgetItem* templateOptionsPage; KPageWidgetItem* outputPage; KPageWidgetItem* dummyPage; TemplateSelectionPage* templateSelectionPageWidget; ClassIdentifierPage* classIdentifierPageWidget; OverridesPage* overridesPageWidget; ClassMembersPage* membersPageWidget; TestCasesPage* testCasesPageWidget; LicensePage* licensePageWidget; TemplateOptionsPage* templateOptionsPageWidget; OutputPage* outputPageWidget; QUrl baseUrl; SourceFileTemplate fileTemplate; ICreateClassHelper* helper; TemplateClassGenerator* generator; TemplateRenderer* renderer; QVariantHash templateOptions; }; TemplateClassAssistantPrivate::TemplateClassAssistantPrivate(const QUrl& baseUrl) : baseUrl(baseUrl) , helper(nullptr) , generator(nullptr) , renderer(nullptr) { } TemplateClassAssistantPrivate::~TemplateClassAssistantPrivate() { delete helper; if (generator) { delete generator; } else { // if we got a generator, it should keep ownership of the renderer // otherwise, we created a templaterenderer on our own delete renderer; } } void TemplateClassAssistantPrivate::addFilesToTarget (const QHash< QString, QUrl >& fileUrls) { // Add the generated files to a target, if one is found QUrl url = baseUrl; if (!url.isValid()) { // This was probably not launched from the project manager view // Still, we try to find the common URL where the generated files are located if (!fileUrls.isEmpty()) { url = fileUrls.constBegin().value().adjusted(QUrl::RemoveFilename); } } qCDebug(PLUGIN_FILETEMPLATES) << "Searching for targets with URL" << url; IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if (!project || !project->buildSystemManager()) { qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project found"; return; } const QList items = project->itemsForPath(IndexedString(url)); if (items.isEmpty()) { qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project items found"; return; } QList targets; ProjectTargetItem* target = nullptr; for (ProjectBaseItem* item : items) { if (ProjectTargetItem* target = item->target()) { targets << target; } } if (targets.isEmpty()) { // If no target was explicitly found yet, try all the targets in the current folder targets.reserve(items.size()); for (ProjectBaseItem* item : items) { targets << item->targetList(); } } if (targets.isEmpty()) { // If still no targets, we traverse the tree up to the first directory with targets ProjectBaseItem* item = items.first()->parent(); while (targets.isEmpty() && item) { targets = item->targetList(); item = item->parent(); } } if (targets.size() == 1) { qCDebug(PLUGIN_FILETEMPLATES) << "Only one candidate target," << targets.first()->text() << ", using it"; target = targets.first(); } else if (targets.size() > 1) { // More than one candidate target, show the chooser dialog ScopedDialog d; auto mainLayout = new QVBoxLayout(d); mainLayout->addWidget(new QLabel(i18n("Choose one target to add the file or cancel if you do not want to do so."))); QListWidget* targetsWidget = new QListWidget(d); targetsWidget->setSelectionMode(QAbstractItemView::SingleSelection); - for (ProjectTargetItem* target : targets) { + for (ProjectTargetItem* target : qAsConst(targets)) { targetsWidget->addItem(target->text()); } targetsWidget->setCurrentRow(0); mainLayout->addWidget(targetsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); d->connect(buttonBox, &QDialogButtonBox::accepted, d.data(), &QDialog::accept); d->connect(buttonBox, &QDialogButtonBox::rejected, d.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if(d->exec() == QDialog::Accepted) { if (!targetsWidget->selectedItems().isEmpty()) { target = targets[targetsWidget->currentRow()]; } else { qCDebug(PLUGIN_FILETEMPLATES) << "Did not select anything, not adding to a target"; return; } } else { qCDebug(PLUGIN_FILETEMPLATES) << "Canceled select target dialog, not adding to a target"; return; } } else { // No target, not doing anything qCDebug(PLUGIN_FILETEMPLATES) << "No possible targets for URL" << url; return; } Q_ASSERT(target); QList fileItems; for (const QUrl& fileUrl : fileUrls) { foreach (ProjectBaseItem* item, project->itemsForPath(IndexedString(KIO::upUrl(fileUrl)))) { if (ProjectFolderItem* folder = item->folder()) { ///FIXME: use Path instead of QUrl in the template class assistant if (ProjectFileItem* file = project->projectFileManager()->addFile(Path(fileUrl), folder)) { fileItems << file; break; } } } } if (!fileItems.isEmpty()) { project->buildSystemManager()->addFilesToTarget(fileItems, target); } } TemplateClassAssistant::TemplateClassAssistant(QWidget* parent, const QUrl& baseUrl) : KAssistantDialog(parent) , d(new TemplateClassAssistantPrivate(baseUrl)) { ZERO_PAGE(templateSelection) ZERO_PAGE(templateOptions) ZERO_PAGE(members) ZERO_PAGE(classIdentifier) ZERO_PAGE(overrides) ZERO_PAGE(license) ZERO_PAGE(output) ZERO_PAGE(testCases) setup(); } TemplateClassAssistant::~TemplateClassAssistant() { delete d; } void TemplateClassAssistant::setup() { if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template in %1", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(i18n("Create Files from Template")); } d->templateSelectionPageWidget = new TemplateSelectionPage(this); connect(this, &TemplateClassAssistant::accepted, d->templateSelectionPageWidget, &TemplateSelectionPage::saveConfig); d->templateSelectionPage = addPage(d->templateSelectionPageWidget, i18n("Language and Template")); d->templateSelectionPage->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); // KAssistantDialog creates a help button by default, no option to prevent that QPushButton *helpButton = button(QDialogButtonBox::Help); if (helpButton) { buttonBox()->removeButton(helpButton); delete helpButton; } } void TemplateClassAssistant::templateChosen(const QString& templateDescription) { d->fileTemplate.setTemplateDescription(templateDescription); const auto type = d->fileTemplate.type(); d->generator = nullptr; if (!d->fileTemplate.isValid()) { return; } qCDebug(PLUGIN_FILETEMPLATES) << "Selected template" << templateDescription << "of type" << type; removePage(d->dummyPage); if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template %1 in %2", d->fileTemplate.name(), d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(xi18n("Create Files from Template %1", d->fileTemplate.name())); } if (type == QLatin1String("Class")) { d->classIdentifierPageWidget = new ClassIdentifierPage(this); d->classIdentifierPage = addPage(d->classIdentifierPageWidget, i18n("Class Basics")); d->classIdentifierPage->setIcon(QIcon::fromTheme(QStringLiteral("classnew"))); connect(d->classIdentifierPageWidget, &ClassIdentifierPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->classIdentifierPage, false); d->overridesPageWidget = new OverridesPage(this); d->overridesPage = addPage(d->overridesPageWidget, i18n("Override Methods")); d->overridesPage->setIcon(QIcon::fromTheme(QStringLiteral("code-class"))); setValid(d->overridesPage, true); d->membersPageWidget = new ClassMembersPage(this); d->membersPage = addPage(d->membersPageWidget, i18n("Class Members")); d->membersPage->setIcon(QIcon::fromTheme(QStringLiteral("field"))); setValid(d->membersPage, true); d->helper = nullptr; QString languageName = d->fileTemplate.languageName(); auto language = ICore::self()->languageController()->language(languageName); if (language) { d->helper = language->createClassHelper(); } if (!d->helper) { qCDebug(PLUGIN_FILETEMPLATES) << "No class creation helper for language" << languageName; d->helper = new DefaultCreateClassHelper; } d->generator = d->helper->createGenerator(d->baseUrl); Q_ASSERT(d->generator); d->generator->setTemplateDescription(d->fileTemplate); d->renderer = d->generator->renderer(); } else { if (type == QLatin1String("Test")) { d->testCasesPageWidget = new TestCasesPage(this); d->testCasesPage = addPage(d->testCasesPageWidget, i18n("Test Cases")); connect(d->testCasesPageWidget, &TestCasesPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->testCasesPage, false); } d->renderer = new TemplateRenderer; d->renderer->setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); } d->licensePageWidget = new LicensePage(this); d->licensePage = addPage(d->licensePageWidget, i18n("License")); d->licensePage->setIcon(QIcon::fromTheme(QStringLiteral("text-x-copying"))); setValid(d->licensePage, true); d->outputPageWidget = new OutputPage(this); d->outputPageWidget->prepareForm(d->fileTemplate); d->outputPage = addPage(d->outputPageWidget, i18n("Output")); d->outputPage->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); connect(d->outputPageWidget, &OutputPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->outputPage, false); if (d->fileTemplate.hasCustomOptions()) { qCDebug(PLUGIN_FILETEMPLATES) << "Class generator has custom options"; d->templateOptionsPageWidget = new TemplateOptionsPage(this); d->templateOptionsPage = insertPage(d->outputPage, d->templateOptionsPageWidget, i18n("Template Options")); } setCurrentPage(d->templateSelectionPage); } void TemplateClassAssistant::next() { qCDebug(PLUGIN_FILETEMPLATES) << currentPage()->name() << currentPage()->header(); if (currentPage() == d->templateSelectionPage) { // We have chosen the template // Depending on the template's language, we can now create a helper QString description = d->templateSelectionPageWidget->selectedTemplate(); templateChosen(description); if (!d->fileTemplate.isValid()) { return; } } else if (currentPage() == d->classIdentifierPage) { d->generator->setIdentifier(d->classIdentifierPageWidget->identifier()); d->generator->setBaseClasses(d->classIdentifierPageWidget->inheritanceList()); } else if (currentPage() == d->overridesPage) { ClassDescription desc = d->generator->description(); desc.methods.clear(); const auto overrides = d->overridesPageWidget->selectedOverrides(); desc.methods.reserve(overrides.size()); for (const DeclarationPointer& declaration : overrides) { desc.methods << FunctionDescription(declaration); } d->generator->setDescription(desc); } else if (currentPage() == d->membersPage) { ClassDescription desc = d->generator->description(); desc.members = d->membersPageWidget->members(); d->generator->setDescription(desc); } else if (currentPage() == d->licensePage) { if (d->generator) { d->generator->setLicense(d->licensePageWidget->license()); } else { d->renderer->addVariable(QStringLiteral("license"), d->licensePageWidget->license()); } } else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) { if (d->generator) { d->generator->addVariables(d->templateOptionsPageWidget->templateOptions()); } else { d->renderer->addVariables(d->templateOptionsPageWidget->templateOptions()); } } else if (currentPage() == d->testCasesPage) { d->renderer->addVariable(QStringLiteral("name"), d->testCasesPageWidget->name()); d->renderer->addVariable(QStringLiteral("testCases"), d->testCasesPageWidget->testCases()); } KAssistantDialog::next(); if (currentPage() == d->classIdentifierPage) { d->classIdentifierPageWidget->setInheritanceList(d->fileTemplate.defaultBaseClasses()); } else if (currentPage() == d->membersPage) { d->membersPageWidget->setMembers(d->generator->description().members); } else if (currentPage() == d->overridesPage) { d->overridesPageWidget->clear(); d->overridesPageWidget->addCustomDeclarations(i18n("Default"), d->helper->defaultMethods(d->generator->name())); d->overridesPageWidget->addBaseClasses(d->generator->directBaseClasses(), d->generator->allBaseClasses()); } else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) { d->templateOptionsPageWidget->load(d->fileTemplate, d->renderer); } else if (currentPage() == d->outputPage) { d->outputPageWidget->loadFileTemplate(d->fileTemplate, d->baseUrl, d->renderer); } if (auto* pageFocus = dynamic_cast(currentPage()->widget())) { pageFocus->setFocusToFirstEditWidget(); } } void TemplateClassAssistant::back() { KAssistantDialog::back(); if (currentPage() == d->templateSelectionPage) { REMOVE_PAGE(classIdentifier) REMOVE_PAGE(overrides) REMOVE_PAGE(members) REMOVE_PAGE(testCases) REMOVE_PAGE(output) REMOVE_PAGE(templateOptions) REMOVE_PAGE(license) delete d->helper; d->helper = nullptr; if (d->generator) { delete d->generator; } else { delete d->renderer; } d->generator = nullptr; d->renderer = nullptr; if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template in %1", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(i18n("Create Files from Template")); } d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); } } void TemplateClassAssistant::accept() { // next() is not called for the last page (when the user clicks Finish), so we have to set output locations here const QHash fileUrls = d->outputPageWidget->fileUrls(); QHash filePositions = d->outputPageWidget->filePositions(); DocumentChangeSet changes; if (d->generator) { QHash::const_iterator it = fileUrls.constBegin(); for (; it != fileUrls.constEnd(); ++it) { d->generator->setFileUrl(it.key(), it.value()); d->generator->setFilePosition(it.key(), filePositions.value(it.key())); } d->generator->addVariables(d->templateOptions); changes = d->generator->generate(); } else { changes = d->renderer->renderFileTemplate(d->fileTemplate, d->baseUrl, fileUrls); } d->addFilesToTarget(fileUrls); changes.applyAllChanges(); // Open the generated files in the editor for (const QUrl& url : fileUrls) { ICore::self()->documentController()->openDocument(url); } KAssistantDialog::accept(); } void TemplateClassAssistant::setCurrentPageValid(bool valid) { setValid(currentPage(), valid); } QUrl TemplateClassAssistant::baseUrl() const { return d->baseUrl; } diff --git a/plugins/filetemplates/templateselectionpage.cpp b/plugins/filetemplates/templateselectionpage.cpp index 1833082112..3a16b2a649 100644 --- a/plugins/filetemplates/templateselectionpage.cpp +++ b/plugins/filetemplates/templateselectionpage.cpp @@ -1,276 +1,277 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula 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 "templateselectionpage.h" #include "templateclassassistant.h" #include "templatepreview.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include #include #include "ui_templateselection.h" #include #include #include #include #include #include #include using namespace KDevelop; static const char LastUsedTemplateEntry[] = "LastUsedTemplate"; static const char FileTemplatesGroup[] = "SourceFileTemplates"; class KDevelop::TemplateSelectionPagePrivate { public: explicit TemplateSelectionPagePrivate(TemplateSelectionPage* page_) : page(page_) {} TemplateSelectionPage* page; Ui::TemplateSelection* ui; QString selectedTemplate; TemplateClassAssistant* assistant; TemplatesModel* model; void currentTemplateChanged(const QModelIndex& index); void getMoreClicked(); void loadFileClicked(); void previewTemplate(const QString& templateFile); }; void TemplateSelectionPagePrivate::currentTemplateChanged(const QModelIndex& index) { // delete preview tabs if (!index.isValid() || index.model()->hasChildren(index)) { // invalid or has child assistant->setValid(assistant->currentPage(), false); ui->previewLabel->setVisible(false); ui->tabWidget->setVisible(false); } else { selectedTemplate = model->data(index, TemplatesModel::DescriptionFileRole).toString(); assistant->setValid(assistant->currentPage(), true); previewTemplate(selectedTemplate); ui->previewLabel->setVisible(true); ui->tabWidget->setVisible(true); ui->previewLabel->setText(i18nc("%1: template comment", "Preview: %1", index.data(TemplatesModel::CommentRole).toString())); } } void TemplateSelectionPagePrivate::previewTemplate(const QString& file) { SourceFileTemplate fileTemplate(file); if (!fileTemplate.isValid() || fileTemplate.outputFiles().isEmpty()) { return; } TemplatePreviewRenderer renderer; // set default option values if (fileTemplate.hasCustomOptions()) { QVariantHash extraVars; - for (const auto& optionGroup : fileTemplate.customOptions(&renderer)) { + const auto& optionGroups = fileTemplate.customOptions(&renderer); + for (const auto& optionGroup : optionGroups) { for (const auto& entry : optionGroup.options) { extraVars[entry.name] = entry.value; } } renderer.addVariables(extraVars); } renderer.setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); QTemporaryDir dir; QUrl base = QUrl::fromLocalFile(dir.path() + QLatin1Char('/')); QHash fileUrls; foreach(const SourceFileTemplate::OutputFile& out, fileTemplate.outputFiles()) { QUrl url = base.resolved(QUrl(renderer.render(out.outputName))); fileUrls.insert(out.identifier, url); } DocumentChangeSet changes = renderer.renderFileTemplate(fileTemplate, base, fileUrls); changes.setActivationPolicy(DocumentChangeSet::DoNotActivate); changes.setUpdateHandling(DocumentChangeSet::NoUpdate); DocumentChangeSet::ChangeResult result = changes.applyAllChanges(); if (!result) { return; } int idx = 0; foreach(const SourceFileTemplate::OutputFile& out, fileTemplate.outputFiles()) { TemplatePreview* preview = nullptr; if (ui->tabWidget->count() > idx) { // reuse existing tab preview = qobject_cast(ui->tabWidget->widget(idx)); ui->tabWidget->setTabText(idx, out.label); Q_ASSERT(preview); } else { // create new tabs on demand preview = new TemplatePreview(page); ui->tabWidget->addTab(preview, out.label); } preview->document()->openUrl(fileUrls.value(out.identifier)); ++idx; } // remove superfluous tabs from last time while (ui->tabWidget->count() > fileUrls.size()) { delete ui->tabWidget->widget(fileUrls.size()); } return; } void TemplateSelectionPagePrivate::getMoreClicked() { KNS3::DownloadDialog(QStringLiteral("kdevfiletemplates.knsrc"), ui->view).exec(); model->refresh(); } void TemplateSelectionPagePrivate::loadFileClicked() { const QStringList filters{ QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), QStringLiteral("application/zip") }; ScopedDialog dlg(page); dlg->setMimeTypeFilters(filters); dlg->setFileMode(QFileDialog::ExistingFiles); if (!dlg->exec()) { return; } foreach(const QString& fileName, dlg->selectedFiles()) { QString destination = model->loadTemplateFile(fileName); QModelIndexList indexes = model->templateIndexes(destination); int n = indexes.size(); if (n > 1) { ui->view->setCurrentIndex(indexes[1]); } } } void TemplateSelectionPage::saveConfig() { KSharedConfigPtr config; if (IProject* project = ICore::self()->projectController()->findProjectForUrl(d->assistant->baseUrl())) { config = project->projectConfiguration(); } else { config = ICore::self()->activeSession()->config(); } KConfigGroup group(config, FileTemplatesGroup); group.writeEntry(LastUsedTemplateEntry, d->selectedTemplate); group.sync(); } TemplateSelectionPage::TemplateSelectionPage(TemplateClassAssistant* parent) : QWidget(parent) , d(new TemplateSelectionPagePrivate(this)) { d->assistant = parent; d->ui = new Ui::TemplateSelection; d->ui->setupUi(this); d->model = new TemplatesModel(QStringLiteral("kdevfiletemplates"), this); d->model->refresh(); d->ui->view->setLevels(3); d->ui->view->setHeaderLabels(QStringList{i18n("Language"), i18n("Framework"), i18n("Template")}); d->ui->view->setModel(d->model); connect(d->ui->view, &MultiLevelListView::currentIndexChanged, this, [&] (const QModelIndex& index) { d->currentTemplateChanged(index); }); QModelIndex templateIndex; while (d->model->hasIndex(0, 0, templateIndex)) { templateIndex = d->model->index(0, 0, templateIndex); } KSharedConfigPtr config; if (IProject* project = ICore::self()->projectController()->findProjectForUrl(d->assistant->baseUrl())) { config = project->projectConfiguration(); } else { config = ICore::self()->activeSession()->config(); } KConfigGroup group(config, FileTemplatesGroup); QString lastTemplate = group.readEntry(LastUsedTemplateEntry); QModelIndexList indexes = d->model->match(d->model->index(0, 0), TemplatesModel::DescriptionFileRole, lastTemplate, 1, Qt::MatchRecursive); if (!indexes.isEmpty()) { templateIndex = indexes.first(); } d->ui->view->setCurrentIndex(templateIndex); QPushButton* getMoreButton = new QPushButton(i18n("Get More Templates..."), d->ui->view); getMoreButton->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); connect (getMoreButton, &QPushButton::clicked, this, [&] { d->getMoreClicked(); }); d->ui->view->addWidget(0, getMoreButton); QPushButton* loadButton = new QPushButton(QIcon::fromTheme(QStringLiteral("application-x-archive")), i18n("Load Template From File"), d->ui->view); connect (loadButton, &QPushButton::clicked, this, [&] { d->loadFileClicked(); }); d->ui->view->addWidget(0, loadButton); d->ui->view->setContentsMargins(0, 0, 0, 0); } TemplateSelectionPage::~TemplateSelectionPage() { delete d->ui; delete d; } QSize TemplateSelectionPage::minimumSizeHint() const { return QSize(400, 600); } QString TemplateSelectionPage::selectedTemplate() const { return d->selectedTemplate; } #include "moc_templateselectionpage.cpp" diff --git a/plugins/flatpak/flatpakplugin.cpp b/plugins/flatpak/flatpakplugin.cpp index 9f80288120..7c1ec94672 100644 --- a/plugins/flatpak/flatpakplugin.cpp +++ b/plugins/flatpak/flatpakplugin.cpp @@ -1,221 +1,222 @@ /* Copyright 2017 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "flatpakplugin.h" #include "flatpakruntime.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 K_PLUGIN_FACTORY_WITH_JSON(KDevFlatpakFactory, "kdevflatpak.json", registerPlugin();) using namespace KDevelop; FlatpakPlugin::FlatpakPlugin(QObject *parent, const QVariantList & /*args*/) : KDevelop::IPlugin( QStringLiteral("kdevflatpak"), parent ) { auto ac = actionCollection(); auto action = new QAction(QIcon::fromTheme(QStringLiteral("run-build-clean")), i18n("Rebuild environment"), this); action->setWhatsThis(i18n("Recompiles all dependencies for a fresh environment.")); ac->setDefaultShortcut(action, Qt::CTRL | Qt::META | Qt::Key_X); connect(action, &QAction::triggered, this, &FlatpakPlugin::rebuildCurrent); ac->addAction(QStringLiteral("runtime_flatpak_rebuild"), action); auto exportAction = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export flatpak bundle..."), this); exportAction->setWhatsThis(i18n("Exports the current build into a 'bundle.flatpak' file.")); ac->setDefaultShortcut(exportAction, Qt::CTRL | Qt::META | Qt::Key_E); connect(exportAction, &QAction::triggered, this, &FlatpakPlugin::exportCurrent); ac->addAction(QStringLiteral("runtime_flatpak_export"), exportAction); auto remoteAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-remote-symbolic")), i18n("Send to device..."), this); ac->setDefaultShortcut(remoteAction, Qt::CTRL | Qt::META | Qt::Key_D); connect(remoteAction, &QAction::triggered, this, &FlatpakPlugin::executeOnRemoteDevice); ac->addAction(QStringLiteral("runtime_flatpak_remote"), remoteAction); runtimeChanged(ICore::self()->runtimeController()->currentRuntime()); setXMLFile( QStringLiteral("kdevflatpakplugin.rc") ); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &FlatpakPlugin::runtimeChanged); } FlatpakPlugin::~FlatpakPlugin() = default; void FlatpakPlugin::runtimeChanged(KDevelop::IRuntime* newRuntime) { const bool isFlatpak = qobject_cast(newRuntime); - for(auto action: actionCollection()->actions()) { + const auto& actions = actionCollection()->actions(); + for (auto action: actions) { action->setEnabled(isFlatpak); } } void FlatpakPlugin::rebuildCurrent() { const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); Q_ASSERT(runtime); ICore::self()->runController()->registerJob(runtime->rebuild()); } void FlatpakPlugin::exportCurrent() { const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); Q_ASSERT(runtime); const QString path = QFileDialog::getSaveFileName(ICore::self()->uiController()->activeMainWindow(), i18n("Export %1 to...", runtime->name()), {}, i18n("Flatpak Bundle (*.flatpak)")); if (!path.isEmpty()) { ICore::self()->runController()->registerJob(new ExecuteCompositeJob(runtime, runtime->exportBundle(path))); } } void FlatpakPlugin::createRuntime(const KDevelop::Path &file, const QString &arch) { QTemporaryDir* dir = new QTemporaryDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/kdevelop-flatpak-")); const KDevelop::Path path(dir->path()); auto process = FlatpakRuntime::createBuildDirectory(path, file, arch); connect(process, &KJob::finished, this, [path, file, arch, dir] (KJob* job) { if (job->error() != 0) { delete dir; return; } auto rt = new FlatpakRuntime(path, file, arch); connect(rt, &QObject::destroyed, rt, [dir]() { delete dir; }); ICore::self()->runtimeController()->addRuntimes(rt); }); process->start(); } static QStringList availableArches(const KDevelop::Path& url) { QProcess supportedArchesProcess; QStringList ret; QObject::connect(&supportedArchesProcess, static_cast(&QProcess::finished), &supportedArchesProcess, [&supportedArchesProcess, &ret]() { supportedArchesProcess.deleteLater(); QTextStream stream(&supportedArchesProcess); while (!stream.atEnd()) { const QString line = stream.readLine(); ret << line.section(QLatin1Char('/'), 2, 2); } }); const auto doc = FlatpakRuntime::config(url); const QString sdkName = doc[QLatin1String("sdk")].toString(); const QString runtimeVersion = doc[QLatin1String("runtime-version")].toString(); supportedArchesProcess.start("flatpak", {"info", "-r", sdkName + "//" + runtimeVersion }); supportedArchesProcess.waitForFinished(); return ret; } KDevelop::ContextMenuExtension FlatpakPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { QList urls; if ( context->type() == KDevelop::Context::FileContext ) { KDevelop::FileContext* filectx = static_cast(context); urls = filectx->urls(); } else if ( context->type() == KDevelop::Context::ProjectItemContext ) { KDevelop::ProjectItemContext* projctx = static_cast(context); foreach( KDevelop::ProjectBaseItem* item, projctx->items() ) { if ( item->file() ) { urls << item->file()->path().toUrl(); } } } const QRegularExpression nameRx(".*\\..*\\..*\\.json$"); for(auto it = urls.begin(); it != urls.end(); ) { if (it->isLocalFile() && it->path().contains(nameRx)) { ++it; } else { it = urls.erase(it); } } if ( !urls.isEmpty() ) { KDevelop::ContextMenuExtension ext; foreach(const QUrl &url, urls) { const KDevelop::Path file(url); foreach(const QString &arch, availableArches(file)) { auto action = new QAction(i18n("Build flatpak %1 for %2", file.lastPathSegment(), arch), parent); connect(action, &QAction::triggered, this, [this, file, arch]() { createRuntime(file, arch); }); ext.addAction(KDevelop::ContextMenuExtension::RunGroup, action); } } return ext; } return KDevelop::IPlugin::contextMenuExtension(context, parent); } void FlatpakPlugin::executeOnRemoteDevice() { const auto runtime = qobject_cast(ICore::self()->runtimeController()->currentRuntime()); Q_ASSERT(runtime); KConfigGroup group(KSharedConfig::openConfig(), "Flatpak"); const QString lastDeviceAddress = group.readEntry("DeviceAddress"); const QString host = QInputDialog::getText( ICore::self()->uiController()->activeMainWindow(), i18n("Choose tag name..."), i18n("Device hostname"), QLineEdit::Normal, lastDeviceAddress ); if (host.isEmpty()) return; group.writeEntry("DeviceAddress", host); QTemporaryFile* file = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + runtime->name() + "XXXXXX.flatpak"); file->open(); file->close(); auto job = runtime->executeOnDevice(host, file->fileName()); file->setParent(file); ICore::self()->runController()->registerJob(job); } #include "flatpakplugin.moc" diff --git a/plugins/gdb/debuggerplugin.cpp b/plugins/gdb/debuggerplugin.cpp index e668a32837..cbe6e58807 100644 --- a/plugins/gdb/debuggerplugin.cpp +++ b/plugins/gdb/debuggerplugin.cpp @@ -1,175 +1,177 @@ // /* // * GDB Debugger Support // * // * Copyright 1999-2001 John Birch // * Copyright 2001 by Bernd Gehrmann // * Copyright 2006 Vladimir Prus // * Copyright 2007 Hamish Rodda // * // * 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 "debuggerplugin.h" #include "config-gdb-plugin.h" #include "widgets/disassemblewidget.h" #include "memviewdlg.h" #include "gdboutputwidget.h" #include "gdbconfigpage.h" #include "debugsession.h" #include #include #include #include #include #include #include #include // explicit init of resources needed, because all files // are first compiled into a static library which is also used for unit testing // for some reason the respective resource init methods are not triggered or registered then inline void initMyResource() { Q_INIT_RESOURCE(kdevgdb); } using namespace KDevMI::GDB; K_PLUGIN_FACTORY_WITH_JSON(CppDebuggerFactory, "kdevgdb.json", registerPlugin(); ) CppDebuggerPlugin::CppDebuggerPlugin(QObject *parent, const QVariantList &) : MIDebuggerPlugin(QStringLiteral("kdevgdb"), i18n("GDB"), parent) , disassemblefactory(nullptr) , gdbfactory(nullptr) , memoryviewerfactory(nullptr) { initMyResource(); setXMLFile(QStringLiteral("kdevgdbui.rc")); auto pluginController = core()->pluginController(); - for(auto plugin : pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"))) { + const auto plugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin")); + for (auto plugin : plugins) { setupExecutePlugin(plugin, true); } connect(pluginController, &KDevelop::IPluginController::pluginLoaded, this, [this](KDevelop::IPlugin* plugin) { setupExecutePlugin(plugin, true); }); connect(pluginController, &KDevelop::IPluginController::unloadingPlugin, this, [this](KDevelop::IPlugin* plugin) { setupExecutePlugin(plugin, false); }); } void CppDebuggerPlugin::unload() { - for(auto plugin : core()->pluginController()->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"))) { + const auto plugins = core()->pluginController()->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin")); + for (auto plugin : plugins) { setupExecutePlugin(plugin, false); } Q_ASSERT(m_launchers.isEmpty()); } void CppDebuggerPlugin::setupExecutePlugin(KDevelop::IPlugin* plugin, bool load) { if (plugin == this) { return; } auto iface = plugin->extension(); if (!iface) { return; } auto type = core()->runController()->launchConfigurationTypeForId(iface->nativeAppConfigTypeId()); Q_ASSERT(type); if (load) { auto launcher = new GdbLauncher(this, iface); m_launchers.insert(plugin, launcher); type->addLauncher(launcher); } else { auto launcher = m_launchers.take(plugin); Q_ASSERT(launcher); type->removeLauncher(launcher); delete launcher; } } void CppDebuggerPlugin::setupToolViews() { disassemblefactory = new DebuggerToolFactory( this, QStringLiteral("org.kdevelop.debugger.DisassemblerView"), Qt::BottomDockWidgetArea); gdbfactory = new DebuggerToolFactory( this, QStringLiteral("org.kdevelop.debugger.ConsoleView"),Qt::BottomDockWidgetArea); core()->uiController()->addToolView( i18n("Disassemble/Registers"), disassemblefactory); core()->uiController()->addToolView( i18n("GDB"), gdbfactory); #ifndef KDEV_WITH_MEMVIEW memoryviewerfactory = nullptr; #else memoryviewerfactory = new DebuggerToolFactory( this, QStringLiteral("org.kdevelop.debugger.MemoryView"), Qt::BottomDockWidgetArea); core()->uiController()->addToolView( i18n("Memory"), memoryviewerfactory); #endif } void CppDebuggerPlugin::unloadToolViews() { if (disassemblefactory) { core()->uiController()->removeToolView(disassemblefactory); disassemblefactory = nullptr; } if (gdbfactory) { core()->uiController()->removeToolView(gdbfactory); gdbfactory = nullptr; } if (memoryviewerfactory) { core()->uiController()->removeToolView(memoryviewerfactory); memoryviewerfactory = nullptr; } } CppDebuggerPlugin::~CppDebuggerPlugin() { } DebugSession* CppDebuggerPlugin::createSession() { DebugSession *session = new DebugSession(this); KDevelop::ICore::self()->debugController()->addSession(session); connect(session, &DebugSession::showMessage, this, &CppDebuggerPlugin::showStatusMessage); connect(session, &DebugSession::reset, this, &CppDebuggerPlugin::reset); connect(session, &DebugSession::raiseDebuggerConsoleViews, this, &CppDebuggerPlugin::raiseDebuggerConsoleViews); return session; } #include "debuggerplugin.moc" diff --git a/plugins/gdb/debugsession.cpp b/plugins/gdb/debugsession.cpp index b5ff64ce56..ff8352e15a 100644 --- a/plugins/gdb/debugsession.cpp +++ b/plugins/gdb/debugsession.cpp @@ -1,324 +1,325 @@ /* * GDB Debugger Support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * 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 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 "debugsession.h" #include "debuglog.h" #include "debuggerplugin.h" #include "gdb.h" #include "gdbbreakpointcontroller.h" #include "gdbframestackmodel.h" #include "mi/micommand.h" #include "stty.h" #include "variablecontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI::GDB; using namespace KDevMI::MI; using namespace KDevelop; DebugSession::DebugSession(CppDebuggerPlugin *plugin) : MIDebugSession(plugin) { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new GdbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } void DebugSession::setAutoDisableASLR(bool enable) { m_autoDisableASLR = enable; } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } GdbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } GdbDebugger *DebugSession::createDebugger() const { return new GdbDebugger; } void DebugSession::initializeDebugger() { //addCommand(new GDBCommand(GDBMI::EnableTimings, "yes")); addCommand(new CliCommand(MI::GdbShow, QStringLiteral("version"), this, &DebugSession::handleVersion)); // This makes gdb pump a variable out on one line. addCommand(MI::GdbSet, QStringLiteral("width 0")); addCommand(MI::GdbSet, QStringLiteral("height 0")); addCommand(MI::SignalHandle, QStringLiteral("SIG32 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG41 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG42 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG43 pass nostop noprint")); addCommand(MI::EnablePrettyPrinting); addCommand(MI::GdbSet, QStringLiteral("charset UTF-8")); addCommand(MI::GdbSet, QStringLiteral("print sevenbit-strings off")); QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevgdb/printers/gdbinit")); if (!fileName.isEmpty()) { QFileInfo fileInfo(fileName); QString quotedPrintersPath = fileInfo.dir().path() .replace('\\', QLatin1String("\\\\")) .replace('"', QLatin1String("\\\"")); addCommand(MI::NonMI, QStringLiteral("python sys.path.insert(0, \"%0\")").arg(quotedPrintersPath)); addCommand(MI::NonMI, "source " + fileName); } // GDB can't disable ASLR on CI server. addCommand(MI::GdbSet, QStringLiteral("disable-randomization %1").arg(m_autoDisableASLR ? "on" : "off")); qCDebug(DEBUGGERGDB) << "Initialized GDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &) { // Read Configuration values KConfigGroup grp = cfg->config(); bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); bool displayStaticMembers = grp.readEntry(Config::StaticMembersEntry, false); bool asmDemangle = grp.readEntry(Config::DemangleNamesEntry, true); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; foreach (Breakpoint *b, m->breakpoints()) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); if (displayStaticMembers) { addCommand(MI::GdbSet, QStringLiteral("print static-members on")); } else { addCommand(MI::GdbSet, QStringLiteral("print static-members off")); } if (asmDemangle) { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle on")); } else { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle off")); } // Set the environment variables const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { qCWarning(DEBUGGERGDB) << i18n("No environment profile specified, looks like a broken " "configuration, please check run configuration '%1'. " "Using default environment profile.", cfg->name()); envProfileName = environmentProfiles.defaultProfileName(); } - for (const auto &envvar : environmentProfiles.createEnvironment(envProfileName, {})) { + const auto& envvars = environmentProfiles.createEnvironment(envProfileName, {}); + for (const auto& envvar : envvars) { addCommand(GdbSet, "environment " + envvar); } qCDebug(DEBUGGERGDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(KDevelop::ILaunchConfiguration *cfg, IExecutePlugin *, const QString &executable) { qCDebug(DEBUGGERGDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); QUrl configGdbScript = grp.readEntry(Config::RemoteGdbConfigEntry, QUrl()); QUrl runShellScript = grp.readEntry(Config::RemoteGdbShellEntry, QUrl()); QUrl runGdbScript = grp.readEntry(Config::RemoteGdbRunEntry, QUrl()); // handle remote debug if (configGdbScript.isValid()) { addCommand(MI::NonMI, "source " + KShell::quoteArg(configGdbScript.toLocalFile())); } // FIXME: have a check box option that controls remote debugging if (runShellScript.isValid()) { // Special for remote debug, the remote inferior is started by this shell script QByteArray tty(m_tty->getSlave().toLatin1()); QByteArray options = QByteArray(">") + tty + QByteArray(" 2>&1 <") + tty; QProcess *proc = new QProcess; const QStringList arguments{ QStringLiteral("-c"), KShell::quoteArg(runShellScript.toLocalFile()) + QLatin1Char(' ') + KShell::quoteArg(executable) + QString::fromLatin1(options), }; qCDebug(DEBUGGERGDB) << "starting sh" << arguments; proc->start(QStringLiteral("sh"), arguments); //PORTING TODO QProcess::DontCare); } if (runGdbScript.isValid()) { // Special for remote debug, gdb script at run is requested, to connect to remote inferior // Race notice: wait for the remote gdbserver/executable // - but that might be an issue for this script to handle... // Note: script could contain "run" or "continue" // Future: the shell script should be able to pass info (like pid) // to the gdb script... addCommand(new SentinelCommand([this, runGdbScript]() { breakpointController()->initSendBreakpoints(); breakpointController()->setDeleteDuplicateBreakpoints(true); qCDebug(DEBUGGERGDB) << "Running gdb script " << KShell::quoteArg(runGdbScript.toLocalFile()); addCommand(MI::NonMI, "source " + KShell::quoteArg(runGdbScript.toLocalFile()), [this](const MI::ResultRecord&) { breakpointController()->setDeleteDuplicateBreakpoints(false); }, CmdMaybeStartsRunning); raiseEvent(connected_to_program); }, CmdMaybeStartsRunning)); } else { // normal local debugging addCommand(MI::FileExecAndSymbols, KShell::quoteArg(executable), this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new SentinelCommand([this]() { breakpointController()->initSendBreakpoints(); addCommand(MI::ExecRun, QString(), CmdMaybeStartsRunning); }, CmdMaybeStartsRunning)); } return true; } bool DebugSession::loadCoreFile(KDevelop::ILaunchConfiguration*, const QString& debugee, const QString& corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(NonMI, "core " + corefile, this, &DebugSession::handleCoreFile, CmdHandlesError); return true; } void DebugSession::handleVersion(const QStringList& s) { qCDebug(DEBUGGERGDB) << s.first(); // minimal version is 7.0,0 QRegExp rx("([7-9]+)\\.([0-9]+)(\\.([0-9]+))?"); int idx = rx.indexIn(s.first()); if (idx == -1) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } KMessageBox::error( qApp->activeWindow(), i18n("You need gdb 7.0.0 or higher.
" "You are using: %1", s.first()), i18n("gdb error")); stopDebugger(); } } void DebugSession::handleFileExecAndSymbols(const ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not start debugger:
")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleCoreFile(const ResultRecord& r) { if (r.reason != QLatin1String("error")) { setDebuggerStateOn(s_programExited | s_core); } else { KMessageBox::error( qApp->activeWindow(), i18n("Failed to load core file" "

Debugger reported the following error:" "

%1", r["msg"].literal()), i18n("Startup error")); stopDebugger(); } } diff --git a/plugins/lldb/debuggerplugin.cpp b/plugins/lldb/debuggerplugin.cpp index bed8266b65..65e09147fe 100644 --- a/plugins/lldb/debuggerplugin.cpp +++ b/plugins/lldb/debuggerplugin.cpp @@ -1,143 +1,145 @@ /* * LLDB Debugger Support * Copyright 2016 Aetf * * 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 "debuggerplugin.h" #include "debuglog.h" #include "lldblauncher.h" #include "widgets/debuggerconsoleview.h" #include #include #include #include #include #include using namespace KDevMI::LLDB; inline void initMyResource() { Q_INIT_RESOURCE(kdevlldb); } K_PLUGIN_FACTORY_WITH_JSON(LldbDebuggerFactory, "kdevlldb.json", registerPlugin(); ) LldbDebuggerPlugin::LldbDebuggerPlugin(QObject *parent, const QVariantList &) : MIDebuggerPlugin(QStringLiteral("kdevlldb"), i18n("LLDB"), parent) , m_consoleFactory(nullptr) , m_disassembleFactory(nullptr) { initMyResource(); setXMLFile(QStringLiteral("kdevlldbui.rc")); auto pluginController = core()->pluginController(); - for(auto plugin : pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"))) { + const auto plugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin")); + for (auto plugin : plugins) { setupExecutePlugin(plugin, true); } connect(pluginController, &KDevelop::IPluginController::pluginLoaded, this, [this](KDevelop::IPlugin* plugin) { setupExecutePlugin(plugin, true); }); connect(pluginController, &KDevelop::IPluginController::unloadingPlugin, this, [this](KDevelop::IPlugin* plugin) { setupExecutePlugin(plugin, false); }); } void LldbDebuggerPlugin::unload() { - for(auto plugin : core()->pluginController()->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"))) { + const auto plugins = core()->pluginController()->allPluginsForExtension(QStringLiteral("org.kdevelop.IExecutePlugin")); + for (auto plugin : plugins) { setupExecutePlugin(plugin, false); } Q_ASSERT(m_launchers.isEmpty()); } void LldbDebuggerPlugin::setupExecutePlugin(KDevelop::IPlugin* plugin, bool load) { if (plugin == this) { return; } auto iface = plugin->extension(); if (!iface) { return; } auto type = core()->runController()->launchConfigurationTypeForId(iface->nativeAppConfigTypeId()); Q_ASSERT(type); if (load) { auto launcher = new LldbLauncher(this, iface); m_launchers.insert(plugin, launcher); type->addLauncher(launcher); } else { auto launcher = m_launchers.take(plugin); Q_ASSERT(launcher); type->removeLauncher(launcher); delete launcher; } } void LldbDebuggerPlugin::setupToolViews() { m_consoleFactory = new DebuggerToolFactory(this, QStringLiteral("org.kdevelop.debugger.LldbConsole"), Qt::BottomDockWidgetArea); core()->uiController()->addToolView(i18n("LLDB Console"), m_consoleFactory); /* m_disassembleFactory = new DebuggerToolFactory(this, "org.kdevelop.debugger.LldbDisassemble", Qt::BottomDockWidgetArea); core()->uiController()->addToolView(i18n("LLDB Disassemble/Register"), m_disassembleFactory); */ } void LldbDebuggerPlugin::unloadToolViews() { if (m_consoleFactory) { qCDebug(DEBUGGERLLDB) << "Removing tool view"; core()->uiController()->removeToolView(m_consoleFactory); m_consoleFactory = nullptr; } /* core()->uiController()->removeToolView(m_disassembleFactory); core()->uiController()->removeToolView(memoryviewerfactory); */ } LldbDebuggerPlugin::~LldbDebuggerPlugin() { } DebugSession* LldbDebuggerPlugin::createSession() { DebugSession *session = new DebugSession(this); core()->debugController()->addSession(session); connect(session, &DebugSession::showMessage, this, &LldbDebuggerPlugin::showStatusMessage); connect(session, &DebugSession::reset, this, &LldbDebuggerPlugin::reset); connect(session, &DebugSession::raiseDebuggerConsoleViews, this, &LldbDebuggerPlugin::raiseDebuggerConsoleViews); return session; } #include "debuggerplugin.moc" diff --git a/plugins/lldb/debugsession.cpp b/plugins/lldb/debugsession.cpp index 693becc9b2..44ab1f8825 100644 --- a/plugins/lldb/debugsession.cpp +++ b/plugins/lldb/debugsession.cpp @@ -1,495 +1,496 @@ /* * LLDB Debugger Support * Copyright 2016 Aetf * * 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 "debugsession.h" #include "controllers/variable.h" #include "dbgglobal.h" #include "debuggerplugin.h" #include "debuglog.h" #include "lldbcommand.h" #include "mi/micommand.h" #include "stty.h" #include "stringhelpers.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include using namespace KDevMI::LLDB; using namespace KDevMI::MI; using namespace KDevMI; using namespace KDevelop; struct ExecRunHandler : public MICommandHandler { explicit ExecRunHandler(DebugSession *session, int maxRetry = 5) : m_session(session) , m_remainRetry(maxRetry) , m_activeCommands(1) { } void handle(const ResultRecord& r) override { --m_activeCommands; if (r.reason == QLatin1String("error")) { if (r.hasField(QStringLiteral("msg")) && r[QStringLiteral("msg")].literal().contains(QLatin1String("Invalid process during debug session"))) { // for some unknown reason, lldb-mi sometimes fails to start process if (m_remainRetry && m_session) { qCDebug(DEBUGGERLLDB) << "Retry starting"; --m_remainRetry; // resend the command again. ++m_activeCommands; m_session->addCommand(ExecRun, QString(), this, // use *this as handler, so we can track error times CmdMaybeStartsRunning | CmdHandlesError); return; } } qCDebug(DEBUGGERLLDB) << "Failed to start inferior:" << "exceeded retry times or session become invalid"; m_session->stopDebugger(); } if (m_activeCommands == 0) delete this; } bool handlesError() override { return true; } bool autoDelete() override { return false; } QPointer m_session; int m_remainRetry; int m_activeCommands; }; DebugSession::DebugSession(LldbDebuggerPlugin *plugin) : MIDebugSession(plugin) , m_formatterPath() { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new LldbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); connect(this, &DebugSession::stateChanged, this, &DebugSession::handleSessionStateChange); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } LldbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } LldbDebugger *DebugSession::createDebugger() const { return new LldbDebugger; } MICommand *DebugSession::createCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) const { return new LldbCommand(type, arguments, flags); } MICommand *DebugSession::createUserCommand(const QString& cmd) const { if (m_hasCorrectCLIOutput) return MIDebugSession::createUserCommand(cmd); auto msg = i18n("Attempting to execute user command on unsupported LLDB version"); emit debuggerInternalOutput(msg); qCDebug(DEBUGGERLLDB) << "Attempting user command on unsupported LLDB version"; return nullptr; } void DebugSession::setFormatterPath(const QString &path) { m_formatterPath = path; } void DebugSession::initializeDebugger() { //addCommand(MI::EnableTimings, "yes"); // Check version addCommand(new CliCommand(MI::NonMI, QStringLiteral("version"), this, &DebugSession::handleVersion)); // load data formatter auto formatterPath = m_formatterPath; if (!QFileInfo(formatterPath).isFile()) { formatterPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevlldb/formatters/all.py")); } if (!formatterPath.isEmpty()) { addCommand(MI::NonMI, "command script import " + KShell::quoteArg(formatterPath)); } // Treat char array as string addCommand(MI::GdbSet, QStringLiteral("print char-array-as-string on")); // set a larger term width. // TODO: set term-width to exact max column count in console view addCommand(MI::NonMI, QStringLiteral("settings set term-width 1024")); qCDebug(DEBUGGERLLDB) << "Initialized LLDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &executable) { // Read Configuration values KConfigGroup grp = cfg->config(); // Create target as early as possible, so we can do target specific configuration later QString filesymbols = Utils::quote(executable); bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); if (remoteDebugging) { auto connStr = grp.readEntry(Config::LldbRemoteServerEntry, QString()); auto remoteDir = grp.readEntry(Config::LldbRemotePathEntry, QString()); auto remoteExe = QDir(remoteDir).filePath(QFileInfo(executable).fileName()); filesymbols += " -r " + Utils::quote(remoteExe); addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); addCommand(MI::TargetSelect, "remote " + connStr, this, &DebugSession::handleTargetSelect, CmdHandlesError); // ensure executable is on remote end addCommand(MI::NonMI, QStringLiteral("platform mkdir -v 755 %0").arg(Utils::quote(remoteDir))); addCommand(MI::NonMI, QStringLiteral("platform put-file %0 %1") .arg(Utils::quote(executable), Utils::quote(remoteExe))); } else { addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); } raiseEvent(connected_to_program); // Set the environment variables has effect only after target created const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { envProfileName = environmentProfiles.defaultProfileName(); } const auto &envVariables = environmentProfiles.variables(envProfileName); if (!envVariables.isEmpty()) { QStringList vars; vars.reserve(envVariables.size()); for (auto it = envVariables.constBegin(), ite = envVariables.constEnd(); it != ite; ++it) { vars.append(QStringLiteral("%0=%1").arg(it.key(), Utils::quote(it.value()))); } // actually using lldb command 'settings set target.env-vars' which accepts multiple values addCommand(GdbSet, "environment " + vars.join(QLatin1Char(' '))); } // Break on start: can't use "-exec-run --start" because in lldb-mi // the inferior stops without any notification bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; foreach (Breakpoint *b, m->breakpoints()) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); qCDebug(DEBUGGERLLDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(ILaunchConfiguration *cfg, IExecutePlugin *, const QString &) { qCDebug(DEBUGGERLLDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); // Start inferior bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); QUrl configLldbScript = grp.readEntry(Config::LldbConfigScriptEntry, QUrl()); addCommand(new SentinelCommand([this, remoteDebugging, configLldbScript]() { // setup inferior I/O redirection if (!remoteDebugging) { // FIXME: a hacky way to emulate tty setting on linux. Not sure if this provides all needed // functionalities of a pty. Should make this conditional on other platforms. // no need to quote, settings set takes 'raw' input addCommand(MI::NonMI, QStringLiteral("settings set target.input-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.output-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.error-path %0").arg(m_tty->getSlave())); } else { // what is the expected behavior for using external terminal when doing remote debugging? } // send breakpoints already in our breakpoint model to lldb auto bc = breakpointController(); bc->initSendBreakpoints(); qCDebug(DEBUGGERLLDB) << "Turn on delete duplicate mode"; // turn on delete duplicate breakpoints model, so that breakpoints created by user command in // the script and returned as a =breakpoint-created notification won't get duplicated with the // one already in our model. // we will turn this model off once we first reach a paused state, and from that time on, // the user can create duplicated breakpoints using normal command. bc->setDeleteDuplicateBreakpoints(true); // run custom config script right before we starting the inferior, // so the user has the freedom to change everything. if (configLldbScript.isValid()) { addCommand(MI::NonMI, "command source -s 0 " + KShell::quoteArg(configLldbScript.toLocalFile())); } addCommand(MI::ExecRun, QString(), new ExecRunHandler(this), CmdMaybeStartsRunning | CmdHandlesError); }, CmdMaybeStartsRunning)); return true; } bool DebugSession::loadCoreFile(ILaunchConfiguration *, const QString &debugee, const QString &corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new CliCommand(NonMI, "target create -c " + Utils::quote(corefile), this, &DebugSession::handleCoreFile, CmdHandlesError)); return true; } void DebugSession::interruptDebugger() { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; addCommand(ExecInterrupt, QString(), CmdInterrupt); return; } void DebugSession::ensureDebuggerListening() { // lldb always uses async mode and prompt is always available. // no need to interrupt setDebuggerStateOff(s_dbgNotListening); // NOTE: there is actually a bug in lldb-mi that, when receiving SIGINT, // it would print '^done', which doesn't coresponds to any previous command. // This confuses our command queue. } void DebugSession::handleFileExecAndSymbols(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not start debugger:
")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleTargetSelect(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error(qApp->activeWindow(), i18n("Error connecting to remote target:
")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleCoreFile(const QStringList &s) { qCDebug(DEBUGGERLLDB) << s; for (const auto &line : s) { if (line.startsWith(QLatin1String("error:"))) { KMessageBox::error( qApp->activeWindow(), i18n("Failed to load core file" "

Debugger reported the following error:" "

%1", s.join('\n')), i18n("Startup error")); stopDebugger(); return; } } // There's no "thread-group-started" notification from lldb-mi, so pretend we have received one. // see MIDebugSession::processNotification(const MI::AsyncRecord & async) setDebuggerStateOff(s_appNotStarted | s_programExited); setDebuggerStateOn(s_programExited | s_core); } void DebugSession::handleVersion(const QStringList& s) { m_hasCorrectCLIOutput = !s.isEmpty(); if (!m_hasCorrectCLIOutput) { // No output from 'version' command. It's likely that // the lldb used is not patched for the CLI output if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } auto ans = KMessageBox::warningYesNo( qApp->activeWindow(), i18n("Your lldb-mi version is unsupported, as it lacks an essential patch.
" "See https://llvm.org/bugs/show_bug.cgi?id=28026 for more information.
" "Debugger console will be disabled to prevent crash.
" "Do you want to continue?"), i18n("LLDB Version Unsupported"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("unsupported-lldb-debugger")); if (ans == KMessageBox::ButtonCode::No) { programFinished(QStringLiteral("Stopped because of unsupported LLDB version")); stopDebugger(); } return; } qCDebug(DEBUGGERLLDB) << s.first(); // minimal version is 3.8.1 #ifdef Q_OS_OSX QRegularExpression rx("^lldb-(\\d+).(\\d+).(\\d+)\\b", QRegularExpression::MultilineOption); // lldb 3.8.1 reports version 350.99.0 on OS X const int min_ver[] = {350, 99, 0}; #else QRegularExpression rx(QStringLiteral("^lldb version (\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption); const int min_ver[] = {3, 8, 1}; #endif auto match = rx.match(s.first()); int version[] = {0, 0, 0}; if (match.hasMatch()) { for (int i = 0; i != 3; ++i) { version[i] = match.captured(i+1).toInt(); } } bool ok = true; for (int i = 0; i < 3; ++i) { if (version[i] < min_ver[i]) { ok = false; break; } else if (version[i] > min_ver[i]) { ok = true; break; } } if (!ok) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } KMessageBox::error( qApp->activeWindow(), i18n("You need lldb-mi from LLDB 3.8.1 or higher.
" "You are using: %1", s.first()), i18n("LLDB Error")); stopDebugger(); } } void DebugSession::updateAllVariables() { // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist // for variables that have a python synthetic provider. Remove this after this is fixed // in the upstream. // re-fetch all toplevel variables, as -var-update doesn't work with data formatter // we have to pick out top level variables first, as refetching will delete child // variables. QList toplevels; for (auto it = m_allVariables.begin(), ite = m_allVariables.end(); it != ite; ++it) { LldbVariable *var = qobject_cast(it.value()); if (var->topLevel()) { toplevels << var; } } - for (auto var : toplevels) { + for (auto var : qAsConst(toplevels)) { var->refetch(); } } void DebugSession::handleSessionStateChange(IDebugSession::DebuggerState state) { if (state == IDebugSession::PausedState) { // session is paused, the user can input any commands now. // Turn off delete duplicate breakpoints mode, as the user // may intentionaly want to do this. qCDebug(DEBUGGERLLDB) << "Turn off delete duplicate mode"; breakpointController()->setDeleteDuplicateBreakpoints(false); } } diff --git a/plugins/lldb/unittests/test_lldbformatters.cpp b/plugins/lldb/unittests/test_lldbformatters.cpp index b25f1e0cef..ccf1f8affd 100644 --- a/plugins/lldb/unittests/test_lldbformatters.cpp +++ b/plugins/lldb/unittests/test_lldbformatters.cpp @@ -1,1032 +1,1032 @@ /* * Unit tests for LLDB debugger plugin * Copyright 2016 Aetf * * 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 "test_lldbformatters.h" #include "controllers/variable.h" #include "controllers/variablecontroller.h" #include "debugsession.h" #include "stringhelpers.h" #include "tests/testhelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define WAIT_FOR_STATE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__)) return; } while (0) #define WAIT_FOR_STATE_AND_IDLE(session, state) \ do { if (!KDevMI::waitForState((session), (state), __FILE__, __LINE__, true)) return; } while (0) #define WAIT_FOR_A_WHILE_AND_IDLE(session, ms) \ do { if (!KDevMI::waitForAWhile((session), (ms), __FILE__, __LINE__)) return; \ if (!KDevMI::waitForState((session), DebugSession::PausedState, __FILE__, __LINE__, true)) \ return; \ } while (0) #define WAIT_FOR(session, condition) \ do { \ KDevMI::TestWaiter w((session), #condition, __FILE__, __LINE__); \ while (w.waitUnless((condition))) /* nothing */ ; \ } while(0) #define COMPARE_DATA(index, expected) \ do { if (!KDevMI::compareData((index), (expected), __FILE__, __LINE__)) return; } while (0) #define VERIFY_LOCAL(row, name, summary, children) \ do { \ if (!verifyVariable((row), (name), (summary), (children), __FILE__, __LINE__)) \ return; \ } while (0) #define VERIFY_WATCH(row, name, summary, children) \ do { \ if (!verifyVariable((row), (name), (summary), (children), __FILE__, __LINE__, false)) \ return; \ } while (0) using namespace KDevelop; using namespace KDevMI::LLDB; using KDevMI::findExecutable; using KDevMI::findSourceFile; using KDevMI::findFile; using KDevMI::compareData; class TestLaunchConfiguration : public ILaunchConfiguration { public: explicit TestLaunchConfiguration(const QString& executable, const QUrl& workingDirectory = QUrl()) { auto execPath = findExecutable(executable); qDebug() << "FIND" << execPath; c = KSharedConfig::openConfig(); c->deleteGroup("launch"); cfg = c->group("launch"); cfg.writeEntry("isExecutable", true); cfg.writeEntry("Executable", execPath); cfg.writeEntry("Working Directory", workingDirectory); } const KConfigGroup config() const override { return cfg; } KConfigGroup config() override { return cfg; }; QString name() const override { return QStringLiteral("Test-Launch"); } KDevelop::IProject* project() const override { return nullptr; } KDevelop::LaunchConfigurationType* type() const override { return nullptr; } private: KConfigGroup cfg; KSharedConfigPtr c; }; class TestDebugSession : public DebugSession { Q_OBJECT public: TestDebugSession() : DebugSession() { setSourceInitFile(false); // explicit set formatter path to force use in-tree formatters, not the one installed in system. auto formatter = findFile(LLDB_SRC_DIR, "formatters/all.py"); setFormatterPath(formatter); KDevelop::ICore::self()->debugController()->addSession(this); variableController()->setAutoUpdate(IVariableController::UpdateLocals); } }; VariableCollection *LldbFormattersTest::variableCollection() { return m_core->debugController()->variableCollection(); } QModelIndex LldbFormattersTest::watchVariableIndexAt(int i, int col) { auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); return variableCollection()->index(i, col, watchRoot); } QModelIndex LldbFormattersTest::localVariableIndexAt(int i, int col) { auto localRoot = variableCollection()->indexForItem(variableCollection()->locals(), 0); return variableCollection()->index(i, col, localRoot); } // Note: line is zero-based KDevelop::Breakpoint* LldbFormattersTest::addCodeBreakpoint(const QUrl& location, int line) { return m_core->debugController()->breakpointModel()->addCodeBreakpoint(location, line); } // Called before the first testfunction is executed void LldbFormattersTest::initTestCase() { AutoTestShell::init({QStringLiteral("kdevlldb"), QStringLiteral("kdevexecute")}); m_core = TestCore::initialize(Core::NoUi); m_iface = m_core->pluginController() ->pluginForExtension(QStringLiteral("org.kdevelop.IExecutePlugin"), QStringLiteral("kdevexecute")) ->extension(); Q_ASSERT(m_iface); const QString lldbMiExecutable = QStandardPaths::findExecutable(QStringLiteral("lldb-mi")); if (lldbMiExecutable.isEmpty()) { QSKIP("Skipping, lldb-mi not available"); } } // Called after the last testfunction was executed void LldbFormattersTest::cleanupTestCase() { TestCore::shutdown(); } // Called before each testfunction is executed void LldbFormattersTest::init() { //remove all breakpoints - so we can set our own in the test KConfigGroup bpCfg = KSharedConfig::openConfig()->group("breakpoints"); bpCfg.writeEntry("number", 0); bpCfg.sync(); auto count = m_core->debugController()->breakpointModel()->rowCount(); m_core->debugController()->breakpointModel()->removeRows(0, count); while (variableCollection()->watches()->childCount() > 0) { auto idx = watchVariableIndexAt(0); auto var = dynamic_cast(variableCollection()->itemForIndex(idx)); if (!var) break; var->die(); } m_session = new TestDebugSession; } void LldbFormattersTest::cleanup() { // Called after every testfunction if (m_session) m_session->stopDebugger(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); m_session.clear(); } bool LldbFormattersTest::verifyVariable(int index, const QString &name, const QString &expectedSummary, QStringList expectedChildren, const char *file, int line, bool isLocal, bool useRE, bool unordered) { QList> childrenPairs; childrenPairs.reserve(expectedChildren.size()); if (unordered) { qDebug() << "useRE set to true when unordered = true"; useRE = true; expectedChildren.sort(); - for (auto c : expectedChildren) { + for (auto& c : expectedChildren) { childrenPairs << qMakePair(QStringLiteral(R"(^\[\d+\]$)"), c); } } else { for (int i = 0; i != expectedChildren.size(); ++i) { childrenPairs << qMakePair(QStringLiteral("[%0]").arg(i), expectedChildren[i]); } } return verifyVariable(index, name, expectedSummary, childrenPairs, file, line, isLocal, useRE, unordered); } bool LldbFormattersTest::verifyVariable(int index, const QString &name, const QString &expectedSummary, QList> expectedChildren, const char *file, int line, bool isLocal, bool useRE, bool unordered) { QModelIndex varIdx, summaryIdx; if (isLocal) { varIdx = localVariableIndexAt(index, 0); summaryIdx = localVariableIndexAt(index, 1); } else { varIdx = watchVariableIndexAt(index, 0); summaryIdx = watchVariableIndexAt(index, 1); } if (!compareData(varIdx, name, file, line)) { return false; } if (!compareData(summaryIdx, expectedSummary, file, line, useRE)) { return false; } // fetch all children auto var = variableCollection()->itemForIndex(varIdx); auto childCount = 0; while (childCount != variableCollection()->rowCount(varIdx)) { childCount = variableCollection()->rowCount(varIdx); var->fetchMoreChildren(); if (!waitForAWhile(m_session, 50, file, line)) return false; } if (childCount != expectedChildren.length()) { QTest::qFail(qPrintable(QString("'%0' didn't match expected '%1' in %2:%3") .arg(childCount).arg(expectedChildren.length()).arg(file).arg(line)), file, line); return false; } QVector theOrder; theOrder.reserve(childCount); for (int i = 0; i != childCount; ++i) { theOrder.push_back(i); } if (unordered) { qDebug() << "actual list sorted for unordered compare"; std::sort(theOrder.begin(), theOrder.end(), [&](int a, int b){ auto indexA = variableCollection()->index(a, 1, varIdx); auto indexB = variableCollection()->index(b, 1, varIdx); return indexA.model()->data(indexA, Qt::DisplayRole).toString() < indexB.model()->data(indexB, Qt::DisplayRole).toString(); }); std::sort(expectedChildren.begin(), expectedChildren.end(), [](const QPair &a, const QPair &b){ return a.second < b.second; }); qDebug() << "sorted actual order" << theOrder; qDebug() << "sorted expectedChildren" << expectedChildren; } for (int i = 0; i != childCount; ++i) { if (!compareData(variableCollection()->index(theOrder[i], 0, varIdx), expectedChildren[i].first, file, line, useRE)) { return false; } if (!compareData(variableCollection()->index(theOrder[i], 1, varIdx), expectedChildren[i].second, file, line, useRE)) { return false; } } return true; } void LldbFormattersTest::testQChar() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qchar")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qchar.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children << qMakePair(QStringLiteral("ucs"), QStringLiteral("107")); VERIFY_LOCAL(0, "c", "'k'", children); } void LldbFormattersTest::testQString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qstring.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QString expected = QStringLiteral("test最后一个不是特殊字符'\"\\u6211"); QStringList children; for (auto ch : expected) { children << Utils::quote(ch, '\''); } VERIFY_LOCAL(0, "s", Utils::quote(expected), children); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 5); expected.append("x"); children << QStringLiteral("'x'"); VERIFY_LOCAL(0, "s", Utils::quote(expected), children); m_session->run(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); } void LldbFormattersTest::testQByteArray() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qbytearray")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qbytearray.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QStringList charlist { R"(-26 '\xe6')", R"(-104 '\x98')", R"(-81 '\xaf')", R"(39 ''')", R"(34 '"')", R"(92 '\')", R"(117 'u')", R"(54 '6')", R"(50 '2')", R"(49 '1')", R"(49 '1')", }; VERIFY_LOCAL(0, "ba", R"("\xe6\x98\xaf'\"\\u6211")", charlist); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 5); charlist << QStringLiteral("120 'x'"); VERIFY_LOCAL(0, "ba", R"("\xe6\x98\xaf'\"\\u6211x")", charlist); m_session->run(); WAIT_FOR_STATE(m_session, DebugSession::EndedState); } void LldbFormattersTest::testQListContainer_data() { QTest::addColumn("container"); QTest::addColumn("unordered"); QTest::newRow("QList") << "QList" << false; QTest::newRow("QQueue") << "QQueue" << false; QTest::newRow("QVector") << "QVector" << false; QTest::newRow("QStack") << "QStack" << false; QTest::newRow("QLinkedList") << "QLinkedList" << false; QTest::newRow("QSet") << "QSet" << true; } void LldbFormattersTest::testQListContainer() { QFETCH(QString, container); QFETCH(bool, unordered); TestLaunchConfiguration cfg(QStringLiteral("debuggee_qlistcontainer")); cfg.config().writeEntry(KDevMI::Config::BreakOnStartEntry, true); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); m_session->addUserCommand(QStringLiteral("break set --func doStuff<%1>()").arg(container)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); m_session->run(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 33); // line 34: intList << 10 << 20; auto var = variableCollection()->watches()->add(QStringLiteral("intList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 34); // line 35: intList << 30; variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{"10", "20"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 36); // line 37: Container stringList; if (!verifyVariable(0, QStringLiteral("intList"), QStringLiteral(""), QStringList{"10", "20", "30"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 37); // line 38: stringList << "a" << "bc"; var = variableCollection()->watches()->add(QStringLiteral("stringList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 38); // line 39: stringList << "d"; variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{"\"a\"", "\"bc\""}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 40); // line 41: Container structList; if (!verifyVariable(0, QStringLiteral("stringList"), QStringLiteral(""), QStringList{"\"a\"", "\"bc\"", "\"d\""}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 41); // line 42: structList << A(QStringLiteral("a"), QStringLiteral("b"), 100, -200); var = variableCollection()->watches()->add(QStringLiteral("structList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->runUntil({}, 43); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 42); // line 43: structList << A(); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{"{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 44); // line 45: Container pointerList; if (!verifyVariable(0, QStringLiteral("structList"), QStringLiteral(""), QStringList{"{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); // m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 45); // line 46: pointerList << new int(1) << new int(2); var = variableCollection()->watches()->add(QStringLiteral("pointerList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 46); // line 47: pointerList << new int(3); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{"^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$"}, __FILE__, __LINE__, false, true, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 47); // line 48: qDeleteAll(pointerList); if (!verifyVariable(0, QStringLiteral("pointerList"), QStringLiteral(""), QStringList{"^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$", "^0x[0-9A-Fa-f]+$"}, __FILE__, __LINE__, false, true, unordered)) { return; } var->die(); m_session->stepOver(); // step over qDeleteAll // > m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 50); // line 51: pairList << QPair(1, 2) << qMakePair(2, 3); var = variableCollection()->watches()->add(QStringLiteral("pairList")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_WATCH(0, "pairList", "", QStringList{}); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 51); // line 52: pairList << qMakePair(4, 5); variableCollection()->expanded(watchVariableIndexAt(0)); // expand this node for correct update. WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("pairList"), QStringLiteral(""), QStringList{"{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 54); // line 55: int i = 0; if (!verifyVariable(0, QStringLiteral("pairList"), QStringLiteral(""), QStringList{"{...}", "{...}", "{...}"}, __FILE__, __LINE__, false, false, unordered)) { return; } var->die(); } void LldbFormattersTest::testQListPOD() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qlistpod")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qlistpod.cpp")), 30); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("b")); variableCollection()->watches()->add(QStringLiteral("c")); variableCollection()->watches()->add(QStringLiteral("uc")); variableCollection()->watches()->add(QStringLiteral("s")); variableCollection()->watches()->add(QStringLiteral("us")); variableCollection()->watches()->add(QStringLiteral("i")); variableCollection()->watches()->add(QStringLiteral("ui")); variableCollection()->watches()->add(QStringLiteral("l")); variableCollection()->watches()->add(QStringLiteral("ul")); variableCollection()->watches()->add(QStringLiteral("i64")); variableCollection()->watches()->add(QStringLiteral("ui64")); variableCollection()->watches()->add(QStringLiteral("f")); variableCollection()->watches()->add(QStringLiteral("d")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_WATCH(0, "b", "", (QStringList{"false"})); VERIFY_WATCH(1, "c", "", (QStringList{"50 '2'"})); VERIFY_WATCH(2, "uc", "", (QStringList{"50 '2'"})); VERIFY_WATCH(3, "s", "", (QStringList{"50"})); VERIFY_WATCH(4, "us", "", (QStringList{"50"})); VERIFY_WATCH(5, "i", "", (QStringList{"50"})); VERIFY_WATCH(6, "ui", "", (QStringList{"50"})); VERIFY_WATCH(7, "l", "", (QStringList{"50"})); VERIFY_WATCH(8, "ul", "", (QStringList{"50"})); VERIFY_WATCH(9, "i64", "", (QStringList{"50"})); VERIFY_WATCH(10, "ui64", "", (QStringList{"50"})); VERIFY_WATCH(11, "f", "", (QStringList{"50"})); VERIFY_WATCH(12, "d", "", (QStringList{"50"})); } void LldbFormattersTest::testQMapInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(10, 100)", "(20, 200)"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); VERIFY_LOCAL(0, "m", "", (QStringList{"(10, 100)", "(20, 200)", "(30, 300)"})); } void LldbFormattersTest::testQMapString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); // line 8: m[QStringLiteral("30")] = QStringLiteral("300"); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", \"100\")", "(\"20\", \"200\")"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); // line 9: return 0; VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", \"100\")", "(\"20\", \"200\")", "(\"30\", \"300\")"})); } void LldbFormattersTest::testQMapStringBool() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qmapstringbool")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qmapstringbool.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", true)", "(\"20\", false)"})); m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); VERIFY_LOCAL(0, "m", "", (QStringList{"(\"10\", true)", "(\"20\", false)", "(\"30\", true)"})); } void LldbFormattersTest::testQHashInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qhashint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qhashint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), {"(10, 100)", "(20, 200)"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), {"(10, 100)", "(20, 200)", "(30, 300)"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQHashString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qhashstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qhashstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), {"(\"10\", \"100\")", "(\"20\", \"200\")"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); if (!verifyVariable(0, QStringLiteral("h"), QStringLiteral(""), {"(\"10\", \"100\")", "(\"20\", \"200\")", "(\"30\", \"300\")"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQSetInt() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qsetint")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qsetint.cpp")), 6); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), {"10", "20"}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 7); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), {"10", "20", "30"}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQSetString() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qsetstring")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qsetstring.cpp")), 7); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), {"\"10\"", "\"20\""}, __FILE__, __LINE__, true, false, true)) { return; } m_session->stepOver(); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); QCOMPARE(m_session->currentLine(), 8); if (!verifyVariable(0, QStringLiteral("s"), QStringLiteral(""), {"\"10\"", "\"20\"", "\"30\""}, __FILE__, __LINE__, true, false, true)) { return; } } void LldbFormattersTest::testQDate() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qdate")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qdate.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("jd"), QStringLiteral("2455217")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"2010-01-20\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale) and summary are locale dependent if (!verifyVariable(0, QStringLiteral("d"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQTime() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qtime")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qtime.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("mds"), QStringLiteral("55810123")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"15:30:10.000123\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale) and summary are locale dependent if (!verifyVariable(0, QStringLiteral("t"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQDateTime() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qdatetime")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qdatetime.cpp")), 5); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("toTime_t"), QStringLiteral("1264019473")}); children.append({QStringLiteral("(ISO)"), QStringLiteral("\"2010-01-20 20:31:13\"")}); children.append({QStringLiteral("(Locale)"), QStringLiteral("\".+\"")}); // (Locale), (UTC) and summary are locale dependent children.append({QStringLiteral("(UTC)"), QStringLiteral("\".+\"")}); if (!verifyVariable(0, QStringLiteral("dt"), QStringLiteral(".+"), children, __FILE__, __LINE__, true, true, false)) { return; } } void LldbFormattersTest::testQUrl() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_qurl")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("qurl.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> children; children.append({QStringLiteral("(port)"), QStringLiteral("12345")}); children.append({QStringLiteral("(scheme)"), QStringLiteral("\"http\"")}); children.append({QStringLiteral("(userName)"), QStringLiteral("\"user\"")}); children.append({QStringLiteral("(password)"), QStringLiteral("")}); children.append({QStringLiteral("(host)"), QStringLiteral("\"www.kdevelop.org\"")}); children.append({QStringLiteral("(path)"), QStringLiteral("\"/foo\"")}); children.append({QStringLiteral("(query)"), QStringLiteral("\"xyz=bar\"")}); children.append({QStringLiteral("(fragment)"), QStringLiteral("\"asdf\"")}); VERIFY_LOCAL(0, "u", "\"http://user@www.kdevelop.org:12345/foo?xyz=bar#asdf\"", children); } void LldbFormattersTest::testQUuid() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_quuid")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("quuid.cpp")), 4); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); variableCollection()->expanded(localVariableIndexAt(0)); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); VERIFY_LOCAL(0, "id", "QUuid({9ec3b70b-d105-42bf-b3b4-656e44d2e223})", (QStringList{})); } void LldbFormattersTest::testKTextEditorTypes() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_ktexteditortypes")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("ktexteditortypes.cpp")), 8); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("cursor")); variableCollection()->watches()->add(QStringLiteral("range")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> cursorChildren; cursorChildren.append({QStringLiteral("m_line"), QStringLiteral("1")}); cursorChildren.append({QStringLiteral("m_column"), QStringLiteral("1")}); QList> rangeChildren; rangeChildren.append({QStringLiteral("m_start"), QStringLiteral("(1, 1)")}); rangeChildren.append({QStringLiteral("m_end"), QStringLiteral("(2, 2)")}); VERIFY_WATCH(0, "cursor", "(1, 1)", cursorChildren); VERIFY_WATCH(1, "range", "[(1, 1) -> (2, 2)]", rangeChildren); } void LldbFormattersTest::testKDevelopTypes() { TestLaunchConfiguration cfg(QStringLiteral("debuggee_kdeveloptypes")); addCodeBreakpoint(QUrl::fromLocalFile(findSourceFile("kdeveloptypes.cpp")), 11); QVERIFY(m_session->startDebugging(&cfg, m_iface)); WAIT_FOR_STATE_AND_IDLE(m_session, DebugSession::PausedState); // Should be two rows ('auto', 'local') QCOMPARE(variableCollection()->rowCount(), 2); auto watchRoot = variableCollection()->indexForItem(variableCollection()->watches(), 0); variableCollection()->expanded(watchRoot); variableCollection()->variableWidgetShown(); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); variableCollection()->watches()->add(QStringLiteral("path1")); variableCollection()->watches()->add(QStringLiteral("path2")); WAIT_FOR_A_WHILE_AND_IDLE(m_session, 50); QList> path1Children; path1Children.append({QStringLiteral("m_data"), QStringLiteral("")}); QList> path2Children; path2Children.append({QStringLiteral("m_data"), QStringLiteral("")}); VERIFY_WATCH(0, "path1", "(\"tmp\", \"foo\")", path1Children); VERIFY_WATCH(1, "path2", "(\"http://www.test.com\", \"tmp\", \"asdf.txt\")", path2Children); } QTEST_MAIN(LldbFormattersTest) #include "test_lldbformatters.moc" diff --git a/plugins/perforce/ui/perforceimportmetadatawidget.cpp b/plugins/perforce/ui/perforceimportmetadatawidget.cpp index b2add33991..b2f702abeb 100644 --- a/plugins/perforce/ui/perforceimportmetadatawidget.cpp +++ b/plugins/perforce/ui/perforceimportmetadatawidget.cpp @@ -1,211 +1,212 @@ /*************************************************************************** * This file is part of KDevelop Perforce plugin, KDE project * * * * Copyright 2018 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 "perforceimportmetadatawidget.h" #include #include #include #include #include using namespace KDevelop; PerforceImportMetadataWidget::PerforceImportMetadataWidget(QWidget* parent) : VcsImportMetadataWidget(parent) , m_ui(new Ui::PerforceImportMetadataWidget) { m_ui->setupUi(this); m_ui->executableLoc->setText("/usr/bin/p4"); m_ui->p4portEdit->setText("perforce:1666"); QProcessEnvironment curEnv = QProcessEnvironment::systemEnvironment(); m_ui->p4configEdit->setText(curEnv.contains("P4CONFIG") ? curEnv.value("P4CONFIG") : ""); m_ui->p4portEdit->setText(curEnv.contains("P4PORT") ? curEnv.value("P4PORT") : ""); m_ui->p4userEdit->setText(curEnv.contains("P4USER") ? curEnv.value("P4USER") : ""); curEnv.contains("P4CONFIG") ? m_ui->radioButtonConfig->setChecked(true) : m_ui->radioButtonVariables->setChecked(true); curEnv.contains("P4CONFIG") ? m_ui->p4configEdit->setEnabled(true) : m_ui->p4configEdit->setEnabled(false); m_ui->sourceLoc->setEnabled(false); m_ui->sourceLoc->setMode(KFile::Directory); m_ui->errorMsg->setTextColor(QColor(255, 0, 0)); m_ui->errorMsg->setReadOnly(true); m_ui->p4clientEdit->setEditable(true); connect(m_ui->p4clientEdit, static_cast(&KComboBox::returnPressed), this, &PerforceImportMetadataWidget::changed ); connect(m_ui->radioButtonConfig, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setEnabled); connect(m_ui->radioButtonVariables, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setDisabled); connect(m_ui->testP4setupButton, &QPushButton::pressed, this, &PerforceImportMetadataWidget::testP4setup); } QUrl PerforceImportMetadataWidget::source() const { return m_ui->sourceLoc->url(); } VcsLocation PerforceImportMetadataWidget::destination() const { VcsLocation dest; dest.setRepositoryServer(m_ui->p4portEdit->text()); dest.setUserData(QVariant::fromValue(m_ui->p4userEdit->text())); dest.setRepositoryBranch(m_ui->p4clientEdit->itemText(0)); return dest; } QString PerforceImportMetadataWidget::message() const { return QString(); //TODO: return m_ui->message->toPlainText(); } void PerforceImportMetadataWidget::setSourceLocation(const VcsLocation& url) { m_ui->sourceLoc->setUrl(url.localUrl()); } void PerforceImportMetadataWidget::setSourceLocationEditable(bool enable) { m_ui->sourceLoc->setEnabled(enable); } void PerforceImportMetadataWidget::setMessage(const QString& message) { Q_UNUSED(message); //FIXME: correct ui field needs to be set //m_ui->message->setText(message); } bool PerforceImportMetadataWidget::hasValidData() const { // FIXME: It has valid data if testP4setup has completed correctly. AND client name has been set to something return !m_ui->p4clientEdit->itemText(0).isEmpty(); } void PerforceImportMetadataWidget::testP4setup() { m_ui->errorMsg->clear(); m_ui->p4clientEdit->clear(); if (!validateP4executable()) return; QDir execDir(m_ui->sourceLoc->url().toLocalFile()); QTemporaryDir tmpDir; if (!execDir.exists()) execDir = tmpDir.path(); if(!validateP4port(execDir.path())) return; if(!validateP4user(execDir.path())) return; emit changed(); } bool PerforceImportMetadataWidget::validateP4executable() { if (QStandardPaths::findExecutable(m_ui->executableLoc->url().toLocalFile()).isEmpty()) { m_ui->errorMsg->setText("Unable to find perforce executable. Is it installed on the system? Is it in your PATH?"); return false; } return true; } bool PerforceImportMetadataWidget::validateP4user(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList{QStringLiteral("workspaces"), QStringLiteral("-u"), m_ui->p4userEdit->text()} ); exec.waitForFinished(); QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); // std::cout << "Exited with code: " << exec.exitCode() << std::endl; // std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; // std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with exit code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } if(!processStdout.isEmpty()) { QStringList clientCmdOutput = processStdout.split(QLatin1Char('\n'),QString::SkipEmptyParts); QStringList clientItems; clientItems.reserve(clientCmdOutput.size()); for(QString const& clientLine : clientCmdOutput) { QStringList wordsInLine = clientLine.split(QLatin1Char(' ')); // Client mvo_testkdevinteg 2017/05/22 root C:\P4repo 'Created by mvo. ' -- Line would be expected to look like so clientItems.append(wordsInLine.at(1)); } m_ui->p4clientEdit->addItems(clientItems); } return true; } bool PerforceImportMetadataWidget::validateP4port(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); QTextStream out(stdout); - for (QString x : p4execEnvironment.toStringList()) { + const auto& env = p4execEnvironment.toStringList(); + for (const QString& x : env) { out << x << endl; } exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList() << QStringLiteral("info")); exec.waitForFinished(); //QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); //std::cout << "Exited with code: " << exec.exitCode() << std::endl; //std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; //std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with error code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } return true; } diff --git a/plugins/projectmanagerview/cutcopypastehelpers.cpp b/plugins/projectmanagerview/cutcopypastehelpers.cpp index 8a0e0cca5a..7b00f23a6a 100644 --- a/plugins/projectmanagerview/cutcopypastehelpers.cpp +++ b/plugins/projectmanagerview/cutcopypastehelpers.cpp @@ -1,353 +1,354 @@ /* This file is part of KDevelop Copyright (C) 2017 Alexander Potashev 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 "cutcopypastehelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace CutCopyPasteHelpers { TaskInfo::TaskInfo(const TaskStatus status, const TaskType type, const Path::List& src, const Path& dest) : m_status(status), m_type(type), m_src(src), m_dest(dest) { } TaskInfo TaskInfo::createMove(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::MOVE, src, dest); } TaskInfo TaskInfo::createCopy(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::COPY, src, dest); } TaskInfo TaskInfo::createDeletion(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::DELETION, src, dest); } static QWidget* createPasteStatsWidget(QWidget *parent, const QVector& tasks) { // TODO: Create a model for the task list, and use it here instead of using QTreeWidget QTreeWidget* treeWidget = new QTreeWidget(parent); QList items; items.reserve(tasks.size()); for (const TaskInfo& task : tasks) { int srcCount = task.m_src.size(); const bool withChildren = srcCount != 1; const QString destPath = task.m_dest.pathOrUrl(); QString text; if (withChildren) { // Multiple source items in the current suboperation switch (task.m_type) { case TaskType::MOVE: text = i18np("Move %1 item into %2", "Move %1 items into %2", srcCount, destPath); break; case TaskType::COPY: text = i18np("Copy %1 item into %2", "Copy %1 items into %2", srcCount, destPath); break; case TaskType::DELETION: text = i18np("Delete %1 item", "Delete %1 items", srcCount); break; } } else { // One source item in the current suboperation const QString srcPath = task.m_src[0].pathOrUrl(); switch (task.m_type) { case TaskType::MOVE: text = i18n("Move item %1 into %2", srcPath, destPath); break; case TaskType::COPY: text = i18n("Copy item %1 into %2", srcPath, destPath); break; case TaskType::DELETION: text = i18n("Delete item %1", srcPath); break; } } QString tooltip; QString iconName; switch (task.m_status) { case TaskStatus::SUCCESS: tooltip = i18n("Suboperation succeeded"); iconName = QStringLiteral("dialog-ok"); break; case TaskStatus::FAILURE: tooltip = i18n("Suboperation failed"); iconName = QStringLiteral("dialog-error"); break; case TaskStatus::SKIPPED: tooltip = i18n("Suboperation skipped to prevent data loss"); iconName = QStringLiteral("dialog-warning"); break; } QTreeWidgetItem* item = new QTreeWidgetItem; item->setText(0, text); item->setIcon(0, QIcon::fromTheme(iconName)); item->setToolTip(0, tooltip); items.append(item); if (withChildren) { for (const Path& src : task.m_src) { QTreeWidgetItem* childItem = new QTreeWidgetItem; childItem->setText(0, src.pathOrUrl()); item->addChild(childItem); } } } treeWidget->insertTopLevelItems(0, items); treeWidget->headerItem()->setHidden(true); return treeWidget; } SourceToDestinationMap mapSourceToDestination(const Path::List& sourcePaths, const Path& destinationPath) { // For example you are moving the following items into /dest/ // * /tests/ // * /tests/abc.cpp // If you pass them as is, moveFilesAndFolders() will crash (see note: // "Do not attempt to move subitems along with their parents"). // Thus we filter out subitems from "Path::List filteredPaths". // // /tests/abc.cpp will be implicitly moved to /dest/tests/abc.cpp, for // that reason we add "/dest/tests/abc.cpp" into "result.finalPaths" as well as // "/dest/tests". // // "result.finalPaths" will be used to highlight destination items after // copy/move. Path::List sortedPaths = sourcePaths; std::sort(sortedPaths.begin(), sortedPaths.end()); SourceToDestinationMap result; for (const Path& path : sortedPaths) { if (!result.filteredPaths.isEmpty() && result.filteredPaths.back().isParentOf(path)) { // think: "/tests" const Path& previousPath = result.filteredPaths.back(); // think: "/dest" + "/".relativePath("/tests/abc.cpp") = /dest/tests/abc.cpp result.finalPaths[previousPath].append(Path(destinationPath, previousPath.parent().relativePath(path))); } else { // think: "/tests" result.filteredPaths.append(path); // think: "/dest" + "tests" = "/dest/tests" result.finalPaths[path].append(Path(destinationPath, path.lastPathSegment())); } } return result; } struct ClassifiedPaths { // Items originating from projects open in this KDevelop session QHash> itemsPerProject; // Items that do not belong to known projects Path::List alienSrcPaths; }; static ClassifiedPaths classifyPaths(const Path::List& paths, KDevelop::ProjectModel* projectModel) { ClassifiedPaths result; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); if (!items.empty()) { for (ProjectBaseItem* item : items) { IProject* project = item->project(); if (!result.itemsPerProject.contains(project)) { result.itemsPerProject[project] = QList(); } result.itemsPerProject[project].append(item); } } else { result.alienSrcPaths.append(path); } } return result; } QVector copyMoveItems(const Path::List& paths, ProjectBaseItem* destItem, const Operation operation) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); const ClassifiedPaths cl = classifyPaths(paths, projectModel); QVector tasks; IProject* destProject = destItem->project(); IProjectFileManager* destProjectFileManager = destProject->projectFileManager(); ProjectFolderItem* destFolder = destItem->folder(); Path destPath = destFolder->path(); - for (IProject* srcProject : cl.itemsPerProject.keys()) { + const auto& srcProjects = cl.itemsPerProject.keys(); + for (IProject* srcProject : srcProjects) { const auto& itemsList = cl.itemsPerProject[srcProject]; Path::List pathsList; pathsList.reserve(itemsList.size()); for (KDevelop::ProjectBaseItem* item : itemsList) { pathsList.append(item->path()); } if (srcProject == destProject) { if (operation == Operation::CUT) { // Move inside project const bool ok = destProjectFileManager->moveFilesAndFolders(itemsList, destFolder); tasks.append(TaskInfo::createMove(ok, pathsList, destPath)); } else { // Copy inside project const bool ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(ok, pathsList, destPath)); } } else { // Copy/move between projects: // 1. Copy and add into destination project; // 2. Remove from source project. const bool copy_ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(copy_ok, pathsList, destPath)); if (operation == Operation::CUT) { if (copy_ok) { IProjectFileManager* srcProjectFileManager = srcProject->projectFileManager(); const bool deletion_ok = srcProjectFileManager->removeFilesAndFolders(itemsList); tasks.append(TaskInfo::createDeletion(deletion_ok, pathsList, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, pathsList, destPath)); } } } } // Copy/move items from outside of all open projects if (!cl.alienSrcPaths.isEmpty()) { const bool alien_copy_ok = destProjectFileManager->copyFilesAndFolders(cl.alienSrcPaths, destFolder); tasks.append(TaskInfo::createCopy(alien_copy_ok, cl.alienSrcPaths, destPath)); if (operation == Operation::CUT) { if (alien_copy_ok) { QList urlsToDelete; urlsToDelete.reserve(cl.alienSrcPaths.size()); for (const Path& path : cl.alienSrcPaths) { urlsToDelete.append(path.toUrl()); } KIO::DeleteJob* deleteJob = KIO::del(urlsToDelete); const bool deletion_ok = deleteJob->exec(); tasks.append(TaskInfo::createDeletion(deletion_ok, cl.alienSrcPaths, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, cl.alienSrcPaths, destPath)); } } } return tasks; } void showWarningDialogForFailedPaste(QWidget* parent, const QVector& tasks) { QDialog* dialog = new QDialog(parent); dialog->setWindowTitle(i18nc("@title:window", "Paste Failed")); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok); QObject::connect(buttonBox, &QDialogButtonBox::clicked, dialog, &QDialog::accept); dialog->setWindowModality(Qt::WindowModal); dialog->setModal(true); QWidget* mainWidget = new QWidget(dialog); QVBoxLayout* mainLayout = new QVBoxLayout(mainWidget); const int spacingHint = mainWidget->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); mainLayout->setSpacing(spacingHint * 2); // provide extra spacing mainLayout->setMargin(0); QHBoxLayout* hLayout = new QHBoxLayout; hLayout->setMargin(0); hLayout->setSpacing(-1); // use default spacing mainLayout->addLayout(hLayout, 0); QLabel* iconLabel = new QLabel(mainWidget); // Icon QStyleOption option; option.initFrom(mainWidget); QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning")); iconLabel->setPixmap(icon.pixmap(mainWidget->style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, mainWidget))); QVBoxLayout* iconLayout = new QVBoxLayout(); iconLayout->addStretch(1); iconLayout->addWidget(iconLabel); iconLayout->addStretch(5); hLayout->addLayout(iconLayout, 0); hLayout->addSpacing(spacingHint); const QString text = i18n("Failed to paste. Below is a list of suboperations that have been attempted."); QLabel* messageLabel = new QLabel(text, mainWidget); messageLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); hLayout->addWidget(messageLabel, 5); QWidget* statsWidget = createPasteStatsWidget(dialog, tasks); QVBoxLayout* topLayout = new QVBoxLayout; dialog->setLayout(topLayout); topLayout->addWidget(mainWidget); topLayout->addWidget(statsWidget, 1); topLayout->addWidget(buttonBox); dialog->setMinimumSize(300, qMax(150, qMax(iconLabel->sizeHint().height(), messageLabel->sizeHint().height()))); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } // namespace CutCopyPasteHelpers diff --git a/plugins/projectmanagerview/projectmanagerviewplugin.cpp b/plugins/projectmanagerview/projectmanagerviewplugin.cpp index 071c4c5424..c520cebd60 100644 --- a/plugins/projectmanagerview/projectmanagerviewplugin.cpp +++ b/plugins/projectmanagerview/projectmanagerviewplugin.cpp @@ -1,796 +1,798 @@ /* This file is part of KDevelop Copyright 2004 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2016, 2017 Alexander Potashev 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 "projectmanagerviewplugin.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 "projectmanagerview.h" #include "debug.h" #include "cutcopypastehelpers.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ProjectManagerFactory, "kdevprojectmanagerview.json", registerPlugin();) namespace { QAction* createSeparatorAction() { QAction* separator = new QAction(nullptr); separator->setSeparator(true); return separator; } // Returns nullptr iff the list of URLs to copy/cut was empty QMimeData* createClipboardMimeData(const bool cut) { auto* ctx = dynamic_cast( ICore::self()->selectionController()->currentSelection()); QList urls; QList mostLocalUrls; - for (const ProjectBaseItem* item : ctx->items()) { + const auto& items = ctx->items(); + for (const ProjectBaseItem* item : items) { if (item->folder() || item->file()) { const QUrl& url = item->path().toUrl(); urls << url; mostLocalUrls << KFileItem(url).mostLocalUrl(); } } qCDebug(PLUGIN_PROJECTMANAGERVIEW) << urls; if (urls.isEmpty()) { return nullptr; } QMimeData* mimeData = new QMimeData; KIO::setClipboardDataCut(mimeData, cut); KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData); return mimeData; } } // anonymous namespace class KDevProjectManagerViewFactory: public KDevelop::IToolViewFactory { public: explicit KDevProjectManagerViewFactory( ProjectManagerViewPlugin *plugin ): mplugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new ProjectManagerView( mplugin, parent ); } Qt::DockWidgetArea defaultPosition() override { return Qt::LeftDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ProjectsView"); } private: ProjectManagerViewPlugin *mplugin; }; class ProjectManagerViewPluginPrivate { public: ProjectManagerViewPluginPrivate() {} KDevProjectManagerViewFactory *factory; QList ctxProjectItemList; QAction* m_buildAll; QAction* m_build; QAction* m_install; QAction* m_clean; QAction* m_configure; QAction* m_prune; }; static QList itemsFromIndexes(const QList& indexes) { QList items; ProjectModel* model = ICore::self()->projectController()->projectModel(); items.reserve(indexes.size()); for (const QModelIndex& index : indexes) { items += model->itemFromIndex(index); } return items; } ProjectManagerViewPlugin::ProjectManagerViewPlugin( QObject *parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevprojectmanagerview"), parent ), d(new ProjectManagerViewPluginPrivate) { d->m_buildAll = new QAction( i18n("Build all Projects"), this ); d->m_buildAll->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( d->m_buildAll, &QAction::triggered, this, &ProjectManagerViewPlugin::buildAllProjects ); actionCollection()->addAction( QStringLiteral("project_buildall"), d->m_buildAll ); d->m_build = new QAction( i18n("Build Selection"), this ); d->m_build->setIconText( i18n("Build") ); actionCollection()->setDefaultShortcut( d->m_build, Qt::Key_F8 ); d->m_build->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); d->m_build->setEnabled( false ); connect( d->m_build, &QAction::triggered, this, &ProjectManagerViewPlugin::buildProjectItems ); actionCollection()->addAction( QStringLiteral("project_build"), d->m_build ); d->m_install = new QAction( i18n("Install Selection"), this ); d->m_install->setIconText( i18n("Install") ); d->m_install->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); actionCollection()->setDefaultShortcut( d->m_install, Qt::SHIFT + Qt::Key_F8 ); d->m_install->setEnabled( false ); connect( d->m_install, &QAction::triggered, this, &ProjectManagerViewPlugin::installProjectItems ); actionCollection()->addAction( QStringLiteral("project_install"), d->m_install ); d->m_clean = new QAction( i18n("Clean Selection"), this ); d->m_clean->setIconText( i18n("Clean") ); d->m_clean->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); d->m_clean->setEnabled( false ); connect( d->m_clean, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanProjectItems ); actionCollection()->addAction( QStringLiteral("project_clean"), d->m_clean ); d->m_configure = new QAction( i18n("Configure Selection"), this ); d->m_configure->setMenuRole( QAction::NoRole ); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item d->m_configure->setIconText( i18n("Configure") ); d->m_configure->setIcon(QIcon::fromTheme(QStringLiteral("run-build-configure"))); d->m_configure->setEnabled( false ); connect( d->m_configure, &QAction::triggered, this, &ProjectManagerViewPlugin::configureProjectItems ); actionCollection()->addAction( QStringLiteral("project_configure"), d->m_configure ); d->m_prune = new QAction( i18n("Prune Selection"), this ); d->m_prune->setIconText( i18n("Prune") ); d->m_prune->setIcon(QIcon::fromTheme(QStringLiteral("run-build-prune"))); d->m_prune->setEnabled( false ); connect( d->m_prune, &QAction::triggered, this, &ProjectManagerViewPlugin::pruneProjectItems ); actionCollection()->addAction( QStringLiteral("project_prune"), d->m_prune ); // only add the action so that its known in the actionCollection // and so that it's shortcut etc. pp. is restored // apparently that is not possible to be done in the view itself *sigh* actionCollection()->addAction( QStringLiteral("locate_document") ); setXMLFile( QStringLiteral("kdevprojectmanagerview.rc") ); d->factory = new KDevProjectManagerViewFactory( this ); core()->uiController()->addToolView( i18n("Projects"), d->factory ); connect(core()->selectionController(), &ISelectionController::selectionChanged, this, &ProjectManagerViewPlugin::updateActionState); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsInserted, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsRemoved, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::modelReset, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); } void ProjectManagerViewPlugin::updateFromBuildSetChange() { updateActionState( core()->selectionController()->currentSelection() ); } void ProjectManagerViewPlugin::updateActionState( KDevelop::Context* ctx ) { bool isEmpty = ICore::self()->projectController()->buildSetModel()->items().isEmpty(); if( isEmpty ) { isEmpty = !ctx || ctx->type() != Context::ProjectItemContext || static_cast(ctx)->items().isEmpty(); } d->m_build->setEnabled( !isEmpty ); d->m_install->setEnabled( !isEmpty ); d->m_clean->setEnabled( !isEmpty ); d->m_configure->setEnabled( !isEmpty ); d->m_prune->setEnabled( !isEmpty ); } ProjectManagerViewPlugin::~ProjectManagerViewPlugin() { delete d; } void ProjectManagerViewPlugin::unload() { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "unloading manager view"; core()->uiController()->removeToolView(d->factory); } ContextMenuExtension ProjectManagerViewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { if( context->type() != KDevelop::Context::ProjectItemContext ) return IPlugin::contextMenuExtension(context, parent); KDevelop::ProjectItemContext* ctx = static_cast(context); const QList items = ctx->items(); d->ctxProjectItemList.clear(); if( items.isEmpty() ) return IPlugin::contextMenuExtension(context, parent); //TODO: also needs: removeTarget, removeFileFromTarget, runTargetsFromContextMenu ContextMenuExtension menuExt; bool needsCreateFile = true; bool needsCreateFolder = true; bool needsCloseProjects = true; bool needsBuildItems = true; bool needsFolderItems = true; bool needsCutRenameRemove = true; bool needsRemoveTargetFiles = true; bool needsPaste = true; //needsCreateFile if there is one item and it's a folder or target needsCreateFile &= (items.count() == 1) && (items.first()->folder() || items.first()->target()); //needsCreateFolder if there is one item and it's a folder needsCreateFolder &= (items.count() == 1) && (items.first()->folder()); needsPaste = needsCreateFolder; d->ctxProjectItemList.reserve(items.size()); for (ProjectBaseItem* item : items) { d->ctxProjectItemList << item->index(); //needsBuildItems if items are limited to targets and buildfolders needsBuildItems &= item->target() || item->type() == ProjectBaseItem::BuildFolder; //needsCloseProjects if items are limited to top level folders (Project Folders) needsCloseProjects &= item->folder() && !item->folder()->parent(); //needsFolderItems if items are limited to folders needsFolderItems &= (bool)item->folder(); //needsRemove if items are limited to non-top-level folders or files that don't belong to targets needsCutRenameRemove &= (item->folder() && item->parent()) || (item->file() && !item->parent()->target()); //needsRemoveTargets if items are limited to file items with target parents needsRemoveTargetFiles &= (item->file() && item->parent()->target()); } if ( needsCreateFile ) { QAction* action = new QAction(i18n("Create &File..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFileFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsCreateFolder ) { QAction* action = new QAction(i18n("Create F&older..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFolderFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsBuildItems ) { QAction* action = new QAction(i18nc("@action", "&Build"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::buildItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Install"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::installItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Clean"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18n("&Add to Build Set"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); } if ( needsCloseProjects ) { QAction* close = new QAction(i18np("C&lose Project", "Close Projects", items.count()), parent); close->setIcon(QIcon::fromTheme(QStringLiteral("project-development-close"))); connect( close, &QAction::triggered, this, &ProjectManagerViewPlugin::closeProjects ); menuExt.addAction( ContextMenuExtension::ProjectGroup, close ); } if ( needsFolderItems ) { QAction* action = new QAction(i18n("&Reload"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::reloadFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } // Populating cut/copy/paste group if ( !menuExt.actions(ContextMenuExtension::FileGroup).isEmpty() ) { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* cut = KStandardAction::cut(this, SLOT(cutFromContextMenu()), this); cut->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction(ContextMenuExtension::FileGroup, cut); } { QAction* copy = KStandardAction::copy(this, SLOT(copyFromContextMenu()), this); copy->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, copy ); } if (needsPaste) { QAction* paste = KStandardAction::paste(this, SLOT(pasteFromContextMenu()), this); paste->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, paste ); } // Populating rename/remove group { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* remove = new QAction(i18n("Remo&ve"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); QAction* rename = new QAction(i18n("Re&name..."), parent); rename->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect( rename, &QAction::triggered, this, &ProjectManagerViewPlugin::renameItemFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, rename ); } if ( needsRemoveTargetFiles ) { QAction* remove = new QAction(i18n("Remove From &Target"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeTargetFilesFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); } if ( needsCutRenameRemove || needsRemoveTargetFiles ) { menuExt.addAction(ContextMenuExtension::FileGroup, createSeparatorAction()); } return menuExt; } void ProjectManagerViewPlugin::closeProjects() { QList projectsToClose; ProjectModel* model = ICore::self()->projectController()->projectModel(); foreach( const QModelIndex& index, d->ctxProjectItemList ) { KDevelop::ProjectBaseItem* item = model->itemFromIndex(index); if( !projectsToClose.contains( item->project() ) ) { projectsToClose << item->project(); } } d->ctxProjectItemList.clear(); foreach( KDevelop::IProject* proj, projectsToClose ) { core()->projectController()->closeProject( proj ); } } void ProjectManagerViewPlugin::installItemsFromContextMenu() { runBuilderJob( BuilderJob::Install, itemsFromIndexes(d->ctxProjectItemList) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::cleanItemsFromContextMenu() { runBuilderJob( BuilderJob::Clean, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::buildItemsFromContextMenu() { runBuilderJob( BuilderJob::Build, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } QList ProjectManagerViewPlugin::collectAllProjects() { QList items; const auto projects = core()->projectController()->projects(); items.reserve(projects.size()); for (auto* project : projects) { items << project->projectItem(); } return items; } void ProjectManagerViewPlugin::buildAllProjects() { runBuilderJob( BuilderJob::Build, collectAllProjects() ); } QList ProjectManagerViewPlugin::collectItems() { QList items; const QList buildItems = ICore::self()->projectController()->buildSetModel()->items(); if( !buildItems.isEmpty() ) { for (const BuildItem& buildItem : buildItems) { if( ProjectBaseItem* item = buildItem.findItem() ) { items << item; } } } else { KDevelop::ProjectItemContext* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); items = ctx->items(); } return items; } void ProjectManagerViewPlugin::runBuilderJob( BuilderJob::BuildType type, const QList& items ) { BuilderJob* builder = new BuilderJob; builder->addItems( type, items ); builder->updateJobName(); ICore::self()->uiController()->registerStatus(new JobStatus(builder)); ICore::self()->runController()->registerJob( builder ); } void ProjectManagerViewPlugin::installProjectItems() { runBuilderJob( KDevelop::BuilderJob::Install, collectItems() ); } void ProjectManagerViewPlugin::pruneProjectItems() { runBuilderJob( KDevelop::BuilderJob::Prune, collectItems() ); } void ProjectManagerViewPlugin::configureProjectItems() { runBuilderJob( KDevelop::BuilderJob::Configure, collectItems() ); } void ProjectManagerViewPlugin::cleanProjectItems() { runBuilderJob( KDevelop::BuilderJob::Clean, collectItems() ); } void ProjectManagerViewPlugin::buildProjectItems() { runBuilderJob( KDevelop::BuilderJob::Build, collectItems() ); } void ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { ICore::self()->projectController()->buildSetModel()->addProjectItem( item ); } } void ProjectManagerViewPlugin::runTargetsFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { KDevelop::ProjectExecutableTargetItem* t=item->executable(); if(t) { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "Running target: " << t->text() << t->builtUrl(); } } } void ProjectManagerViewPlugin::projectConfiguration( ) { if( !d->ctxProjectItemList.isEmpty() ) { ProjectModel* model = ICore::self()->projectController()->projectModel(); core()->projectController()->configureProject( model->itemFromIndex(d->ctxProjectItemList.at( 0 ))->project() ); } } void ProjectManagerViewPlugin::reloadFromContextMenu( ) { QList< KDevelop::ProjectFolderItem* > folders; foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList ) ) { if ( item->folder() ) { // since reloading should be recursive, only pass the upper-most items bool found = false; foreach ( KDevelop::ProjectFolderItem* existing, folders ) { if ( existing->path().isParentOf(item->folder()->path()) ) { // simply skip this child found = true; break; } else if ( item->folder()->path().isParentOf(existing->path()) ) { // remove the child in the list and add the current item instead folders.removeOne(existing); // continue since there could be more than one existing child } } if ( !found ) { folders << item->folder(); } } } foreach( KDevelop::ProjectFolderItem* folder, folders ) { folder->project()->projectFileManager()->reload(folder); } } void ProjectManagerViewPlugin::createFolderFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { if ( item->folder() ) { QWidget* window(ICore::self()->uiController()->activeMainWindow()->window()); QString name = QInputDialog::getText ( window, i18n ( "Create Folder in %1", item->folder()->path().pathOrUrl() ), i18n ( "Folder name:" ) ); if (!name.isEmpty()) { item->project()->projectFileManager()->addFolder( Path(item->path(), name), item->folder() ); } } } } void ProjectManagerViewPlugin::removeFromContextMenu() { removeItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::removeItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } //copy the list of selected items and sort it to guarantee parents will come before children QList sortedItems = items; std::sort(sortedItems.begin(), sortedItems.end(), ProjectBaseItem::pathLessThan); Path lastFolder; QHash< IProjectFileManager*, QList > filteredItems; QStringList itemPaths; foreach( KDevelop::ProjectBaseItem* item, sortedItems ) { if (item->isProjectRoot()) { continue; } else if (item->folder() || item->file()) { //make sure no children of folders that will be deleted are listed if (lastFolder.isParentOf(item->path())) { continue; } else if (item->folder()) { lastFolder = item->path(); } IProjectFileManager* manager = item->project()->projectFileManager(); if (manager) { filteredItems[manager] << item; itemPaths << item->path().pathOrUrl(); } } } if (filteredItems.isEmpty()) { return; } if (KMessageBox::warningYesNoList( QApplication::activeWindow(), i18np("Do you really want to delete this item?", "Do you really want to delete these %1 items?", itemPaths.size()), itemPaths, i18n("Delete Files"), KStandardGuiItem::del(), KStandardGuiItem::cancel() ) == KMessageBox::No) { return; } //Go though projectmanagers, have them remove the files and folders that they own QHash< IProjectFileManager*, QList >::iterator it; for (it = filteredItems.begin(); it != filteredItems.end(); ++it) { Q_ASSERT(it.key()); it.key()->removeFilesAndFolders(it.value()); } } void ProjectManagerViewPlugin::removeTargetFilesFromContextMenu() { const QList items = itemsFromIndexes( d->ctxProjectItemList ); QHash< IBuildSystemManager*, QList > itemsByBuildSystem; for (ProjectBaseItem* item : items) { itemsByBuildSystem[item->project()->buildSystemManager()].append(item->file()); } QHash< IBuildSystemManager*, QList >::iterator it; for (it = itemsByBuildSystem.begin(); it != itemsByBuildSystem.end(); ++it) it.key()->removeFilesFromTargets(it.value()); } void ProjectManagerViewPlugin::renameItemFromContextMenu() { renameItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::renameItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); for (KDevelop::ProjectBaseItem* item : items) { if ((item->type()!=ProjectBaseItem::BuildFolder && item->type()!=ProjectBaseItem::Folder && item->type()!=ProjectBaseItem::File) || !item->parent()) { continue; } const QString src = item->text(); //Change QInputDialog->KFileSaveDialog? QString name = QInputDialog::getText( window, i18n("Rename..."), i18n("New name for '%1':", item->text()), QLineEdit::Normal, item->text() ); if (!name.isEmpty() && name != src) { ProjectBaseItem::RenameStatus status = item->rename( name ); switch(status) { case ProjectBaseItem::RenameOk: break; case ProjectBaseItem::ExistingItemSameName: KMessageBox::error(window, i18n("There is already a file named '%1'", name)); break; case ProjectBaseItem::ProjectManagerRenameFailed: KMessageBox::error(window, i18n("Could not rename '%1'", name)); break; case ProjectBaseItem::InvalidNewName: KMessageBox::error(window, i18n("'%1' is not a valid file name", name)); break; } } } } ProjectFileItem* createFile(const ProjectFolderItem* item) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); QString name = QInputDialog::getText(window, i18n("Create File in %1", item->path().pathOrUrl()), i18n("File name:")); if(name.isEmpty()) return nullptr; ProjectFileItem* ret = item->project()->projectFileManager()->addFile( Path(item->path(), name), item->folder() ); if (ret) { ICore::self()->documentController()->openDocument( ret->path().toUrl() ); } return ret; } void ProjectManagerViewPlugin::createFileFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList ) ) { if ( item->folder() ) { createFile(item->folder()); } else if ( item->target() ) { ProjectFolderItem* folder=dynamic_cast(item->parent()); if(folder) { ProjectFileItem* f=createFile(folder); if(f) item->project()->buildSystemManager()->addFilesToTarget(QList() << f, item->target()); } } } } void ProjectManagerViewPlugin::copyFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(false)); } void ProjectManagerViewPlugin::cutFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(true)); } static void selectItemsByPaths(ProjectManagerView* view, const Path::List& paths) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); QList newItems; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); newItems.append(items); - for (ProjectBaseItem* item : items) { + for (ProjectBaseItem* item : qAsConst(items)) { view->expandItem(item->parent()); } } view->selectItems(newItems); } void ProjectManagerViewPlugin::pasteFromContextMenu() { KDevelop::ProjectItemContext* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); if (ctx->items().count() != 1) { return; //do nothing if multiple or none items are selected } ProjectBaseItem* destItem = ctx->items().at(0); if (!destItem->folder()) { return; //do nothing if the target is not a directory } const QMimeData* data = qApp->clipboard()->mimeData(); qCDebug(PLUGIN_PROJECTMANAGERVIEW) << data->urls(); Path::List origPaths = toPathList(data->urls()); const bool isCut = KIO::isClipboardDataCut(data); const CutCopyPasteHelpers::SourceToDestinationMap map = CutCopyPasteHelpers::mapSourceToDestination(origPaths, destItem->folder()->path()); QVector tasks = CutCopyPasteHelpers::copyMoveItems( map.filteredPaths, destItem, isCut ? CutCopyPasteHelpers::Operation::CUT : CutCopyPasteHelpers::Operation::COPY); // Select new items in the project manager view ProjectManagerViewItemContext* itemCtx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemCtx) { Path::List finalPathsList; for (const auto& task : tasks) { if (task.m_status == CutCopyPasteHelpers::TaskStatus::SUCCESS && task.m_type != CutCopyPasteHelpers::TaskType::DELETION) { finalPathsList.reserve(finalPathsList.size() + task.m_src.size()); for (const Path& src : task.m_src) { finalPathsList.append(map.finalPaths[src]); } } } selectItemsByPaths(itemCtx->view(), finalPathsList); } // If there was a single failure, display a warning dialog. const bool anyFailed = std::any_of(tasks.begin(), tasks.end(), [](const CutCopyPasteHelpers::TaskInfo& task) { return task.m_status != CutCopyPasteHelpers::TaskStatus::SUCCESS; }); if (anyFailed) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); showWarningDialogForFailedPaste(window, tasks); } } #include "projectmanagerviewplugin.moc" diff --git a/plugins/projectmanagerview/projecttreeview.cpp b/plugins/projectmanagerview/projecttreeview.cpp index f6bd6467b0..c4f2323040 100644 --- a/plugins/projectmanagerview/projecttreeview.cpp +++ b/plugins/projectmanagerview/projecttreeview.cpp @@ -1,477 +1,479 @@ /* This file is part of KDevelop Copyright 2005 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2009 Aleix Pol 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 "projecttreeview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectmanagerviewplugin.h" #include "projectmodelsaver.h" #include "projectmodelitemdelegate.h" #include "debug.h" #include #include using namespace KDevelop; namespace { const char settingsConfigGroup[] = "ProjectTreeView"; QList fileItemsWithin(const QList& items) { QList fileItems; fileItems.reserve(items.size()); for (ProjectBaseItem* item : items) { if (ProjectFileItem *file = item->file()) fileItems.append(file); else if (item->folder()) fileItems.append(fileItemsWithin(item->children())); } return fileItems; } QList topLevelItemsWithin(QList items) { std::sort(items.begin(), items.end(), ProjectBaseItem::pathLessThan); Path lastFolder; for (int i = items.size() - 1; i >= 0; --i) { if (lastFolder.isParentOf(items[i]->path())) items.removeAt(i); else if (items[i]->folder()) lastFolder = items[i]->path(); } return items; } template void filterDroppedItems(QList &items, ProjectBaseItem* dest) { for (int i = items.size() - 1; i >= 0; --i) { //No drag and drop from and to same location if (items[i]->parent() == dest) items.removeAt(i); //No moving between projects (technically feasible if the projectmanager is the same though...) else if (items[i]->project() != dest->project()) items.removeAt(i); } } //TODO test whether this could be replaced by projectbuildsetwidget.cpp::showContextMenu_appendActions void popupContextMenu_appendActions(QMenu& menu, const QList& actions) { menu.addActions(actions); menu.addSeparator(); } } ProjectTreeView::ProjectTreeView( QWidget *parent ) : QTreeView( parent ), m_previousSelection ( nullptr ) { header()->hide(); setEditTriggers( QAbstractItemView::EditKeyPressed ); setContextMenuPolicy( Qt::CustomContextMenu ); setSelectionMode( QAbstractItemView::ExtendedSelection ); setIndentation(10); setDragEnabled(true); setDragDropMode(QAbstractItemView::InternalMove); setAutoScroll(true); setAutoExpandDelay(300); setItemDelegate(new ProjectModelItemDelegate(this)); connect( this, &ProjectTreeView::customContextMenuRequested, this, &ProjectTreeView::popupContextMenu ); connect( this, &ProjectTreeView::activated, this, &ProjectTreeView::slotActivated ); connect( ICore::self(), &ICore::aboutToShutdown, this, &ProjectTreeView::aboutToShutdown); connect( ICore::self()->projectController(), &IProjectController::projectOpened, this, &ProjectTreeView::restoreState ); connect( ICore::self()->projectController(), &IProjectController::projectClosed, this, &ProjectTreeView::projectClosed ); } ProjectTreeView::~ProjectTreeView() { } ProjectBaseItem* ProjectTreeView::itemAtPos(const QPoint& pos) const { return indexAt(pos).data(ProjectModel::ProjectItemRole).value(); } void ProjectTreeView::dropEvent(QDropEvent* event) { ProjectItemContext* selectionCtxt = static_cast(KDevelop::ICore::self()->selectionController()->currentSelection()); ProjectBaseItem* destItem = itemAtPos(event->pos()); if (destItem && (dropIndicatorPosition() == AboveItem || dropIndicatorPosition() == BelowItem)) destItem = destItem->parent(); if (selectionCtxt && destItem) { if (ProjectFolderItem *folder = destItem->folder()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ShiftModifier ).toString(); seq.chop(1); // chop superfluous '+' QAction* move = new QAction(i18n( "&Move Here" ) + '\t' + seq, &dropMenu); move->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); dropMenu.addAction(move); seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* copy = new QAction(i18n( "&Copy Here" ) + '\t' + seq, &dropMenu); copy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); dropMenu.addAction(copy); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n( "C&ancel" ) + '\t' + QKeySequence( Qt::Key_Escape ).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = copy; } else if (modifiers == Qt::ShiftModifier) { executedAction = move; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } QList usefulItems = topLevelItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); Path::List paths; paths.reserve(usefulItems.size()); foreach (ProjectBaseItem* i, usefulItems) { paths << i->path(); } bool success = false; if (executedAction == copy) { success = destItem->project()->projectFileManager()->copyFilesAndFolders(paths, folder); } else if (executedAction == move) { success = destItem->project()->projectFileManager()->moveFilesAndFolders(usefulItems, folder); } if (success) { //expand target folder expand( mapFromItem(folder)); //and select new items QItemSelection selection; foreach (const Path &path, paths) { const Path targetPath(folder->path(), path.lastPathSegment()); foreach (ProjectBaseItem *item, folder->children()) { if (item->path() == targetPath) { QModelIndex indx = mapFromItem( item ); selection.append(QItemSelectionRange(indx, indx)); setCurrentIndex(indx); } } } selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } } else if (destItem->target() && destItem->project()->buildSystemManager()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* addToTarget = new QAction(i18n( "&Add to Target" ) + '\t' + seq, &dropMenu); addToTarget->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); dropMenu.addAction(addToTarget); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n( "C&ancel" ) + '\t' + QKeySequence( Qt::Key_Escape ).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = addToTarget; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } if (executedAction == addToTarget) { QList usefulItems = fileItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); destItem->project()->buildSystemManager()->addFilesToTarget(usefulItems, destItem->target()); } } } event->accept(); } QModelIndex ProjectTreeView::mapFromSource(const QAbstractProxyModel* proxy, const QModelIndex& sourceIdx) { const QAbstractItemModel* next = proxy->sourceModel(); Q_ASSERT(next == sourceIdx.model() || qobject_cast(next)); if(next == sourceIdx.model()) return proxy->mapFromSource(sourceIdx); else { const QAbstractProxyModel* nextProxy = qobject_cast(next); QModelIndex idx = mapFromSource(nextProxy, sourceIdx); Q_ASSERT(idx.model() == nextProxy); return proxy->mapFromSource(idx); } } QModelIndex ProjectTreeView::mapFromItem(const ProjectBaseItem* item) { QModelIndex ret = mapFromSource(qobject_cast(model()), item->index()); Q_ASSERT(ret.model() == model()); return ret; } void ProjectTreeView::slotActivated( const QModelIndex &index ) { if ( QApplication::keyboardModifiers() & Qt::CTRL || QApplication::keyboardModifiers() & Qt::SHIFT ) { // Do not open file when Ctrl or Shift is pressed; that's for selection return; } KDevelop::ProjectBaseItem *item = index.data(ProjectModel::ProjectItemRole).value(); if ( item && item->file() ) { emit activate( item->file()->path() ); } } void ProjectTreeView::projectClosed(KDevelop::IProject* project) { if ( project == m_previousSelection ) m_previousSelection = nullptr; } QList ProjectTreeView::selectedProjects() { QList itemlist; if ( selectionModel()->hasSelection() ) { QModelIndexList indexes = selectionModel()->selectedRows(); for ( const QModelIndex& index: indexes ) { ProjectBaseItem* item = index.data( ProjectModel::ProjectItemRole ).value(); if ( item ) { itemlist << item; m_previousSelection = item->project(); } } } // add previous selection if nothing is selected right now if ( itemlist.isEmpty() && m_previousSelection ) { itemlist << m_previousSelection->projectItem(); } return itemlist; } KDevelop::IProject* ProjectTreeView::getCurrentProject() { auto itemList = selectedProjects(); if ( !itemList.isEmpty() ) { return itemList.at( 0 )->project(); } return nullptr; } void ProjectTreeView::popupContextMenu( const QPoint &pos ) { QList itemlist; if ( indexAt( pos ).isValid() ) { itemlist = selectedProjects(); } QMenu menu( this ); KDevelop::ProjectItemContextImpl context(itemlist); const QList extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, &menu); QList buildActions; QList vcsActions; QList analyzeActions; QList extActions; QList projectActions; QList fileActions; QList runActions; for (const ContextMenuExtension& ext : extensions) { buildActions += ext.actions(ContextMenuExtension::BuildGroup); fileActions += ext.actions(ContextMenuExtension::FileGroup); projectActions += ext.actions(ContextMenuExtension::ProjectGroup); vcsActions += ext.actions(ContextMenuExtension::VcsGroup); analyzeActions += ext.actions(ContextMenuExtension::AnalyzeProjectGroup); extActions += ext.actions(ContextMenuExtension::ExtensionGroup); runActions += ext.actions(ContextMenuExtension::RunGroup); } if ( analyzeActions.count() ) { QMenu* analyzeMenu = new QMenu(i18n("Analyze With"), &menu); analyzeMenu->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); foreach( QAction* act, analyzeActions ) { analyzeMenu->addAction( act ); } analyzeActions = {analyzeMenu->menuAction()}; } popupContextMenu_appendActions(menu, buildActions); popupContextMenu_appendActions(menu, runActions ); popupContextMenu_appendActions(menu, fileActions); popupContextMenu_appendActions(menu, vcsActions); popupContextMenu_appendActions(menu, analyzeActions); popupContextMenu_appendActions(menu, extActions); if (itemlist.size() == 1 && itemlist.first()->folder() && !itemlist.first()->folder()->parent()) { QAction* projectConfig = new QAction(i18n("Open Configuration..."), &menu); projectConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect( projectConfig, &QAction::triggered, this, &ProjectTreeView::openProjectConfig ); projectActions << projectConfig; } popupContextMenu_appendActions(menu, projectActions); if ( !menu.isEmpty() ) { menu.exec(viewport()->mapToGlobal(pos)); } } void ProjectTreeView::openProjectConfig() { if ( IProject* project = getCurrentProject() ) { IProjectController* ip = ICore::self()->projectController(); ip->configureProject( project ); } } void ProjectTreeView::saveState( IProject* project ) { // nullptr won't create a usable saved state, so spare the effort if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), QString( settingsConfigGroup ).append( project->name() ) ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.saveState( configGroup ); } void ProjectTreeView::restoreState( IProject* project ) { if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), QString( settingsConfigGroup ).append( project->name() ) ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.restoreState( configGroup ); } void ProjectTreeView::rowsInserted( const QModelIndex& parent, int start, int end ) { QTreeView::rowsInserted( parent, start, end ); if ( !parent.model() ) { - for ( const auto& project: selectedProjects() ) { + const auto& projects = selectedProjects(); + for (const auto& project: projects) { restoreState( project->project() ); } } } void ProjectTreeView::rowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ) { if ( !parent.model() ) { - for ( const auto& project: selectedProjects() ) { + const auto& projects = selectedProjects(); + for (const auto& project : projects) { saveState( project->project() ); } } QTreeView::rowsAboutToBeRemoved( parent, start, end ); } void ProjectTreeView::aboutToShutdown() { // save all projects, not just the selected ones const auto projects = ICore::self()->projectController()->projects(); for ( const auto& project: projects ) { saveState( project ); } } void ProjectTreeView::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Return && currentIndex().isValid() && state()!=QAbstractItemView::EditingState) { event->accept(); slotActivated(currentIndex()); } else QTreeView::keyPressEvent(event); } void ProjectTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { if (WidgetColorizer::colorizeByProject()) { const auto projectPath = index.data(ProjectModel::ProjectRole).value()->path(); const QColor color = WidgetColorizer::colorForId(qHash(projectPath), palette(), true); WidgetColorizer::drawBranches(this, painter, rect, index, color); } QTreeView::drawBranches(painter, rect, index); } diff --git a/plugins/qmakemanager/qmakeprojectfile.cpp b/plugins/qmakemanager/qmakeprojectfile.cpp index 09866fa3c9..5746e375e0 100644 --- a/plugins/qmakemanager/qmakeprojectfile.cpp +++ b/plugins/qmakemanager/qmakeprojectfile.cpp @@ -1,443 +1,446 @@ /* KDevelop QMake Support * * Copyright 2006 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. * * 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 "qmakeprojectfile.h" #include #include #include #include "debug.h" #include "parser/ast.h" #include "qmakecache.h" #include "qmakemkspecs.h" #include "qmakeconfig.h" #include #include #define ifDebug(x) QHash> QMakeProjectFile::m_qmakeQueryCache; const QStringList QMakeProjectFile::FileVariables = QStringList{ QStringLiteral("IDLS"), QStringLiteral("RESOURCES"), QStringLiteral("IMAGES"), QStringLiteral("LEXSOURCES"), QStringLiteral("DISTFILES"), QStringLiteral("YACCSOURCES"), QStringLiteral("TRANSLATIONS"), QStringLiteral("HEADERS"), QStringLiteral("SOURCES"), QStringLiteral("INTERFACES"), QStringLiteral("FORMS"), }; QMakeProjectFile::QMakeProjectFile(const QString& projectfile) : QMakeFile(projectfile) , m_mkspecs(nullptr) , m_cache(nullptr) { } void QMakeProjectFile::setQMakeCache(QMakeCache* cache) { m_cache = cache; } void QMakeProjectFile::setMkSpecs(QMakeMkSpecs* mkspecs) { m_mkspecs = mkspecs; } bool QMakeProjectFile::read() { // default values // NOTE: if we already have such a var, e.g. in an include file, we must not overwrite it here! if (!m_variableValues.contains(QStringLiteral("QT"))) { m_variableValues[QStringLiteral("QT")] = QStringList{QStringLiteral("core"), QStringLiteral("gui")}; } if (!m_variableValues.contains(QStringLiteral("CONFIG"))) { m_variableValues[QStringLiteral("CONFIG")] = QStringList() << QStringLiteral("qt"); } Q_ASSERT(m_mkspecs); foreach (const QString& var, m_mkspecs->variables()) { if (!m_variableValues.contains(var)) { m_variableValues[var] = m_mkspecs->variableValues(var); } } if (m_cache) { foreach (const QString& var, m_cache->variables()) { if (!m_variableValues.contains(var)) { m_variableValues[var] = m_cache->variableValues(var); } } } /// TODO: more special variables m_variableValues[QStringLiteral("PWD")] = QStringList() << pwd(); m_variableValues[QStringLiteral("_PRO_FILE_")] = QStringList() << proFile(); m_variableValues[QStringLiteral("_PRO_FILE_PWD_")] = QStringList() << proFilePwd(); m_variableValues[QStringLiteral("OUT_PWD")] = QStringList() << outPwd(); const QString qtInstallHeaders = QStringLiteral("QT_INSTALL_HEADERS"); const QString qtVersion = QStringLiteral("QT_VERSION"); const QString qtInstallLibs = QStringLiteral("QT_INSTALL_LIBS"); const QString executable = QMakeConfig::qmakeExecutable(project()); if (!m_qmakeQueryCache.contains(executable)) { const auto queryResult = QMakeConfig::queryQMake(executable, {qtInstallHeaders, qtVersion, qtInstallLibs}); if (queryResult.isEmpty()) { qCWarning(KDEV_QMAKE) << "Failed to query qmake - bad qmake executable configured?" << executable; } m_qmakeQueryCache[executable] = queryResult; } const auto cachedQueryResult = m_qmakeQueryCache.value(executable); m_qtIncludeDir = cachedQueryResult.value(qtInstallHeaders); m_qtVersion = cachedQueryResult.value(qtVersion); m_qtLibDir = cachedQueryResult.value(qtInstallLibs); return QMakeFile::read(); } QStringList QMakeProjectFile::subProjects() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching subprojects";) QStringList list; - for (QString subdir : variableValues(QStringLiteral("SUBDIRS"))) { + const auto& subdirs = variableValues(QStringLiteral("SUBDIRS")); + for (QString subdir : subdirs) { QString fileOrPath; ifDebug(qCDebug(KDEV_QMAKE) << "Found value:" << subdir;) if (containsVariable(subdir + ".file") && !variableValues(subdir + ".file").isEmpty()) { subdir = variableValues(subdir + ".file").first(); } else if (containsVariable(subdir + ".subdir") && !variableValues(subdir + ".subdir").isEmpty()) { subdir = variableValues(subdir + ".subdir").first(); } if (subdir.endsWith(QLatin1String(".pro"))) { fileOrPath = resolveToSingleFileName(subdir.trimmed()); } else { fileOrPath = resolveToSingleFileName(subdir.trimmed()); } if (fileOrPath.isEmpty()) { qCWarning(KDEV_QMAKE) << "could not resolve subdir" << subdir << "to file or path, skipping"; continue; } list << fileOrPath; } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "subprojects";) return list; } bool QMakeProjectFile::hasSubProject(const QString& file) const { foreach (const QString& sub, subProjects()) { if (sub == file) { return true; } else if (QFileInfo(file).absoluteDir() == sub) { return true; } } return false; } void QMakeProjectFile::addPathsForVariable(const QString& variable, QStringList* list, const QString& base) const { const QStringList values = variableValues(variable); ifDebug(qCDebug(KDEV_QMAKE) << variable << values;) for (const QString& val : values) { QString path = resolveToSingleFileName(val, base); if (!path.isEmpty() && !list->contains(val)) { list->append(path); } } } QStringList QMakeProjectFile::includeDirectories() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching include dirs" << m_qtIncludeDir;) ifDebug(qCDebug(KDEV_QMAKE) << "CONFIG" << variableValues("CONFIG");) QStringList list; addPathsForVariable(QStringLiteral("INCLUDEPATH"), &list); addPathsForVariable(QStringLiteral("QMAKE_INCDIR"), &list); if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("opengl"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_OPENGL"), &list); } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("qt"))) { if (!list.contains(m_qtIncludeDir)) list << m_qtIncludeDir; QDir incDir(m_qtIncludeDir); auto modules = variableValues(QStringLiteral("QT")); if (!modules.isEmpty() && !modules.contains(QStringLiteral("core"))) { // TODO: proper dependency tracking of modules // for now, at least include core if we include any other module modules << QStringLiteral("core"); } // TODO: This is all very fragile, should rather read QMake module .pri files (e.g. qt_lib_core_private.pri) foreach (const QString& module, modules) { QString pattern = module; bool isPrivate = false; if (module.endsWith(QLatin1String("-private"))) { pattern.chop(qstrlen("-private")); isPrivate = true; } else if (module.endsWith(QLatin1String("_private"))) { // _private is less common, but still a valid suffix pattern.chop(qstrlen("_private")); isPrivate = true; } if (pattern == QLatin1String("qtestlib") || pattern == QLatin1String("testlib")) { pattern = QStringLiteral("QtTest"); } else if (pattern == QLatin1String("qaxcontainer")) { pattern = QStringLiteral("ActiveQt"); } else if (pattern == QLatin1String("qaxserver")) { pattern = QStringLiteral("ActiveQt"); } QFileInfoList match = incDir.entryInfoList({QString("Qt%1").arg(pattern)}, QDir::Dirs); if (match.isEmpty()) { // try non-prefixed pattern match = incDir.entryInfoList({pattern}, QDir::Dirs); if (match.isEmpty()) { qCWarning(KDEV_QMAKE) << "unhandled Qt module:" << module << pattern; continue; } } QString path = match.first().canonicalFilePath(); if (isPrivate) { path += '/' + m_qtVersion + '/' + match.first().fileName() + "/private/"; } if (!list.contains(path)) { list << path; } } } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("thread"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_THREAD"), &list); } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("x11"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_X11"), &list); } addPathsForVariable(QStringLiteral("MOC_DIR"), &list, outPwd()); addPathsForVariable(QStringLiteral("OBJECTS_DIR"), &list, outPwd()); addPathsForVariable(QStringLiteral("UI_DIR"), &list, outPwd()); ifDebug(qCDebug(KDEV_QMAKE) << "final list:" << list;) return list; } // Scan QMAKE_C*FLAGS for -F and -iframework and QMAKE_LFLAGS for good measure. Time will // tell if we need to scan the release/debug/... specific versions of QMAKE_C*FLAGS. // Also include QT_INSTALL_LIBS which corresponds to Qt's framework directory on OS X. QStringList QMakeProjectFile::frameworkDirectories() const { const auto variablesToCheck = {QStringLiteral("QMAKE_CFLAGS"), QStringLiteral("QMAKE_CXXFLAGS"), QStringLiteral("QMAKE_LFLAGS")}; const QLatin1String fOption("-F"); const QLatin1String iframeworkOption("-iframework"); QStringList fwDirs; for (const auto& var : variablesToCheck) { bool storeArg = false; foreach (const auto& arg, variableValues(var)) { if (arg == fOption || arg == iframeworkOption) { // detached -F/-iframework arg; set a warrant to store the next argument storeArg = true; } else { if (arg.startsWith(fOption)) { fwDirs << arg.mid(fOption.size()); } else if (arg.startsWith(iframeworkOption)) { fwDirs << arg.mid(iframeworkOption.size()); } else if (storeArg) { fwDirs << arg; } // cancel any outstanding warrants to store the next argument storeArg = false; } } } #ifdef Q_OS_OSX fwDirs << m_qtLibDir; #endif return fwDirs; } QStringList QMakeProjectFile::extraArguments() const { const auto variablesToCheck = {QStringLiteral("QMAKE_CXXFLAGS")}; const auto prefixes = { "-F", "-iframework", "-I", "-D" }; QStringList args; for (const auto& var : variablesToCheck) { foreach (const auto& arg, variableValues(var)) { auto argHasPrefix = [arg](const char* prefix) { return arg.startsWith(prefix); }; if ( !std::any_of(prefixes.begin(), prefixes.end(), argHasPrefix)) { args << arg; } } } return args; } QStringList QMakeProjectFile::files() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching files";) QStringList list; foreach (const QString& variable, QMakeProjectFile::FileVariables) { foreach (const QString& value, variableValues(variable)) { list += resolveFileName(value); } } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "files";) return list; } QStringList QMakeProjectFile::filesForTarget(const QString& s) const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching files";) QStringList list; if (variableValues(QStringLiteral("INSTALLS")).contains(s)) { const QStringList files = variableValues(s + ".files"); for (const QString& val : files) { list += QStringList(resolveFileName(val)); } } if (!variableValues(QStringLiteral("INSTALLS")).contains(s) || s == QLatin1String("target")) { foreach (const QString& variable, QMakeProjectFile::FileVariables) { foreach (const QString& value, variableValues(variable)) { list += QStringList(resolveFileName(value)); } } } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "files";) return list; } QString QMakeProjectFile::getTemplate() const { QString templ = QStringLiteral("app"); if (!variableValues(QStringLiteral("TEMPLATE")).isEmpty()) { templ = variableValues(QStringLiteral("TEMPLATE")).first(); } return templ; } QStringList QMakeProjectFile::targets() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching targets";) QStringList list; list += variableValues(QStringLiteral("TARGET")); if (list.isEmpty() && getTemplate() != QLatin1String("subdirs")) { list += QFileInfo(absoluteFile()).baseName(); } - for (const QString& target : variableValues(QStringLiteral("INSTALLS"))) { + const auto& targets = variableValues(QStringLiteral("INSTALLS")); + for (const QString& target : targets) { if (!target.isEmpty() && target != QLatin1String("target")) list << target; } if (list.removeAll(QString())) { // remove empty targets - which is probably a bug... qCWarning(KDEV_QMAKE) << "got empty entry in TARGET of file" << absoluteFile(); } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "targets";) return list; } QMakeProjectFile::~QMakeProjectFile() { // TODO: delete cache, specs, ...? } QStringList QMakeProjectFile::resolveVariable(const QString& variable, VariableInfo::VariableType type) const { if (type == VariableInfo::QtConfigVariable) { if (m_mkspecs->isQMakeInternalVariable(variable)) { return QStringList() << m_mkspecs->qmakeInternalVariable(variable); } else { qCWarning(KDEV_QMAKE) << "unknown QtConfig Variable:" << variable; return QStringList(); } } return QMakeFile::resolveVariable(variable, type); } QMakeMkSpecs* QMakeProjectFile::mkSpecs() const { return m_mkspecs; } QMakeCache* QMakeProjectFile::qmakeCache() const { return m_cache; } QList QMakeProjectFile::defines() const { QList d; - for (const QString& def : variableMap()[QStringLiteral("DEFINES")]) { + const auto& defs = variableMap()[QStringLiteral("DEFINES")]; + for (const QString& def : defs) { int pos = def.indexOf('='); if (pos >= 0) { // a value is attached to define d.append(DefinePair(def.left(pos), def.mid(pos + 1))); } else { // a value-less define d.append(DefinePair(def, QString())); } } return d; } QString QMakeProjectFile::pwd() const { return absoluteDir(); } QString QMakeProjectFile::outPwd() const { if (!project()) { return absoluteDir(); } else { return QMakeConfig::buildDirFromSrc(project(), KDevelop::Path(absoluteDir())).toLocalFile(); } } QString QMakeProjectFile::proFile() const { return absoluteFile(); } QString QMakeProjectFile::proFilePwd() const { return absoluteDir(); } diff --git a/plugins/qmakemanager/tests/test_qmakefile.cpp b/plugins/qmakemanager/tests/test_qmakefile.cpp index 9c4ce11a2c..3782ceac8a 100644 --- a/plugins/qmakemanager/tests/test_qmakefile.cpp +++ b/plugins/qmakemanager/tests/test_qmakefile.cpp @@ -1,678 +1,678 @@ /* KDevelop QMake Support * * Copyright 2010 Milian Wolff * * 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 "test_qmakefile.h" #include "qmakefile.h" #include "variablereferenceparser.h" #include "qmakeprojectfile.h" #include "qmakemkspecs.h" #include #include #include #include #include #include #include #include #include #include QTEST_MAIN(TestQMakeFile) typedef QHash DefineHash; Q_DECLARE_METATYPE(QMakeFile::VariableMap) Q_DECLARE_METATYPE(DefineHash) namespace QTest { template <> char* toString(const QStringList& list) { QByteArray ba; if (list.isEmpty()) { ba = "()"; } else { ba = "(\"" + list.join(QStringLiteral("\", \"")).toLocal8Bit() + "\")"; } return qstrdup(ba.data()); } template <> char* toString(const QMakeFile::VariableMap& variables) { QByteArray ba = "VariableMap("; QMakeFile::VariableMap::const_iterator it = variables.constBegin(); while (it != variables.constEnd()) { ba += "["; ba += it.key().toLocal8Bit(); ba += "] = "; ba += toString(it.value()); ++it; if (it != variables.constEnd()) { ba += ", "; } } ba += ")"; return qstrdup(ba.data()); } } QHash setDefaultMKSpec(QMakeProjectFile& file) { static const QHash qmvars = QMakeConfig::queryQMake(QMakeConfig::qmakeExecutable(nullptr)); static const QString specFile = QMakeConfig::findBasicMkSpec(qmvars); if (!QFile::exists(specFile)) { qDebug() << "mkspec file does not exist:" << specFile; return {}; } QMakeMkSpecs* mkspecs = new QMakeMkSpecs(specFile, qmvars); mkspecs->read(); file.setMkSpecs(mkspecs); return qmvars; } void TestQMakeFile::varResolution() { QFETCH(QString, fileContents); QFETCH(QMakeFile::VariableMap, variables); QTemporaryFile tmpfile; tmpfile.open(); QTextStream stream(&tmpfile); stream << fileContents; stream << flush; tmpfile.close(); QMakeFile file(tmpfile.fileName()); QVERIFY(file.read()); QCOMPARE(file.variableMap(), variables); } void TestQMakeFile::varResolution_data() { QTest::addColumn("fileContents"); QTest::addColumn("variables"); { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR1")] = QStringList() << QStringLiteral("1"); QTest::newRow("simple") << "VAR1 = 1\n" << variables; } { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR1")] = QStringList() << QStringLiteral("1"); variables[QStringLiteral("VAR2")] = QStringList() << QStringLiteral("1"); QTest::newRow("var-in-var") << "VAR1 = 1\nVAR2 = $$VAR1\n" << variables; } { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR1")] = QStringList() << QStringLiteral("foo"); variables[QStringLiteral("VAR2")] = QStringList() << QStringLiteral("foo"); QTest::newRow("curlyvar") << "VAR1 = foo\nVAR2 = $${VAR1}\n" << variables; } { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR1")] = QStringList() << QProcessEnvironment::systemEnvironment().value(QStringLiteral("USER")); QTest::newRow("qmakeshell") << "VAR1 = $$(USER)\n" << variables; } { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR1")] = QStringList() << QStringLiteral("foo"); variables[QStringLiteral("VAR2")] = QStringList() << QStringLiteral("foo/bar"); QTest::newRow("path") << "VAR1 = foo\nVAR2 = $$VAR1/bar\n" << variables; } { QMakeFile::VariableMap variables; variables[QStringLiteral("VAR_1")] = QStringList() << QStringLiteral("foo"); variables[QStringLiteral("VAR_2")] = QStringList() << QStringLiteral("foo/bar"); QTest::newRow("var-underscore") << "VAR_1 = foo\nVAR_2 = $$VAR_1/bar" << variables; } } void TestQMakeFile::referenceParser() { QFETCH(QString, var); VariableReferenceParser parser; parser.setContent(var); QVERIFY(parser.parse()); } void TestQMakeFile::referenceParser_data() { QTest::addColumn("var"); QTest::newRow("dot") << "."; QTest::newRow("dotdot") << ".."; } void TestQMakeFile::libTarget() { QFETCH(QString, target); QFETCH(QString, resolved); QTemporaryFile tmpfile; tmpfile.open(); QTextStream stream(&tmpfile); stream << "TARGET = " << target << "\nTEMPLATE = lib\n"; stream << flush; tmpfile.close(); QMakeProjectFile file(tmpfile.fileName()); const auto qmvars = setDefaultMKSpec(file); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(file.read()); QCOMPARE(file.targets(), QStringList() << resolved); } void TestQMakeFile::libTarget_data() { QTest::addColumn("target"); QTest::addColumn("resolved"); QTest::newRow("simple") << "MyLib" << "MyLib"; QTest::newRow("qtLibraryTarget") << "$$qtLibraryTarget(MyLib)" << "MyLib"; QTest::newRow("qtLibraryTarget-Var") << "MyLib\nTARGET = $$qtLibraryTarget($$TARGET)" << "MyLib"; } void TestQMakeFile::defines() { QFETCH(QString, fileContents); QFETCH(DefineHash, expectedDefines); QTemporaryFile tmpfile; tmpfile.open(); QTextStream stream(&tmpfile); stream << fileContents; stream << flush; tmpfile.close(); QMakeProjectFile file(tmpfile.fileName()); const auto qmvars = setDefaultMKSpec(file); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(file.read()); const QList list = file.defines(); QCOMPARE(list.size(), expectedDefines.size()); - for (QMakeProjectFile::DefinePair define : list) { + for (const QMakeProjectFile::DefinePair& define : list) { QVERIFY(expectedDefines.find(define.first) != expectedDefines.end()); QCOMPARE(define.second, expectedDefines[define.first]); } } void TestQMakeFile::defines_data() { QTest::addColumn("fileContents"); QTest::addColumn("expectedDefines"); { DefineHash list; list.insert(QStringLiteral("VAR1"), QString()); QTest::newRow("Simple define") << "DEFINES += VAR1" << list; } { DefineHash list; list.insert(QStringLiteral("ANSWER"), QStringLiteral("42")); QTest::newRow("Define with value") << "DEFINES += ANSWER=42" << list; } { DefineHash list; list.insert(QStringLiteral("ANSWER"), QStringLiteral("42")); list.insert(QStringLiteral("ANOTHER_DEFINE"), QString()); QTest::newRow("Multiple defines") << "DEFINES += ANSWER=42 ANOTHER_DEFINE" << list; } } void TestQMakeFile::replaceFunctions_data() { QTest::addColumn("fileContents"); QTest::addColumn("definedVariables"); QTest::addColumn("undefinedVariables"); { QString contents = "defineReplace(test) {\n" " FOO = $$1\n" " return($$FOO)\n" "}\n" "BAR = $$test(asdf)\n"; QMakeFile::VariableMap vars; vars[QStringLiteral("BAR")] = QStringList() << QStringLiteral("asdf"); QStringList undefined; undefined << QStringLiteral("FOO") << QStringLiteral("1"); QTest::newRow("defineReplace-1") << contents << vars << undefined; } } void TestQMakeFile::replaceFunctions() { QFETCH(QString, fileContents); QFETCH(QMakeFile::VariableMap, definedVariables); QFETCH(QStringList, undefinedVariables); QTemporaryFile tmpFile; tmpFile.open(); tmpFile.write(fileContents.toUtf8()); tmpFile.close(); QMakeProjectFile file(tmpFile.fileName()); setDefaultMKSpec(file); QVERIFY(file.read()); QMakeFile::VariableMap::const_iterator it = definedVariables.constBegin(); while (it != definedVariables.constEnd()) { QCOMPARE(file.variableValues(it.key()), it.value()); ++it; } foreach (const QString& var, undefinedVariables) { QVERIFY(!file.containsVariable(var)); } } void TestQMakeFile::qtIncludeDirs_data() { QTest::addColumn("fileContents"); QTest::addColumn("modules"); QTest::addColumn("missingModules"); { QStringList list; list << QStringLiteral("core") << QStringLiteral("gui"); QTest::newRow("defaults") << "" << list; } { QStringList list; list << QStringLiteral("core"); QTest::newRow("minimal") << "QT -= gui" << list; } { QStringList modules; modules << QStringLiteral("core") << QStringLiteral("gui") << QStringLiteral("network") << QStringLiteral("opengl") << QStringLiteral("phonon") << QStringLiteral("script") << QStringLiteral("scripttools") << QStringLiteral("sql") << QStringLiteral("svg") << QStringLiteral("webkit") << QStringLiteral("xml") << QStringLiteral("xmlpatterns") << QStringLiteral("qt3support") << QStringLiteral("designer") << QStringLiteral("uitools") << QStringLiteral("help") << QStringLiteral("assistant") << QStringLiteral("qtestlib") << QStringLiteral("testlib") << QStringLiteral("qaxcontainer") << QStringLiteral("qaxserver") << QStringLiteral("dbus") << QStringLiteral("declarative"); foreach (const QString& module, modules) { QStringList expected; expected << module; if (module != QLatin1String("core")) { expected << QStringLiteral("core"); } QTest::newRow(qPrintable(module)) << QStringLiteral("QT = %1").arg(module) << expected; } } } void TestQMakeFile::qtIncludeDirs() { QFETCH(QString, fileContents); QFETCH(QStringList, modules); QMap moduleMap; moduleMap[QStringLiteral("core")] = QStringLiteral("QtCore"); moduleMap[QStringLiteral("gui")] = QStringLiteral("QtGui"); moduleMap[QStringLiteral("network")] = QStringLiteral("QtNetwork"); moduleMap[QStringLiteral("opengl")] = QStringLiteral("QtOpenGL"); moduleMap[QStringLiteral("phonon")] = QStringLiteral("Phonon"); moduleMap[QStringLiteral("script")] = QStringLiteral("QtScript"); moduleMap[QStringLiteral("scripttools")] = QStringLiteral("QtScriptTools"); moduleMap[QStringLiteral("sql")] = QStringLiteral("QtSql"); moduleMap[QStringLiteral("svg")] = QStringLiteral("QtSvg"); moduleMap[QStringLiteral("webkit")] = QStringLiteral("QtWebKit"); moduleMap[QStringLiteral("xml")] = QStringLiteral("QtXml"); moduleMap[QStringLiteral("xmlpatterns")] = QStringLiteral("QtXmlPatterns"); moduleMap[QStringLiteral("qt3support")] = QStringLiteral("Qt3Support"); moduleMap[QStringLiteral("designer")] = QStringLiteral("QtDesigner"); moduleMap[QStringLiteral("uitools")] = QStringLiteral("QtUiTools"); moduleMap[QStringLiteral("help")] = QStringLiteral("QtHelp"); moduleMap[QStringLiteral("assistant")] = QStringLiteral("QtAssistant"); moduleMap[QStringLiteral("qtestlib")] = QStringLiteral("QtTest"); moduleMap[QStringLiteral("testlib")] = QStringLiteral("QtTest"); moduleMap[QStringLiteral("qaxcontainer")] = QStringLiteral("ActiveQt"); moduleMap[QStringLiteral("qaxserver")] = QStringLiteral("ActiveQt"); moduleMap[QStringLiteral("dbus")] = QStringLiteral("QtDBus"); moduleMap[QStringLiteral("declarative")] = QStringLiteral("QtDeclarative"); QTemporaryFile tmpFile; tmpFile.open(); tmpFile.write(fileContents.toUtf8()); tmpFile.close(); QMakeProjectFile file(tmpFile.fileName()); const auto qmvars = setDefaultMKSpec(file); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(file.read()); const QStringList includes = file.includeDirectories(); // should always be there QVERIFY(includes.contains(qmvars["QT_INSTALL_HEADERS"])); for (QMap::const_iterator it = moduleMap.constBegin(); it != moduleMap.constEnd(); ++it) { QFileInfo include(qmvars[QStringLiteral("QT_INSTALL_HEADERS")] + "/" + it.value()); bool shouldBeIncluded = include.exists(); if (shouldBeIncluded) { shouldBeIncluded = modules.contains(it.key()); if (!shouldBeIncluded) { foreach (const QString& module, modules) { if (module != it.key() && moduleMap.value(module) == it.value()) { shouldBeIncluded = true; break; } } } } QCOMPARE((bool)includes.contains(include.filePath()), shouldBeIncluded); } } void TestQMakeFile::testInclude() { QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); QTemporaryFile includeFile(tempDir.path() + "/qmake-include"); QVERIFY(includeFile.open()); includeFile.write("DEFINES += SOME_INCLUDE_DEF\n" "SOURCES += includedFile.cpp\n" "INCLUDEPATH += $$PWD\n" "QT += webkit\n"); includeFile.close(); QTemporaryFile baseFile; baseFile.open(); baseFile.write("TEMPLATE = app\n" "TARGET = includeTest\n" "QT += network\n" "DEFINES += SOME_DEF\n" "SOURCES += file.cpp\n" /* "CONFIG += console" "# Comment to enable Debug Messages" "DEFINES += QT_NO_DEBUG_OUTPUT" "DESTDIR = ../bin" "RESOURCES = phantomjs.qrc" "HEADERS += csconverter.h \\" " phantom.h \\" " webpage.h \\" " consts.h \\" " utils.h \\" " networkaccessmanager.h \\" " cookiejar.h \\" " filesystem.h \\" " terminal.h \\" " encoding.h \\" " config.h \\" " mimesniffer.cpp \\" " third_party/mongoose/mongoose.h \\" " webserver.h" "SOURCES += phantom.cpp \\" " webpage.cpp \\" " main.cpp \\" " csconverter.cpp \\" " utils.cpp \\" " networkaccessmanager.cpp \\" " cookiejar.cpp \\" " filesystem.cpp \\" " terminal.cpp \\" " encoding.cpp \\" " config.cpp \\" " mimesniffer.cpp \\" " third_party/mongoose/mongoose.c \\" " webserver.cpp" "" "OTHER_FILES += usage.txt \\" " bootstrap.js \\" " configurator.js \\" " modules/fs.js \\" " modules/webpage.js \\" " modules/webserver.js" "" */ "include(" + includeFile.fileName().toLocal8Bit() + ")\n"); baseFile.close(); QMakeProjectFile file(baseFile.fileName()); const auto qmvars = setDefaultMKSpec(file); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(file.read()); QCOMPARE(file.variableValues("DEFINES"), QStringList() << "SOME_DEF" << "SOME_INCLUDE_DEF"); QCOMPARE(file.variableValues("SOURCES"), QStringList() << "file.cpp" << "includedFile.cpp"); QCOMPARE(file.variableValues("QT"), QStringList() << "core" << "gui" << "network" << "webkit"); // verify that include path was properly propagated QVERIFY(file.includeDirectories().contains(tempDir.path())); } void TestQMakeFile::globbing_data() { QTest::addColumn("files"); QTest::addColumn("pattern"); QTest::addColumn("matches"); QTest::newRow("wildcard-simple") << (QStringList() << QStringLiteral("foo.cpp")) << "*.cpp" << (QStringList() << QStringLiteral("foo.cpp")); QTest::newRow("wildcard-extended") << (QStringList() << QStringLiteral("foo.cpp") << QStringLiteral("bar.h") << QStringLiteral("asdf.cpp")) << "*.cpp" << (QStringList() << QStringLiteral("foo.cpp") << QStringLiteral("asdf.cpp")); QTest::newRow("wildcard-multiple") << (QStringList() << QStringLiteral("foo.cpp") << QStringLiteral("bar.h") << QStringLiteral("asdf.cpp")) << "*.cpp *.h" << (QStringList() << QStringLiteral("foo.cpp") << QStringLiteral("bar.h") << QStringLiteral("asdf.cpp")); QTest::newRow("wildcard-subdir") << (QStringList() << QStringLiteral("foo/bar.cpp") << QStringLiteral("fooasdf/bar.cpp") << QStringLiteral("asdf/asdf.cpp")) << "foo*/*.cpp" << (QStringList() << QStringLiteral("foo/bar.cpp") << QStringLiteral("fooasdf/bar.cpp")); QTest::newRow("bracket") << (QStringList() << QStringLiteral("foo1.cpp") << QStringLiteral("foo2.cpp") << QStringLiteral("fooX.cpp")) << "foo[0-9].cpp" << (QStringList() << QStringLiteral("foo1.cpp") << QStringLiteral("foo2.cpp")); QTest::newRow("questionmark") << (QStringList() << QStringLiteral("foo1.cpp") << QStringLiteral("fooX.cpp") << QStringLiteral("foo.cpp") << QStringLiteral("fooXY.cpp")) << "foo?.cpp" << (QStringList() << QStringLiteral("foo1.cpp") << QStringLiteral("fooX.cpp")); QTest::newRow("mixed") << (QStringList() << QStringLiteral("foo/asdf/test.cpp") << QStringLiteral("fooX/asdf1/test.cpp")) << "foo?/asdf[0-9]/*.cpp" << (QStringList() << QStringLiteral("fooX/asdf1/test.cpp")); } void TestQMakeFile::globbing() { QFETCH(QStringList, files); QFETCH(QString, pattern); QFETCH(QStringList, matches); QTemporaryDir tempDir; QDir tempDirDir(tempDir.path()); QVERIFY(tempDir.isValid()); foreach (const QString& file, files) { QVERIFY(tempDirDir.mkpath(QFileInfo(file).path())); QFile f(tempDir.path() + '/' + file); QVERIFY(f.open(QIODevice::WriteOnly)); } QTemporaryFile testFile(tempDir.path() + "/XXXXXX.pro"); QVERIFY(testFile.open()); testFile.write(("SOURCES = " + pattern + "\n").toUtf8()); testFile.close(); QMakeProjectFile pro(testFile.fileName()); const auto qmvars = setDefaultMKSpec(pro); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(pro.read()); QStringList actual; foreach (QString path, pro.files()) { actual << path.remove(tempDir.path() + '/'); } std::sort(actual.begin(), actual.end()); std::sort(matches.begin(), matches.end()); QCOMPARE(actual, matches); } void TestQMakeFile::benchGlobbing() { QTemporaryDir tempDir; QDir dir(tempDir.path()); const int folders = 10; const int files = 100; for (int i = 0; i < folders; ++i) { QString folder = QStringLiteral("folder%1").arg(i); dir.mkdir(folder); for (int j = 0; j < files; ++j) { QFile f1(dir.filePath(folder + QStringLiteral("/file%1.cpp").arg(j))); QVERIFY(f1.open(QIODevice::WriteOnly)); QFile f2(dir.filePath(folder + QStringLiteral("/file%1.h").arg(j))); QVERIFY(f2.open(QIODevice::WriteOnly)); } } QTemporaryFile testFile(tempDir.path() + "/XXXXXX.pro"); QVERIFY(testFile.open()); testFile.write("SOURCES = fo?der[0-9]/*.cpp\n"); testFile.close(); QMakeProjectFile pro(testFile.fileName()); const auto qmvars = setDefaultMKSpec(pro); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(pro.read()); int found = 0; QBENCHMARK { found = pro.files().size(); } QCOMPARE(found, files * folders); } void TestQMakeFile::benchGlobbingNoPattern() { QTemporaryDir tempDir; QDir dir(tempDir.path()); const int folders = 10; const int files = 100; for (int i = 0; i < folders; ++i) { QString folder = QStringLiteral("folder%1").arg(i); dir.mkdir(folder); for (int j = 0; j < files; ++j) { QFile f1(dir.filePath(folder + QStringLiteral("/file%1.cpp").arg(j))); QVERIFY(f1.open(QIODevice::WriteOnly)); QFile f2(dir.filePath(folder + QStringLiteral("/file%1.h").arg(j))); QVERIFY(f2.open(QIODevice::WriteOnly)); } } QTemporaryFile testFile(tempDir.path() + "/XXXXXX.pro"); QVERIFY(testFile.open()); testFile.write("SOURCES = folder0/file1.cpp\n"); testFile.close(); QMakeProjectFile pro(testFile.fileName()); const auto qmvars = setDefaultMKSpec(pro); if (qmvars.isEmpty()) { QSKIP("Problem querying QMake, skipping test function"); } QVERIFY(pro.read()); int found = 0; QBENCHMARK { found = pro.files().size(); } QCOMPARE(found, 1); } diff --git a/plugins/qmljs/codecompletion/context.cpp b/plugins/qmljs/codecompletion/context.cpp index 0615f24061..817531c159 100644 --- a/plugins/qmljs/codecompletion/context.cpp +++ b/plugins/qmljs/codecompletion/context.cpp @@ -1,504 +1,506 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2013 Sven Brauch * * 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 "context.h" #include "items/modulecompletionitem.h" #include "items/functioncalltipcompletionitem.h" #include #include #include #include #include #include #include #include #include #include "../duchain/expressionvisitor.h" #include "../duchain/helper.h" #include "../duchain/cache.h" #include "../duchain/frameworks/nodejs.h" #include #include using namespace KDevelop; typedef QPair DeclarationDepthPair; namespace QmlJS { CodeCompletionContext::CodeCompletionContext(const DUContextPointer& context, const QString& text, const CursorInRevision& position, int depth) : KDevelop::CodeCompletionContext(context, extractLastLine(text), position, depth), m_completionKind(NormalCompletion) { // Detect "import ..." and provide import completions if (m_text.startsWith(QLatin1String("import "))) { m_completionKind = ImportCompletion; } // Node.js module completions if (m_text.endsWith(QLatin1String("require("))) { m_completionKind = NodeModulesCompletion; } // Detect whether the cursor is in a comment bool isLastLine = true; bool inString = false; for (int index = text.size()-1; index > 0; --index) { const QChar c = text.at(index); const QChar prev = text.at(index - 1); if (c == QLatin1Char('\n')) { isLastLine = false; } else if (isLastLine && prev == QLatin1Char('/') && c == QLatin1Char('/')) { // Single-line comment on the current line, we are in a comment m_completionKind = CommentCompletion; break; } else if (prev == QLatin1Char('/') && c == QLatin1Char('*')) { // Start of a multi-line comment encountered m_completionKind = CommentCompletion; break; } else if (prev == QLatin1Char('*') && c == QLatin1Char('/')) { // End of a multi-line comment. Because /* and */ cannot be nested, // encountering a */ is enough to know that the cursor is outside a // comment break; } else if (prev != QLatin1Char('\\') && (c == QLatin1Char('"') || c == QLatin1Char('\''))) { // Toggle whether we are in a string or not inString = !inString; } } if (inString) { m_completionKind = StringCompletion; } // Some specific constructs don't need any code-completion at all (mainly // because the user will declare new things, not use ones) if (m_text.contains(QRegExp(QLatin1String("(var|function)\\s+$"))) || // "var |" or "function |" m_text.contains(QRegExp(QLatin1String("property\\s+[a-zA-Z0-9_]+\\s+$"))) || // "property |" m_text.contains(QRegExp(QLatin1String("function(\\s+[a-zA-Z0-9_]+)?\\s*\\($"))) || // "function (|" or "function (|" m_text.contains(QRegExp(QLatin1String("id:\\s*"))) // "id: |" ) { m_completionKind = NoCompletion; } } QList CodeCompletionContext::completionItems(bool& abort, bool fullCompletion) { Q_UNUSED (fullCompletion); if (abort) { return QList(); } switch (m_completionKind) { case NormalCompletion: return normalCompletion(); case CommentCompletion: return commentCompletion(); case ImportCompletion: return importCompletion(); case NodeModulesCompletion: return nodeModuleCompletions(); case StringCompletion: case NoCompletion: break; } return QList(); } AbstractType::Ptr CodeCompletionContext::typeToMatch() const { return m_typeToMatch; } QList CodeCompletionContext::normalCompletion() { QList items; QChar lastChar = m_text.size() > 0 ? m_text.at(m_text.size() - 1) : QLatin1Char('\0'); bool inQmlObjectScope = (m_duContext->type() == DUContext::Class); // Start with the function call-tips, because functionCallTips is also responsible // for setting m_declarationForTypeMatch items << functionCallTips(); if (lastChar == QLatin1Char('.') || lastChar == QLatin1Char('[')) { // Offer completions for object members and array subscripts items << fieldCompletions( m_text.left(m_text.size() - 1), lastChar == QLatin1Char('[') ? CompletionItem::QuotesAndBracket : CompletionItem::NoDecoration ); } // "object." must only display the members of object, the declarations // available in the current context. if (lastChar != QLatin1Char('.')) { if (inQmlObjectScope) { DUChainReadLocker lock; // The cursor is in a QML object and there is nothing before it. Display // a list of properties and signals that can be used in a script binding. // Note that the properties/signals of parent QML objects are not displayed here items << completionsInContext(m_duContext, CompletionOnlyLocal | CompletionHideWrappers, CompletionItem::ColonOrBracket); items << completionsFromImports(CompletionHideWrappers); items << completionsInContext(DUContextPointer(m_duContext->topContext()), CompletionHideWrappers, CompletionItem::NoDecoration); } else { items << completionsInContext(m_duContext, CompletionInContextFlags(), CompletionItem::NoDecoration); items << completionsFromImports(CompletionInContextFlags()); items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_ecmascript")); if (!QmlJS::isQmlFile(m_duContext.data())) { items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_dom")); } } } return items; } QList CodeCompletionContext::commentCompletion() { return QList(); } QList CodeCompletionContext::importCompletion() { QList items; QString fragment = m_text.section(QLatin1Char(' '), -1, -1); // Use the cache to find the directory corresponding to the fragment // (org.kde is, for instance, /usr/lib64/kde4/imports/org/kde), and list // its subdirectories QString dataDir = Cache::instance().modulePath(m_duContext->url(), fragment); QDir dir; if (!dataDir.isEmpty()) { dir.setPath(dataDir); const auto dirEntries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); items.reserve(dirEntries.size()); for (const QString& entry : dirEntries) { items.append(CompletionTreeItemPointer(new ModuleCompletionItem( fragment + entry.section(QLatin1Char('.'), 0, 0), ModuleCompletionItem::Import ))); } } return items; } QList CodeCompletionContext::nodeModuleCompletions() { QList items; QDir dir; - for (auto path : NodeJS::instance().moduleDirectories(m_duContext->url().str())) { + const auto& paths = NodeJS::instance().moduleDirectories(m_duContext->url().str()); + for (auto& path : paths) { dir.setPath(path.toLocalFile()); - for (QString entry : dir.entryList(QDir::Files, QDir::Name)) { + const auto& entries = dir.entryList(QDir::Files, QDir::Name); + for (QString entry : entries) { entry.remove(QLatin1String(".js")); if (entry.startsWith(QLatin1String("__"))) { // Internal module, don't show continue; } items.append(CompletionTreeItemPointer( new ModuleCompletionItem(entry, ModuleCompletionItem::Quotes) )); } } return items; } QList CodeCompletionContext::functionCallTips() { Stack stack = expressionStack(m_text); QList items; int argumentHintDepth = 1; bool isTopOfStack = true; DUChainReadLocker lock; while (!stack.isEmpty()) { ExpressionStackEntry entry = stack.pop(); if (isTopOfStack && entry.operatorStart > entry.startPosition) { // Deduce the declaration for type matching using operatorStart: // // table[document.base + // [ ^ // // ^ = operatorStart. Just before operatorStart is a code snippet that ends // with the declaration whose type should be used. DeclarationPointer decl = declarationAtEndOfString(m_text.left(entry.operatorStart)); if (decl) { m_typeToMatch = decl->abstractType(); } } if (entry.startPosition > 0 && m_text.at(entry.startPosition - 1) == QLatin1Char('(')) { // The current entry represents a function call, create a call-tip for it DeclarationPointer functionDecl = declarationAtEndOfString(m_text.left(entry.startPosition - 1)); if (functionDecl) { auto item = new FunctionCalltipCompletionItem( functionDecl, argumentHintDepth, entry.commas ); items << CompletionTreeItemPointer(item); argumentHintDepth++; if (isTopOfStack && !m_typeToMatch) { m_typeToMatch = item->currentArgumentType(); } } } isTopOfStack = false; } return items; } QList CodeCompletionContext::completionsFromImports(CompletionInContextFlags flags) { QList items; // Iterate over all the imported namespaces and add their definitions DUChainReadLocker lock; const QList imports = m_duContext->findDeclarations(globalImportIdentifier()); QList realImports; for (Declaration* import : imports) { if (import->kind() != Declaration::NamespaceAlias) { continue; } NamespaceAliasDeclaration* decl = static_cast(import); realImports << m_duContext->findDeclarations(decl->importIdentifier()); } items.reserve(realImports.size()); foreach (Declaration* import, realImports) { items << completionsInContext( DUContextPointer(import->internalContext()), flags, CompletionItem::NoDecoration ); } return items; } QList CodeCompletionContext::completionsFromNodeModule(CompletionInContextFlags flags, const QString& module) { return completionsInContext( DUContextPointer(QmlJS::getInternalContext( QmlJS::NodeJS::instance().moduleExports(module, m_duContext->url()) )), flags | CompletionOnlyLocal, CompletionItem::NoDecoration ); } QList CodeCompletionContext::completionsInContext(const DUContextPointer& context, CompletionInContextFlags flags, CompletionItem::Decoration decoration) { QList items; DUChainReadLocker lock; if (context) { const auto declarations = context->allDeclarations( CursorInRevision::invalid(), context->topContext(), !flags.testFlag(CompletionOnlyLocal) ); for (const DeclarationDepthPair& decl : declarations) { DeclarationPointer declaration(decl.first); CompletionItem::Decoration decorationOfThisItem = decoration; if (declaration->identifier() == globalImportIdentifier()) { continue; } if (declaration->qualifiedIdentifier().isEmpty()) { continue; } else if (context->owner() && ( context->owner()->kind() == Declaration::Namespace || context->owner()->kind() == Declaration::NamespaceAlias ) && decl.second != 0 && decl.second != 1001) { // Only show the local declarations of modules, or the declarations // immediately in its imported parent contexts (that are global // contexts, hence the distance of 1001). This prevens "String()", // "QtQuick1.0" and "builtins" from being listed when the user // types "PlasmaCore.". continue; } else if (decorationOfThisItem == CompletionItem::NoDecoration && declaration->abstractType() && declaration->abstractType()->whichType() == AbstractType::TypeFunction) { // Decorate function calls with brackets decorationOfThisItem = CompletionItem::Brackets; } else if (flags.testFlag(CompletionHideWrappers)) { ClassDeclaration* classDecl = dynamic_cast(declaration.data()); if (classDecl && classDecl->classType() == ClassDeclarationData::Interface) { continue; } } items << CompletionTreeItemPointer(new CompletionItem(declaration, decl.second, decorationOfThisItem)); } } return items; } QList CodeCompletionContext::fieldCompletions(const QString& expression, CompletionItem::Decoration decoration) { // The statement given to this method ends with an expression that may identify // a declaration ("foo" in "test(1, 2, foo"). List the declarations of this // inner context DUContext* context = getInternalContext(declarationAtEndOfString(expression)); if (context) { return completionsInContext(DUContextPointer(context), CompletionOnlyLocal, decoration); } else { return QList(); } } Stack CodeCompletionContext::expressionStack(const QString& expression) { Stack stack; ExpressionStackEntry entry; QmlJS::Lexer lexer(nullptr); bool atEnd = false; lexer.setCode(expression, 1, false); entry.startPosition = 0; entry.operatorStart = 0; entry.operatorEnd = 0; entry.commas = 0; stack.push(entry); // NOTE: KDevelop uses 0-indexed columns while QMLJS uses 1-indexed columns while (!atEnd) { switch (lexer.lex()) { case QmlJSGrammar::EOF_SYMBOL: atEnd = true; break; case QmlJSGrammar::T_LBRACE: case QmlJSGrammar::T_LBRACKET: case QmlJSGrammar::T_LPAREN: entry.startPosition = lexer.tokenEndColumn() - 1; entry.operatorStart = entry.startPosition; entry.operatorEnd = entry.startPosition; entry.commas = 0; stack.push(entry); break; case QmlJSGrammar::T_RBRACE: case QmlJSGrammar::T_RBRACKET: case QmlJSGrammar::T_RPAREN: if (stack.count() > 1) { stack.pop(); } break; case QmlJSGrammar::T_IDENTIFIER: case QmlJSGrammar::T_DOT: case QmlJSGrammar::T_THIS: break; case QmlJSGrammar::T_COMMA: stack.top().commas++; default: // The last operator of every sub-expression is stored on the stack // so that "A = foo." can know that attributes of foo having the same // type as A should be highlighted. stack.top().operatorStart = lexer.tokenStartColumn() - 1; stack.top().operatorEnd = lexer.tokenEndColumn() - 1; } } return stack; } DeclarationPointer CodeCompletionContext::declarationAtEndOfString(const QString& expression) { // Build the expression stack of expression and use the valid portion of the // top sub-expression to find the right-most declaration that can be found // in expression. QmlJS::Document::MutablePtr doc = QmlJS::Document::create(QStringLiteral("inline"), Dialect::JavaScript); ExpressionStackEntry topEntry = expressionStack(expression).top(); doc->setSource(expression.mid(topEntry.operatorEnd)); doc->parseExpression(); if (!doc || !doc->isParsedCorrectly()) { return DeclarationPointer(); } // Use ExpressionVisitor to find the type (and associated declaration) of // the snippet that has been parsed. The inner context of the declaration // can be used to get the list of completions ExpressionVisitor visitor(m_duContext.data()); doc->ast()->accept(&visitor); return visitor.lastDeclaration(); } bool CodeCompletionContext::containsOnlySpaces(const QString& str) { for (int i=0; i * Copyright (c) 2014 Denis Steckelmacher * * 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 "cache.h" #include "debug.h" +#include #include #include #include #include #include #include QmlJS::Cache::Cache() { // qmlplugindump from Qt4 and Qt5. They will be tried in order when dumping // a binary QML file. m_pluginDumpExecutables << PluginDumpExecutable(QStringLiteral("qmlplugindump"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt4"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt5"), QStringLiteral("2.0")) << PluginDumpExecutable(QStringLiteral("qml1plugindump-qt5"), QStringLiteral("1.0")); } QmlJS::Cache& QmlJS::Cache::instance() { static Cache *c = nullptr; if (!c) { c = new Cache(); } return *c; } QString QmlJS::Cache::modulePath(const KDevelop::IndexedString& baseFile, const QString& uri, const QString& version) { QMutexLocker lock(&m_mutex); QString cacheKey = uri + version; QString path = m_modulePaths.value(cacheKey, QString()); if (!path.isEmpty()) { return path; } // List of the paths in which the modules will be looked for KDevelop::Path::List paths; - for (auto path : QCoreApplication::instance()->libraryPaths()) { + const auto& libraryPaths = QCoreApplication::instance()->libraryPaths(); + for (auto& path : libraryPaths) { KDevelop::Path p(path); // Change /path/to/qt5/plugins to /path/to/qt5/{qml,imports} paths << p.cd(QStringLiteral("../qml")); paths << p.cd(QStringLiteral("../imports")); } paths << m_includeDirs[baseFile]; // Find the path for which /u/r/i exists QString fragment = QString(uri).replace(QLatin1Char('.'), QDir::separator()); bool isVersion1 = version.startsWith(QLatin1String("1.")); bool isQtQuick = (uri == QLatin1String("QtQuick")); const QStringList modulesWithoutVersionSuffix{"QtQml", "QtMultimedia", "QtQuick.LocalStorage", "QtQuick.XmlListModel"}; if (!version.isEmpty() && !isVersion1 && !modulesWithoutVersionSuffix.contains(uri)) { // Modules having a version greater or equal to 2 are stored in a directory // name like QtQuick.2 fragment += QLatin1Char('.') + version.section(QLatin1Char('.'), 0, 0); } - for (auto p : paths) { + for (auto& p : qAsConst(paths)) { QString pathString = p.cd(fragment).path(); // HACK: QtQuick 1.0 is put in $LIB/qt5/imports/builtins.qmltypes. The "QtQuick" // identifier appears nowhere. if (isQtQuick && isVersion1) { if (QFile::exists(p.cd(QStringLiteral("builtins.qmltypes")).path())) { path = p.path(); break; } } else if (QFile::exists(pathString + QLatin1String("/plugins.qmltypes"))) { path = pathString; break; } } m_modulePaths.insert(cacheKey, path); return path; } QStringList QmlJS::Cache::getFileNames(const QFileInfoList& fileInfos) { QStringList result; for (const QFileInfo& fileInfo : fileInfos) { QString filePath = fileInfo.canonicalFilePath(); // If the module directory contains a plugins.qmltypes files, use it // and skip everything else if (filePath.endsWith(QLatin1String("plugins.qmltypes"))) { return QStringList() << filePath; } else if (fileInfo.dir().exists(QStringLiteral("plugins.qmltypes"))) { return {fileInfo.dir().filePath(QStringLiteral("plugins.qmltypes"))}; } // Non-so files don't need any treatment if (!filePath.endsWith(QLatin1String(".so"))) { result.append(filePath); continue; } // Use the cache to speed-up reparses { QMutexLocker lock(&m_mutex); if (m_modulePaths.contains(filePath)) { QString cachedFilePath = m_modulePaths.value(filePath); if (!cachedFilePath.isEmpty()) { result.append(cachedFilePath); } continue; } } // Locate an existing dump of the file QString dumpFile = QStringLiteral("kdevqmljssupport/%1.qml").arg( QString::fromLatin1(QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Md5).toHex()) ); QString dumpPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dumpFile ); if (!dumpPath.isEmpty()) { QMutexLocker lock(&m_mutex); result.append(dumpPath); m_modulePaths.insert(filePath, dumpPath); continue; } // Create a dump of the file const QStringList args = {QStringLiteral("-noinstantiate"), QStringLiteral("-path"), filePath}; - for (const PluginDumpExecutable& executable : m_pluginDumpExecutables) { + for (const PluginDumpExecutable& executable : qAsConst(m_pluginDumpExecutables)) { QProcess qmlplugindump; qmlplugindump.setProcessChannelMode(QProcess::SeparateChannels); qmlplugindump.start(executable.executable, args, QIODevice::ReadOnly); qCDebug(KDEV_QMLJS_DUCHAIN) << "starting qmlplugindump with args:" << executable.executable << args << qmlplugindump.state() << fileInfo.absolutePath(); if (!qmlplugindump.waitForFinished(3000)) { if (qmlplugindump.state() == QProcess::Running) { qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump didn't finish in time -- killing"; qmlplugindump.kill(); qmlplugindump.waitForFinished(100); } else { qCDebug(KDEV_QMLJS_DUCHAIN) << "qmlplugindump attempt failed" << qmlplugindump.program() << qmlplugindump.arguments() << qmlplugindump.readAllStandardError(); } continue; } if (qmlplugindump.exitCode() != 0) { qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump finished with exit code:" << qmlplugindump.exitCode(); continue; } // Open a file in which the dump can be written QFile dumpFile( QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + dumpPath ); if (dumpFile.open(QIODevice::WriteOnly)) { qmlplugindump.readLine(); // Skip "import QtQuick.tooling 1.1" dumpFile.write("// " + filePath.toUtf8() + '\n'); dumpFile.write("import QtQuick " + executable.quickVersion.toUtf8() + '\n'); dumpFile.write(qmlplugindump.readAllStandardOutput()); dumpFile.close(); result.append(dumpFile.fileName()); QMutexLocker lock(&m_mutex); m_modulePaths.insert(filePath, dumpFile.fileName()); break; } } } return result; } void QmlJS::Cache::setFileCustomIncludes(const KDevelop::IndexedString& file, const KDevelop::Path::List& dirs) { QMutexLocker lock(&m_mutex); m_includeDirs[file] = dirs; } void QmlJS::Cache::addDependency(const KDevelop::IndexedString& file, const KDevelop::IndexedString& dependency) { QMutexLocker lock(&m_mutex); m_dependees[dependency].insert(file); m_dependencies[file].insert(dependency); } QList QmlJS::Cache::filesThatDependOn(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependees[file].toList(); } QList QmlJS::Cache::dependencies(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependencies[file].toList(); } bool QmlJS::Cache::isUpToDate(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_isUpToDate.value(file, false); } void QmlJS::Cache::setUpToDate(const KDevelop::IndexedString& file, bool upToDate) { QMutexLocker lock(&m_mutex); m_isUpToDate[file] = upToDate; } diff --git a/plugins/qmljs/duchain/declarationbuilder.cpp b/plugins/qmljs/duchain/declarationbuilder.cpp index 22975300d6..228cde9b36 100644 --- a/plugins/qmljs/duchain/declarationbuilder.cpp +++ b/plugins/qmljs/duchain/declarationbuilder.cpp @@ -1,1543 +1,1543 @@ /************************************************************************************* * Copyright (C) 2012 by Aleix Pol * * Copyright (C) 2012 by Milian Wolff * * * * 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 "declarationbuilder.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include "expressionvisitor.h" #include "parsesession.h" #include "functiondeclaration.h" #include "functiontype.h" #include "helper.h" #include "cache.h" #include "frameworks/nodejs.h" #include #include #include using namespace KDevelop; DeclarationBuilder::DeclarationBuilder(ParseSession* session) : m_prebuilding(false) { m_session = session; } ReferencedTopDUContext DeclarationBuilder::build(const IndexedString& url, QmlJS::AST::Node* node, const ReferencedTopDUContext& updateContext_) { Q_ASSERT(m_session->url() == url); ReferencedTopDUContext updateContext(updateContext_); // The declaration builder needs to run twice, so it can resolve uses of e.g. functions // which are called before they are defined (which is easily possible, due to JS's dynamic nature). if (!m_prebuilding) { qCDebug(KDEV_QMLJS_DUCHAIN) << "building, but running pre-builder first"; auto prebuilder = new DeclarationBuilder(m_session); prebuilder->m_prebuilding = true; updateContext = prebuilder->build(url, node, updateContext); qCDebug(KDEV_QMLJS_DUCHAIN) << "pre-builder finished"; delete prebuilder; if (!m_session->allDependenciesSatisfied()) { qCDebug(KDEV_QMLJS_DUCHAIN) << "dependencies were missing, don't perform the second parsing pass"; return updateContext; } } else { qCDebug(KDEV_QMLJS_DUCHAIN) << "prebuilding"; } return DeclarationBuilderBase::build(url, node, updateContext); } void DeclarationBuilder::startVisiting(QmlJS::AST::Node* node) { DUContext* builtinQmlContext = nullptr; if (QmlJS::isQmlFile(currentContext()) && !currentContext()->url().str().contains(QLatin1String("__builtin_qml.qml"))) { builtinQmlContext = m_session->contextOfFile( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/nodejsmodules/__builtin_qml.qml")) ); } { DUChainWriteLocker lock; // Remove all the imported parent contexts: imports may have been edited // and there musn't be any leftover parent context currentContext()->topContext()->clearImportedParentContexts(); // Initialize Node.js QmlJS::NodeJS::instance().initialize(this); // Built-in QML types (color, rect, etc) if (builtinQmlContext) { topContext()->addImportedParentContext(builtinQmlContext); } } DeclarationBuilderBase::startVisiting(node); } /* * Functions */ template void DeclarationBuilder::declareFunction(QmlJS::AST::Node* node, bool newPrototypeContext, const Identifier& name, const RangeInRevision& nameRange, QmlJS::AST::Node* parameters, const RangeInRevision& parametersRange, QmlJS::AST::Node* body, const RangeInRevision& bodyRange) { setComment(node); // Declare the function QmlJS::FunctionType::Ptr func(new QmlJS::FunctionType); Decl* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, nameRange); decl->setKind(Declaration::Type); func->setDeclaration(decl); decl->setType(func); } openType(func); // Parameters, if any (a function must always have an internal function context, // so always open a context here even if there are no parameters) DUContext* parametersContext = openContext( node + 1, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, bodyRange.end), // Ensure that this context contains both the parameters and the body DUContext::Function, QualifiedIdentifier(name) ); if (parameters) { QmlJS::AST::Node::accept(parameters, this); } // The internal context of the function is its parameter context { DUChainWriteLocker lock; decl->setInternalContext(parametersContext); } // Open the prototype context, if any. This has to be done before the body // because this context is needed for "this" to be properly resolved // in it. if (newPrototypeContext) { DUChainWriteLocker lock; QmlJS::FunctionDeclaration* d = reinterpret_cast(decl); d->setPrototypeContext(openContext( node + 2, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, parametersRange.start), DUContext::Function, // This allows QmlJS::getOwnerOfContext to know that the parent of this context is the function declaration QualifiedIdentifier(name) )); if (name != Identifier(QStringLiteral("Object"))) { // Every class inherit from Object QmlJS::importObjectContext(currentContext(), topContext()); } closeContext(); } // Body, if any (it is a child context of the parameters) openContext( node, bodyRange, DUContext::Other, QualifiedIdentifier(name) ); if (body) { QmlJS::AST::Node::accept(body, this); } // Close the body context and then the parameters context closeContext(); closeContext(); } template void DeclarationBuilder::declareParameters(Node* node, QStringRef Node::*typeAttribute) { for (Node *plist = node; plist; plist = plist->next) { const Identifier name(plist->name.toString()); const RangeInRevision range = m_session->locationToRange(plist->identifierToken); AbstractType::Ptr type = (typeAttribute ? typeFromName((plist->*typeAttribute).toString()) : // The typeAttribute attribute of plist contains the type name of the argument AbstractType::Ptr(new IntegralType(IntegralType::TypeMixed)) // No type information, use mixed ); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); closeAndAssignType(); if (QmlJS::FunctionType::Ptr funType = currentType()) { funType->addArgument(type); } } } bool DeclarationBuilder::visit(QmlJS::AST::FunctionDeclaration* node) { declareFunction( node, true, // A function declaration always has its own prototype context Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FunctionExpression* node) { declareFunction( node, false, Identifier(), QmlJS::emptyRangeOnLine(node->functionToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FormalParameterList* node) { declareParameters(node, (QStringRef QmlJS::AST::FormalParameterList::*)nullptr); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiParameterList* node) { declareParameters(node, &QmlJS::AST::UiParameterList::type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::ReturnStatement* node) { if (QmlJS::FunctionType::Ptr func = currentType()) { AbstractType::Ptr returnType; if (node->expression) { returnType = findType(node->expression).type; } else { returnType = new IntegralType(IntegralType::TypeVoid); } DUChainWriteLocker lock; func->setReturnType(QmlJS::mergeTypes(func->returnType(), returnType)); } return false; // findType has already explored node } void DeclarationBuilder::endVisitFunction() { QmlJS::FunctionType::Ptr func = currentType(); if (func && !func->returnType()) { // A function that returns nothing returns void DUChainWriteLocker lock; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionDeclaration* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionExpression* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } /* * Variables */ void DeclarationBuilder::inferArgumentsFromCall(QmlJS::AST::Node* base, QmlJS::AST::ArgumentList* arguments) { ContextBuilder::ExpressionType expr = findType(base); QmlJS::FunctionType::Ptr func_type = QmlJS::FunctionType::Ptr::dynamicCast(expr.type); DUChainWriteLocker lock; if (!func_type) { return; } auto func_declaration = dynamic_cast(func_type->declaration(topContext())); if (!func_declaration || !func_declaration->internalContext()) { return; } // Put the argument nodes in a list that has a definite size QVector argumentDecls = func_declaration->internalContext()->localDeclarations(); QVector args; for (auto argument = arguments; argument; argument = argument->next) { args.append(argument); } // Don't update a function when it is called with the wrong number // of arguments if (args.size() != argumentDecls.count()) { return; } // Update the types of the function arguments QmlJS::FunctionType::Ptr new_func_type(new QmlJS::FunctionType); for (int i=0; iabstractType(); // Merge the current type of the argument with its type in the call expression AbstractType::Ptr call_type = findType(argument->expression).type; AbstractType::Ptr new_type = QmlJS::mergeTypes(current_type, call_type); // Update the declaration of the argument and its type in the function type if (func_declaration->topContext() == topContext()) { new_func_type->addArgument(new_type); argumentDecls.at(i)->setAbstractType(new_type); } // Add a warning if it is possible that the argument types don't match if (!m_prebuilding && !areTypesEqual(current_type, call_type)) { m_session->addProblem(argument, i18n( "Possible type mismatch between the argument type (%1) and the value passed as argument (%2)", current_type->toString(), call_type->toString() ), IProblem::Hint); } } // Replace the function's type with the new type having updated arguments if (func_declaration->topContext() == topContext()) { new_func_type->setReturnType(func_type->returnType()); new_func_type->setDeclaration(func_declaration); func_declaration->setAbstractType(new_func_type.cast()); if (expr.declaration) { // expr.declaration is the variable that contains the function, while // func_declaration is the declaration of the function. They can be // different and both need to be updated expr.declaration->setAbstractType(new_func_type.cast()); } } return; } bool DeclarationBuilder::visit(QmlJS::AST::VariableDeclaration* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); const Identifier name(node->name.toString()); const RangeInRevision range = m_session->locationToRange(node->identifierToken); const AbstractType::Ptr type = findType(node->expression).type; { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); return false; // findType has already explored node } void DeclarationBuilder::endVisit(QmlJS::AST::VariableDeclaration* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } bool DeclarationBuilder::visit(QmlJS::AST::BinaryExpression* node) { if (node->op == QSOperator::Assign) { ExpressionType leftType = findType(node->left); ExpressionType rightType = findType(node->right); DUChainWriteLocker lock; if (leftType.declaration) { DUContext* leftCtx = leftType.declaration->context(); DUContext* leftInternalCtx = QmlJS::getInternalContext(leftType.declaration); // object.prototype.method = function(){} : when assigning a function // to a variable living in a Class context, set the prototype // context of the function to the context of the variable if (rightType.declaration && leftCtx->type() == DUContext::Class) { auto func = rightType.declaration.dynamicCast(); if (!QmlJS::getOwnerOfContext(leftCtx) && !leftCtx->importers().isEmpty()) { // MyClass.prototype.myfunc declares "myfunc" in a small context // that is imported by MyClass. The prototype of myfunc should // be the context of MyClass, not the small context in which // it has been declared leftCtx = leftCtx->importers().at(0); } if (func && !func->prototypeContext()) { func->setPrototypeContext(leftCtx); } } if (leftType.declaration->topContext() != topContext()) { // Do not modify a declaration of another file } else if (leftType.isPrototype && leftInternalCtx) { // Assigning something to a prototype is equivalent to making it // inherit from a class: "Class.prototype = ClassOrObject;" leftInternalCtx->clearImportedParentContexts(); QmlJS::importDeclarationInContext( leftInternalCtx, rightType.declaration ); } else { // Merge the already-known type of the variable with the new one leftType.declaration->setAbstractType(QmlJS::mergeTypes(leftType.type, rightType.type)); } } return false; // findType has already explored node } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::CallExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } bool DeclarationBuilder::visit(QmlJS::AST::NewMemberExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } /* * Arrays */ void DeclarationBuilder::declareFieldMember(const KDevelop::DeclarationPointer& declaration, const QString& member, QmlJS::AST::Node* node, const QmlJS::AST::SourceLocation& location) { if (QmlJS::isPrototypeIdentifier(member)) { // Don't declare "prototype", this is a special member return; } if (!m_session->allDependenciesSatisfied()) { // Don't declare anything automatically if dependencies are missing: the // checks hereafter may pass now but fail later, thus causing disappearing // declarations return; } DUChainWriteLocker lock; Identifier identifier(member); // Declaration must have an internal context so that the member can be added // into it. DUContext* ctx = QmlJS::getInternalContext(declaration); if (!ctx || ctx->topContext() != topContext()) { return; } // No need to re-declare a field if it already exists // TODO check if we can make getDeclaration receive an Identifier directly if (QmlJS::getDeclaration(QualifiedIdentifier(identifier), ctx, false)) { return; } // The internal context of declaration is already closed and does not contain // location. This can be worked around by opening a new context, declaring the // new field in it, and then adding the context as a parent of // declaration->internalContext(). RangeInRevision range = m_session->locationToRange(location); IntegralType::Ptr type = IntegralType::Ptr(new IntegralType(IntegralType::TypeMixed)); DUContext* importedContext = openContext(node, range, DUContext::Class); Declaration* decl = openDeclaration(identifier, range); decl->setInSymbolTable(false); // This declaration is in an anonymous context, and the symbol table acts as if the declaration was in the global context openType(type); closeAndAssignType(); closeContext(); ctx->addImportedParentContext(importedContext); } bool DeclarationBuilder::visit(QmlJS::AST::FieldMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, node->name.toString(), node, node->identifierToken ); } return false; // findType has already visited node->base } bool DeclarationBuilder::visit(QmlJS::AST::ArrayMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // When the user types array["new_key"], declare "new_key" as a new field of // array. auto stringLiteral = QmlJS::AST::cast(node->expression); if (!stringLiteral) { return DeclarationBuilderBase::visit(node); } ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, stringLiteral->value.toString(), node, stringLiteral->literalToken ); } node->expression->accept(this); return false; // findType has already visited node->base, and we have just visited node->expression } bool DeclarationBuilder::visit(QmlJS::AST::ObjectLiteral* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // Object literals can appear in the "values" property of enumerations. Their // keys must be declared in the enumeration, not in an anonymous class if (currentContext()->type() == DUContext::Enum) { return DeclarationBuilderBase::visit(node); } // Open an anonymous class declaration, with its internal context StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; ClassDeclaration* decl = openDeclaration( Identifier(), QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->setKind(Declaration::Type); decl->setInternalContext(openContext( node, m_session->locationsToRange(node->lbraceToken, node->rbraceToken), DUContext::Class )); type->setDeclaration(decl); // Every object literal inherits from Object QmlJS::importObjectContext(currentContext(), topContext()); } openType(type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::PropertyNameAndValue* node) { setComment(node); if (!node->name || !node->value) { return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->name->propertyNameToken)); Identifier name(QmlJS::getNodeValue(node->name)); // The type of the declaration can either be an enumeration value or the type // of its expression ExpressionType type; bool inSymbolTable = false; if (currentContext()->type() == DUContext::Enum) { // This is an enumeration value auto value = QmlJS::AST::cast(node->value); EnumeratorType::Ptr enumerator(new EnumeratorType); enumerator->setDataType(IntegralType::TypeInt); if (value) { enumerator->setValue((int)value->value); } type.type = AbstractType::Ptr::staticCast(enumerator); type.declaration = nullptr; inSymbolTable = true; } else { // Normal value type = findType(node->value); } // If a function is assigned to an object member, set the prototype context // of the function to the object containing the member if (type.declaration) { DUChainWriteLocker lock; auto func = type.declaration.dynamicCast(); if (func && !func->prototypeContext()) { func->setPrototypeContext(currentContext()); } } // Open the declaration { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setInSymbolTable(inSymbolTable); } openType(type.type); return false; // findType has already explored node->expression } void DeclarationBuilder::endVisit(QmlJS::AST::PropertyNameAndValue* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::ObjectLiteral* node) { DeclarationBuilderBase::endVisit(node); if (currentContext()->type() != DUContext::Enum) { // Enums are special-cased in visit(ObjectLiteral) closeContext(); closeAndAssignType(); } } /* * plugins.qmltypes files */ void DeclarationBuilder::declareComponent(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QString baseClass = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("prototype")).value.section('/', -1, -1); // Declare the component itself StructureType::Ptr type(new StructureType); ClassDeclaration* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Interface); decl->clearBaseClasses(); if (!baseClass.isEmpty()) { addBaseClass(decl, baseClass); } type->setDeclaration(decl); decl->setType(type); // declareExports needs to know the type of decl } openType(type); } void DeclarationBuilder::declareMethod(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name, bool isSlot, bool isSignal) { QString type_name = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value; QmlJS::FunctionType::Ptr type(new QmlJS::FunctionType); if (type_name.isEmpty()) { type->setReturnType(typeFromName(QStringLiteral("void"))); } else { type->setReturnType(typeFromName(type_name)); } { DUChainWriteLocker lock; ClassFunctionDeclaration* decl = openDeclaration(name, range); decl->setIsSlot(isSlot); decl->setIsSignal(isSignal); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareProperty(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setAbstractType(type); } openType(type); } void DeclarationBuilder::declareParameter(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QmlJS::FunctionType::Ptr function = currentType(); AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); Q_ASSERT(function); function->addArgument(type); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); } void DeclarationBuilder::declareEnum(const RangeInRevision &range, const Identifier &name) { EnumerationType::Ptr type(new EnumerationType); { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setType(type); // The type needs to be set here because closeContext is called before closeAndAssignType and needs to know the type of decl type->setDataType(IntegralType::TypeEnumeration); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareComponentSubclass(QmlJS::AST::UiObjectInitializer* node, const KDevelop::RangeInRevision& range, const QString& baseclass, QmlJS::AST::UiQualifiedId* qualifiedId) { Identifier name( QmlJS::getQMLAttributeValue(node->members, QStringLiteral("name")).value.section('/', -1, -1) ); DUContext::ContextType contextType = DUContext::Class; if (baseclass == QLatin1String("Component")) { // QML component, equivalent to a QML class declareComponent(node, range, name); } else if (baseclass == QLatin1String("Method") || baseclass == QLatin1String("Signal") || baseclass == QLatin1String("Slot")) { // Method (that can also be a signal or a slot) declareMethod(node, range, name, baseclass == QLatin1String("Slot"), baseclass == QLatin1String("Signal")); contextType = DUContext::Function; } else if (baseclass == QLatin1String("Property")) { // A property declareProperty(node, range, name); } else if (baseclass == QLatin1String("Parameter") && currentType()) { // One parameter of a signal/slot/method declareParameter(node, range, name); } else if (baseclass == QLatin1String("Enum")) { // Enumeration. The "values" key contains a dictionary of name -> number entries. declareEnum(range, name); contextType = DUContext::Enum; name = Identifier(); // Enum contexts should have no name so that their members have the correct scope } else { // Define an anonymous subclass of the baseclass. This subclass will // be instantiated when "id:" is encountered name = Identifier(); // Use ExpressionVisitor to find the declaration of the base class DeclarationPointer baseClass = findType(qualifiedId).declaration; StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; ClassDeclaration* decl = openDeclaration( currentContext()->type() == DUContext::Global ? Identifier(m_session->moduleName()) : name, QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->clearBaseClasses(); decl->setKind(Declaration::Type); decl->setType(type); // The class needs to know its type early because it contains definitions that depend on that type type->setDeclaration(decl); if (baseClass) { addBaseClass(decl, baseClass->indexedType()); } } openType(type); } // Open a context of the proper type and identifier openContext( node, m_session->locationsToInnerRange(node->lbraceToken, node->rbraceToken), contextType, QualifiedIdentifier(name) ); DUContext* ctx = currentContext(); Declaration* decl = currentDeclaration(); { // Set the inner context of the current declaration, because nested classes // need to know the inner context of their parents DUChainWriteLocker lock; decl->setInternalContext(ctx); if (contextType == DUContext::Enum) { ctx->setPropagateDeclarations(true); } } // If we have have declared a class, import the context of its base classes registerBaseClasses(); } void DeclarationBuilder::declareComponentInstance(QmlJS::AST::ExpressionStatement* expression) { if (!expression) { return; } auto identifier = QmlJS::AST::cast(expression->expression); if (!identifier) { return; } { DUChainWriteLocker lock; injectContext(topContext()); Declaration* decl = openDeclaration( Identifier(identifier->name.toString()), m_session->locationToRange(identifier->identifierToken) ); closeInjectedContext(); // Put the declaration in the global scope decl->setKind(Declaration::Instance); decl->setType(currentAbstractType()); } closeDeclaration(); } DeclarationBuilder::ExportLiteralsAndNames DeclarationBuilder::exportedNames(QmlJS::AST::ExpressionStatement* exports) { ExportLiteralsAndNames res; if (!exports) { return res; } auto exportslist = QmlJS::AST::cast(exports->expression); if (!exportslist) { return res; } // Explore all the exported symbols for this component and keep only those // having a version compatible with the one of this module QSet knownNames; for (auto it = exportslist->elements; it && it->expression; it = it->next) { auto stringliteral = QmlJS::AST::cast(it->expression); if (!stringliteral) { continue; } // String literal like "Namespace/Class version". QStringList nameAndVersion = stringliteral->value.toString().section('/', -1, -1).split(' '); QString name = nameAndVersion.at(0); QString version = (nameAndVersion.count() > 1 ? nameAndVersion.at(1) : QStringLiteral("1.0")); if (!knownNames.contains(name)) { knownNames.insert(name); res.append(qMakePair(stringliteral, name)); } } return res; } void DeclarationBuilder::declareExports(const ExportLiteralsAndNames& exports, ClassDeclaration* classdecl) { DUChainWriteLocker lock; // Create the exported versions of the component - for (auto exp : exports) { + for (auto& exp : exports) { QmlJS::AST::StringLiteral* literal = exp.first; QString name = exp.second; StructureType::Ptr type(new StructureType); injectContext(currentContext()->parentContext()); // Don't declare the export in its C++-ish component, but in the scope above ClassDeclaration* decl = openDeclaration( Identifier(name), m_session->locationToRange(literal->literalToken) ); closeInjectedContext(); // The exported version inherits from the C++ component decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Class); decl->clearBaseClasses(); type->setDeclaration(decl); addBaseClass(decl, classdecl->indexedType()); // Open a context for the exported class, and register its base class in it decl->setInternalContext(openContext( literal, DUContext::Class, QualifiedIdentifier(name) )); registerBaseClasses(); closeContext(); openType(type); closeAndAssignType(); } } /* * UI */ void DeclarationBuilder::importDirectory(const QString& directory, QmlJS::AST::UiImport* node) { DUChainWriteLocker lock; QString currentFilePath = currentContext()->topContext()->url().str(); QFileInfo dir(directory); QFileInfoList entries; if (dir.isDir()) { // Import all the files in the given directory entries = QDir(directory).entryInfoList( QStringList{ (QLatin1String("*.") + currentFilePath.section(QLatin1Char('.'), -1, -1)), QStringLiteral("*.qmltypes"), QStringLiteral("*.so")}, QDir::Files ); } else if (dir.isFile()) { // Import the specific file given in the import statement entries.append(dir); } else if (!m_prebuilding) { m_session->addProblem(node, i18n("Module not found, some types or properties may not be recognized")); return; } // Translate the QFileInfos into QStrings (and replace .so files with // qmlplugindump dumps) lock.unlock(); QStringList filePaths = QmlJS::Cache::instance().getFileNames(entries); lock.lock(); if (node && !node->importId.isEmpty()) { // Open a namespace that will contain the declarations Identifier identifier(node->importId.toString()); RangeInRevision range = m_session->locationToRange(node->importIdToken); Declaration* decl = openDeclaration(identifier, range); decl->setKind(Declaration::Namespace); decl->setInternalContext(openContext(node, range, DUContext::Class, QualifiedIdentifier(identifier))); } for (const QString& filePath : filePaths) { if (filePath == currentFilePath) { continue; } ReferencedTopDUContext context = m_session->contextOfFile(filePath); if (context) { currentContext()->addImportedParentContext(context.data()); } } if (node && !node->importId.isEmpty()) { // Close the namespace containing the declarations closeContext(); closeDeclaration(); } } void DeclarationBuilder::importModule(QmlJS::AST::UiImport* node) { QmlJS::AST::UiQualifiedId *part = node->importUri; QString uri; while (part) { if (!uri.isEmpty()) { uri.append('.'); } uri.append(part->name.toString()); part = part->next; } // Version of the import QString version = m_session->symbolAt(node->versionToken); // Import the directory containing the module QString modulePath = QmlJS::Cache::instance().modulePath(m_session->url(), uri, version); importDirectory(modulePath, node); } bool DeclarationBuilder::visit(QmlJS::AST::UiImport* node) { if (node->importUri) { importModule(node); } else if (!node->fileName.isEmpty() && node->fileName != QLatin1String(".")) { QUrl currentFileUrl = currentContext()->topContext()->url().toUrl(); QUrl importUrl = QUrl(node->fileName.toString()); importDirectory(currentFileUrl.resolved(importUrl).toLocalFile(), node); } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectDefinition* node) { setComment(node); // Do not crash if the user has typed an empty object definition if (!node->initializer || !node->initializer->members) { m_skipEndVisit.push(true); return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->qualifiedTypeNameId->identifierToken)); QString baseclass = node->qualifiedTypeNameId->name.toString(); // "Component" needs special care: a component that appears only in a future // version of this module, or that already appeared in a former version, must // be skipped because it is useless ExportLiteralsAndNames exports; if (baseclass == QLatin1String("Component")) { QmlJS::AST::Statement* statement = QmlJS::getQMLAttribute(node->initializer->members, QStringLiteral("exports")); exports = exportedNames(QmlJS::AST::cast(statement)); if (statement && exports.count() == 0) { // This component has an "exports:" member but no export matched // the version of this module. Skip the component m_skipEndVisit.push(true); return false; } } else if (baseclass == QLatin1String("Module")) { // "Module" is disabled. This allows the declarations of a module // dump to appear in the same namespace as the .qml files in the same // directory. m_skipEndVisit.push(true); return true; } // Declare the component subclass declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); // If we had a component with exported names, declare these exports if (baseclass == QLatin1String("Component")) { ClassDeclaration* classDecl = currentDeclaration(); if (classDecl) { declareExports(exports, classDecl); } } m_skipEndVisit.push(false); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectDefinition* node) { DeclarationBuilderBase::endVisit(node); // Do not crash if the user has typed an empty object definition if (!m_skipEndVisit.pop()) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiScriptBinding* node) { setComment(node); if (!node->qualifiedId) { return DeclarationBuilderBase::visit(node); } // Special-case some binding names QString bindingName = node->qualifiedId->name.toString(); if (bindingName == QLatin1String("id")) { // Instantiate a QML component: its type is the current type (the anonymous // QML class that surrounds the declaration) declareComponentInstance(QmlJS::AST::cast(node->statement)); } // Use ExpressionVisitor to find the signal/property bound DeclarationPointer bindingDecl = findType(node->qualifiedId).declaration; DUChainPointer signal; // If a Javascript block is used as expression or if the script binding is a // slot, open a subcontext so that variables declared in the binding are kept // local, and the signal parameters can be visible to the slot if (( bindingDecl && (signal = bindingDecl.dynamicCast()) && signal->isSignal() ) || node->statement->kind == QmlJS::AST::Node::Kind_Block) { openContext( node->statement, m_session->locationsToInnerRange( node->statement->firstSourceLocation(), node->statement->lastSourceLocation() ), DUContext::Other ); // If this script binding is a slot, import the parameters of its signal if (signal && signal->isSignal() && signal->internalContext()) { DUChainWriteLocker lock; currentContext()->addIndirectImport(DUContext::Import( signal->internalContext(), nullptr )); } } else { // Check that the type of the value matches the type of the property AbstractType::Ptr expressionType = findType(node->statement).type; DUChainReadLocker lock; if (!m_prebuilding && bindingDecl && !areTypesEqual(bindingDecl->abstractType(), expressionType)) { m_session->addProblem(node->qualifiedId, i18n( "Mismatch between the value type (%1) and the property type (%2)", expressionType->toString(), bindingDecl->abstractType()->toString() ), IProblem::Error); } } return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiScriptBinding* node) { QmlJS::AST::Visitor::endVisit(node); // If visit(UiScriptBinding) has opened a context, close it if (currentContext()->type() == DUContext::Other) { closeContext(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectBinding* node) { setComment(node); if (!node->qualifiedId || !node->qualifiedTypeNameId || !node->initializer) { return DeclarationBuilderBase::visit(node); } // Declare the component subclass. "Behavior on ... {}" is treated exactly // like "Behavior {}". RangeInRevision range = m_session->locationToRange(node->qualifiedTypeNameId->identifierToken); QString baseclass = node->qualifiedTypeNameId->name.toString(); declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectBinding* node) { DeclarationBuilderBase::endVisit(node); if (node->qualifiedId && node->qualifiedTypeNameId && node->initializer) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiPublicMember* node) { setComment(node); RangeInRevision range = m_session->locationToRange(node->identifierToken); Identifier id(node->name.toString()); QString typeName = node->memberType.toString(); bool res = DeclarationBuilderBase::visit(node); // Build the type of the public member if (node->type == QmlJS::AST::UiPublicMember::Signal) { // Open a function declaration corresponding to this signal declareFunction( node, false, Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->parameters, m_session->locationToRange(node->identifierToken), // The AST does not provide the location of the parens nullptr, m_session->locationToRange(node->identifierToken) // A body range must be provided ); // This declaration is a signal and its return type is void { DUChainWriteLocker lock; currentDeclaration()->setIsSignal(true); currentType()->setReturnType(typeFromName(QStringLiteral("void"))); } } else { AbstractType::Ptr type; if (typeName == QLatin1String("alias")) { // Property aliases take the type of their aliased property type = findType(node->statement).type; res = false; // findType has already explored node->statement } else { type = typeFromName(typeName); if (node->typeModifier == QLatin1String("list")) { // QML list, noted "list" in the source file ArrayType::Ptr array(new ArrayType); array->setElementType(type); type = array.cast(); } } { DUChainWriteLocker lock; Declaration* decl = openDeclaration(id, range); decl->setInSymbolTable(false); } openType(type); } return res; } void DeclarationBuilder::endVisit(QmlJS::AST::UiPublicMember* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } /* * Utils */ void DeclarationBuilder::setComment(QmlJS::AST::Node* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); } void DeclarationBuilder::closeAndAssignType() { closeType(); Declaration* dec = currentDeclaration(); Q_ASSERT(dec); if (auto type = lastType()) { DUChainWriteLocker lock; dec->setType(type); } closeDeclaration(); } AbstractType::Ptr DeclarationBuilder::typeFromName(const QString& name) { auto type = IntegralType::TypeNone; QString realName = name; // Built-in types if (name == QLatin1String("string")) { type = IntegralType::TypeString; } else if (name == QLatin1String("bool")) { type = IntegralType::TypeBoolean; } else if (name == QLatin1String("int")) { type = IntegralType::TypeInt; } else if (name == QLatin1String("float")) { type = IntegralType::TypeFloat; } else if (name == QLatin1String("double") || name == QLatin1String("real")) { type = IntegralType::TypeDouble; } else if (name == QLatin1String("void")) { type = IntegralType::TypeVoid; } else if (name == QLatin1String("var") || name == QLatin1String("variant")) { type = IntegralType::TypeMixed; } else if (m_session->language() == QmlJS::Dialect::Qml) { // In QML files, some Qt type names need to be renamed to the QML equivalent if (name == QLatin1String("QFont")) { realName = QStringLiteral("Font"); } else if (name == QLatin1String("QColor")) { realName = QStringLiteral("color"); } else if (name == QLatin1String("QDateTime")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QDate")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QTime")) { realName = QStringLiteral("time"); } else if (name == QLatin1String("QRect") || name == QLatin1String("QRectF")) { realName = QStringLiteral("rect"); } else if (name == QLatin1String("QPoint") || name == QLatin1String("QPointF")) { realName = QStringLiteral("point"); } else if (name == QLatin1String("QSize") || name == QLatin1String("QSizeF")) { realName = QStringLiteral("size"); } else if (name == QLatin1String("QUrl")) { realName = QStringLiteral("url"); } else if (name == QLatin1String("QVector3D")) { realName = QStringLiteral("vector3d"); } else if (name.endsWith(QLatin1String("ScriptString"))) { // Q{Declarative,Qml}ScriptString represents a JS snippet auto func = new QmlJS::FunctionType; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); return AbstractType::Ptr(func); } } if (type == IntegralType::TypeNone) { // Not a built-in type, but a class return typeFromClassName(realName); } else { return AbstractType::Ptr(new IntegralType(type)); } } AbstractType::Ptr DeclarationBuilder::typeFromClassName(const QString& name) { DeclarationPointer decl = QmlJS::getDeclaration(QualifiedIdentifier(name), currentContext()); if (!decl) { if (name == QLatin1String("QRegExp")) { decl = QmlJS::NodeJS::instance().moduleMember(QStringLiteral("__builtin_ecmascript"), QStringLiteral("RegExp"), currentContext()->url()); } } if (decl) { return decl->abstractType(); } else { DelayedType::Ptr type(new DelayedType); type->setKind(DelayedType::Unresolved); type->setIdentifier(IndexedTypeIdentifier(name)); return type; } } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const QString& name) { addBaseClass(classDecl, IndexedType(typeFromClassName(name))); } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const IndexedType& type) { BaseClassInstance baseClass; baseClass.access = Declaration::Public; baseClass.virtualInheritance = false; baseClass.baseClass = type; classDecl->addBaseClass(baseClass); } void DeclarationBuilder::registerBaseClasses() { ClassDeclaration* classdecl = currentDeclaration(); DUContext *ctx = currentContext(); if (classdecl) { DUChainWriteLocker lock; for (uint i=0; ibaseClassesSize(); ++i) { const BaseClassInstance &baseClass = classdecl->baseClasses()[i]; StructureType::Ptr baseType = StructureType::Ptr::dynamicCast(baseClass.baseClass.abstractType()); TopDUContext* topctx = topContext(); if (baseType && baseType->declaration(topctx)) { QmlJS::importDeclarationInContext(ctx, DeclarationPointer(baseType->declaration(topctx))); } } } } static bool enumContainsEnumerator(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { Q_ASSERT(a->whichType() == AbstractType::TypeEnumeration); auto aEnum = EnumerationType::Ptr::staticCast(a); Q_ASSERT(b->whichType() == AbstractType::TypeEnumerator); auto bEnumerator = EnumeratorType::Ptr::staticCast(b); return bEnumerator->qualifiedIdentifier().beginsWith(aEnum->qualifiedIdentifier()); } static bool isNumeric(const IntegralType::Ptr& type) { return type->dataType() == IntegralType::TypeInt || type->dataType() == IntegralType::TypeIntegral || type->dataType() == IntegralType::TypeFloat || type->dataType() == IntegralType::TypeDouble; } bool DeclarationBuilder::areTypesEqual(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { if (!a || !b) { return true; } if (a->whichType() == AbstractType::TypeUnsure || b->whichType() == AbstractType::TypeUnsure) { // Don't try to guess something if one of the types is unsure return true; } const auto bIntegral = IntegralType::Ptr::dynamicCast(b); if (bIntegral && (bIntegral->dataType() == IntegralType::TypeString || bIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, a string can be converted to nearly everything else, similarly ignore mixed types return true; } const auto aIntegral = IntegralType::Ptr::dynamicCast(a); if (aIntegral && (aIntegral->dataType() == IntegralType::TypeString || aIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, nearly everything can be to a string, similarly ignore mixed types return true; } if (aIntegral && bIntegral) { if (isNumeric(aIntegral) && isNumeric(bIntegral)) { // Casts between integral types is possible return true; } } if (a->whichType() == AbstractType::TypeEnumeration && b->whichType() == AbstractType::TypeEnumerator) { return enumContainsEnumerator(a, b); } else if (a->whichType() == AbstractType::TypeEnumerator && b->whichType() == AbstractType::TypeEnumeration) { return enumContainsEnumerator(b, a); } { auto aId = dynamic_cast(a.constData()); auto bId = dynamic_cast(b.constData()); if (aId && bId && aId->qualifiedIdentifier() == bId->qualifiedIdentifier()) return true; } { auto aStruct = StructureType::Ptr::dynamicCast(a); auto bStruct = StructureType::Ptr::dynamicCast(b); if (aStruct && bStruct) { auto top = currentContext()->topContext(); auto aDecl = dynamic_cast(aStruct->declaration(top)); auto bDecl = dynamic_cast(bStruct->declaration(top)); if (aDecl && bDecl) { if (aDecl->isPublicBaseClass(bDecl, top) || bDecl->isPublicBaseClass(aDecl, top)) { return true; } } } } return a->equals(b.constData()); } diff --git a/plugins/qmljs/duchain/frameworks/nodejs.cpp b/plugins/qmljs/duchain/frameworks/nodejs.cpp index 1a33a3eca8..6ca5efa0e7 100644 --- a/plugins/qmljs/duchain/frameworks/nodejs.cpp +++ b/plugins/qmljs/duchain/frameworks/nodejs.cpp @@ -1,216 +1,217 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2014 Denis Steckelmacher * * 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 "nodejs.h" #include "../helper.h" #include "../parsesession.h" #include #include #include #include #include #include #include #include using namespace KDevelop; namespace QmlJS { NodeJS::NodeJS() { } NodeJS& NodeJS::instance() { static NodeJS* i = nullptr; if (!i) { i = new NodeJS(); } return *i; } void NodeJS::initialize(DeclarationBuilder* builder) { QMutexLocker lock(&m_mutex); // Create "module", a structure that may contain "exports" if the module // refers to module.exports createObject(QStringLiteral("module"), 1, builder); // Create "exports", that can also contain the exported symbols of the module createObject(QStringLiteral("exports"), 2, builder); } void NodeJS::createObject(const QString& name, int index, DeclarationBuilder* builder) { Identifier identifier(name); StructureType::Ptr type(new StructureType); Declaration* decl = builder->openDeclaration(identifier, RangeInRevision()); type->setDeclaration(decl); decl->setAlwaysForceDirect(true); decl->setKind(Declaration::Type); // Not exactly what the user would expect, but this ensures that QmlJS::getInternalContext does not recurse infinitely decl->setInternalContext(builder->openContext( (QmlJS::AST::Node*)nullptr + index, // Index is used to disambiguate the contexts. "node" is never dereferenced and is only stored in a hash table RangeInRevision(), DUContext::Class, QualifiedIdentifier(identifier) )); builder->closeContext(); builder->openType(type); builder->closeAndAssignType(); } DeclarationPointer NodeJS::moduleExports(const QString& moduleName, const IndexedString& url) { QString urlStr = url.str(); QString fileName = moduleFileName(moduleName, urlStr); DeclarationPointer exports; if (fileName.isEmpty() || urlStr.contains(QLatin1String("__builtin_ecmascript.js")) || urlStr == fileName) { return exports; } ReferencedTopDUContext topContext = ParseSession::contextOfFile(fileName, url, 0); DUChainReadLocker lock; if (topContext) { static const QualifiedIdentifier idModule(QStringLiteral("module")); static const QualifiedIdentifier idExports(QStringLiteral("exports")); // Try "module.exports". If this declaration exists, it contains the // module's exports exports = getDeclaration(idModule, topContext.data()); if (exports && exports->internalContext()) { exports = getDeclaration(idExports, exports->internalContext(), false); } // Try "exports", that always exist, has a structure type, and contains // the exported symbols if (!exports) { exports = getDeclaration(idExports, topContext.data()); } } return exports; } DeclarationPointer NodeJS::moduleMember(const QString& moduleName, const QString& memberName, const IndexedString& url) { DeclarationPointer module = moduleExports(moduleName, url); DeclarationPointer member; if (module) { member = QmlJS::getDeclaration( QualifiedIdentifier(memberName), QmlJS::getInternalContext(module), false ); } return member; } Path::List NodeJS::moduleDirectories(const QString& url) { Path::List paths; // QML/JS ships several modules that exist only in binary form in Node.js - QStringList dirs = QStandardPaths::locateAll( + const QStringList& dirs = QStandardPaths::locateAll( QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/nodejsmodules"), QStandardPaths::LocateDirectory ); paths.reserve(dirs.size()); - for (auto dir : dirs) { + for (auto& dir : dirs) { paths.append(Path(dir)); } // url/../node_modules, then url/../../node_modules, etc Path path(url); path.addPath(QStringLiteral("..")); const int maxPathSize = path.isLocalFile() ? 1 : 2; while (path.segments().size() > maxPathSize) { paths.append(path.cd(QStringLiteral("node_modules"))); path.addPath(QStringLiteral("..")); } return paths; } QString NodeJS::moduleFileName(const QString& moduleName, const QString& url) { QMutexLocker lock(&m_mutex); auto pair = qMakePair(moduleName, url); if (m_cachedModuleFileNames.contains(pair)) { return m_cachedModuleFileNames.value(pair); } QString& fileName = m_cachedModuleFileNames[pair]; // Absolue and relative URLs if (moduleName.startsWith(QLatin1Char('/')) || moduleName.startsWith(QLatin1Char('.'))) { // NOTE: This is not portable to Windows, but the Node.js documentation // only talks about module names that start with /, ./ and ../ . fileName = fileOrDirectoryPath(Path(url).cd(QStringLiteral("..")).cd(moduleName).toLocalFile()); return fileName; } // Try all the paths that might contain modules - for (auto path : moduleDirectories(url)) { + const auto& paths = moduleDirectories(url); + for (auto& path : paths) { fileName = fileOrDirectoryPath(path.cd(moduleName).toLocalFile()); if (!fileName.isEmpty()) { break; } } return fileName; } QString NodeJS::fileOrDirectoryPath(const QString& baseName) { if (QFile::exists(baseName)) { return baseName; } else if (QFile::exists(baseName + QLatin1String(".js"))) { return baseName + QLatin1String(".js"); } else if (QFile::exists(baseName + QLatin1String("/index.js"))) { // TODO: package.json files currently not supported return baseName + QLatin1String("/index.js"); } return QString(); } } diff --git a/plugins/qmljs/duchain/parsesession.cpp b/plugins/qmljs/duchain/parsesession.cpp index 8d6aa4d324..c2ea91634e 100644 --- a/plugins/qmljs/duchain/parsesession.cpp +++ b/plugins/qmljs/duchain/parsesession.cpp @@ -1,286 +1,287 @@ /************************************************************************************* * Copyright (C) 2012 by Milian Wolff * * * * 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 "parsesession.h" #include "debugvisitor.h" #include "cache.h" #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; IndexedString ParseSession::languageString() { static const IndexedString langString("QML/JS"); return langString; } bool isSorted(const QList& locations) { if (locations.size() <= 1) { return true; } for(int i = 1; i < locations.size(); ++i) { if (locations.at(i).begin() <= locations.at(i-1).begin()) { return false; } } return true; } QmlJS::Dialect ParseSession::guessLanguageFromSuffix(const QString& path) { if (path.endsWith(QLatin1String(".js"))) { return QmlJS::Dialect::JavaScript; } else if (path.endsWith(QLatin1String(".json"))) { return QmlJS::Dialect::Json; } else { return QmlJS::Dialect::Qml; } } ParseSession::ParseSession(const IndexedString& url, const QString& contents, int priority) : m_url(url), m_ownPriority(priority), m_allDependenciesSatisfied(true) { const QString path = m_url.str(); m_doc = QmlJS::Document::create(path, guessLanguageFromSuffix(path)); m_doc->setSource(contents); m_doc->parse(); Q_ASSERT(isSorted(m_doc->engine()->comments())); // Parse the module name and the version of url (this is used only when the file // is a QML module, but doesn't break for JavaScript files) m_baseName = QString::fromUtf8(m_url.byteArray()) .section('/', -1, -1) // Base name .section('.', 0, -2); // Without extension } bool ParseSession::isParsedCorrectly() const { return m_doc->isParsedCorrectly(); } QmlJS::AST::Node* ParseSession::ast() const { return m_doc->ast(); } IndexedString ParseSession::url() const { return m_url; } QString ParseSession::moduleName() const { return m_baseName; } void ParseSession::addProblem(QmlJS::AST::Node* node, const QString& message, IProblem::Severity severity) { ProblemPointer p(new Problem); p->setDescription(message); p->setSeverity(severity); p->setSource(IProblem::SemanticAnalysis); p->setFinalLocation(DocumentRange(m_url, editorFindRange(node, node).castToSimpleRange())); m_problems << p; } QList ParseSession::problems() const { QList problems = m_problems; const auto diagnosticMessages = m_doc->diagnosticMessages(); problems.reserve(problems.size() + diagnosticMessages.size()); for (const auto& msg : diagnosticMessages) { ProblemPointer p(new Problem); p->setDescription(msg.message); p->setSeverity(IProblem::Error); p->setSource(IProblem::Parser); p->setFinalLocation(DocumentRange(m_url, locationToRange(msg.loc).castToSimpleRange())); problems << p; } return problems; } QString ParseSession::symbolAt(const QmlJS::AST::SourceLocation& location) const { return m_doc->source().mid(location.offset, location.length); } QmlJS::Dialect ParseSession::language() const { return m_doc->language(); } bool compareSourceLocation(const QmlJS::AST::SourceLocation& l, const QmlJS::AST::SourceLocation& r) { return l.begin() < r.begin(); } QString ParseSession::commentForLocation(const QmlJS::AST::SourceLocation& location) const { // find most recent comment in sorted list of comments const QList< QmlJS::AST::SourceLocation >& comments = m_doc->engine()->comments(); auto it = std::lower_bound( comments.constBegin(), comments.constEnd(), location, compareSourceLocation ); if (it == comments.constBegin()) { return QString(); } // lower bound returns the place of insertion, // we want the comment before that it--; RangeInRevision input = locationToRange(location); RangeInRevision match = locationToRange(*it); if (match.end.line != input.start.line - 1 && match.end.line != input.start.line) { return QString(); } ///TODO: merge consecutive //-style comments? return formatComment(symbolAt(*it)); } RangeInRevision ParseSession::locationToRange(const QmlJS::AST::SourceLocation& location) const { const int linesInLocation = m_doc->source().midRef(location.offset, location.length).count('\n'); return RangeInRevision(location.startLine - 1, location.startColumn - 1, location.startLine - 1 + linesInLocation, location.startColumn - 1 + location.length); } RangeInRevision ParseSession::locationsToRange(const QmlJS::AST::SourceLocation& locationFrom, const QmlJS::AST::SourceLocation& locationTo) const { return RangeInRevision(locationToRange(locationFrom).start, locationToRange(locationTo).end); } RangeInRevision ParseSession::locationsToInnerRange(const QmlJS::AST::SourceLocation& locationFrom, const QmlJS::AST::SourceLocation& locationTo) const { return RangeInRevision(locationToRange(locationFrom).end, locationToRange(locationTo).start); } RangeInRevision ParseSession::editorFindRange(QmlJS::AST::Node* fromNode, QmlJS::AST::Node* toNode) const { return locationsToRange(fromNode->firstSourceLocation(), toNode->lastSourceLocation()); } void ParseSession::setContextOnNode(QmlJS::AST::Node* node, DUContext* context) { m_astToContext.insert(node, DUContextPointer(context)); } DUContext* ParseSession::contextFromNode(QmlJS::AST::Node* node) const { return m_astToContext.value(node, DUContextPointer()).data(); } bool ParseSession::allDependenciesSatisfied() const { return m_allDependenciesSatisfied; } ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName) { ReferencedTopDUContext res = contextOfFile(fileName, m_url, m_ownPriority); if (!res) { // The file was not yet present in the DUChain, store this information. // This will prevent the second parsing pass from running (it would be // useless as the file will be re-parsed when res will become available) m_allDependenciesSatisfied = false; } return res; } ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName, const KDevelop::IndexedString& url, int ownPriority) { if (fileName.isEmpty()) { return ReferencedTopDUContext(); } // Get the top context of this module file DUChainReadLocker lock; IndexedString moduleFileString(fileName); ReferencedTopDUContext moduleContext = DUChain::self()->chainForDocument(moduleFileString); lock.unlock(); QmlJS::Cache::instance().addDependency(url, moduleFileString); if (!moduleContext) { // Queue the file on which we depend with a lower priority than the one of this file scheduleForParsing(moduleFileString, ownPriority - 1); // Register a dependency between this file and the imported one return ReferencedTopDUContext(); } else { return moduleContext; } } void ParseSession::reparseImporters() { - for (const KDevelop::IndexedString& file : QmlJS::Cache::instance().filesThatDependOn(m_url)) { + const auto& files = QmlJS::Cache::instance().filesThatDependOn(m_url); + for (const KDevelop::IndexedString& file : files) { scheduleForParsing(file, m_ownPriority); } } void ParseSession::scheduleForParsing(const IndexedString& url, int priority) { BackgroundParser* bgparser = KDevelop::ICore::self()->languageController()->backgroundParser(); TopDUContext::Features features = (TopDUContext::Features) (TopDUContext::ForceUpdate | TopDUContext::AllDeclarationsContextsAndUses); if (bgparser->isQueued(url)) { bgparser->removeDocument(url); } bgparser->addDocument(url, features, priority, nullptr, ParseJob::FullSequentialProcessing); } void ParseSession::dumpNode(QmlJS::AST::Node* node) const { DebugVisitor v(this); v.startVisiting(node); } diff --git a/plugins/qmljs/navigation/propertypreviewwidget.cpp b/plugins/qmljs/navigation/propertypreviewwidget.cpp index 1b441a0c93..852a4b89d2 100644 --- a/plugins/qmljs/navigation/propertypreviewwidget.cpp +++ b/plugins/qmljs/navigation/propertypreviewwidget.cpp @@ -1,190 +1,191 @@ /************************************************************************************* * Copyright (C) 2013 by Sven Brauch * * * * 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 "propertypreviewwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include // List of supported properties. The string must be the name of the property, // which can contain dots if necessary QHash PropertyPreviewWidget::supportedProperties; QWidget* PropertyPreviewWidget::constructIfPossible(KTextEditor::Document* doc, const KTextEditor::Range& keyRange, const KTextEditor::Range& valueRange, Declaration* decl, const QString& key, const QString& value) { #define PROP(key, filename, type, class) \ supportedProperties.insertMulti(key, SupportedProperty(QUrl(base + filename), type, class)); if ( supportedProperties.isEmpty() ) { QString base = QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/propertywidgets"), QStandardPaths::LocateDirectory ) + '/'; // Positioning PROP("width", "Width.qml", QString(), QString()) PROP("height", "Height.qml", QString(), QString()) PROP("spacing", "Spacing.qml", QString(), QString()) // Margins PROP("margins", "Spacing.qml", QString(), "QQuickAnchors"); // matches anchors.margins and anchors { margins: } PROP("margins", "Spacing.qml", QString(), "QDeclarativeAnchors"); PROP("leftMargin", "Spacing.qml", QString(), "QQuickAnchors"); PROP("leftMargin", "Spacing.qml", QString(), "QDeclarativeAnchors"); PROP("rightMargin", "Spacing.qml", QString(), "QQuickAnchors"); PROP("rightMargin", "Spacing.qml", QString(), "QDeclarativeAnchors"); PROP("topMargin", "Spacing.qml", QString(), "QQuickAnchors"); PROP("topMargin", "Spacing.qml", QString(), "QDeclarativeAnchors"); PROP("bottomMargin", "Spacing.qml", QString(), "QQuickAnchors"); PROP("bottomMargin", "Spacing.qml", QString(), "QDeclarativeAnchors"); // Animations PROP("duration", "Duration.qml", QString(), QString()) // Font QDeclarativeFontValueType, QQuickFontValueType PROP("family", "FontFamily.qml", QString(), "QDeclarativeFontValueType") PROP("family", "FontFamily.qml", QString(), "QQuickFontValueType") PROP("pointSize", "FontSize.qml", QString(), "QDeclarativeFontValueType") PROP("pointSize", "FontSize.qml", QString(), "QQuickFontValueType") // Appearance PROP("opacity", "Opacity.qml", QString(), QString()) // Type-dependent widgets PROP(QString(), "ColorPicker.qml", "color", QString()) } #undef PROP QList properties; properties << supportedProperties.values(key.section(QLatin1Char('.'), -1, -1)); properties << supportedProperties.values(QString()); // Explore each possible supported property and return the first supported widget DUChainReadLocker lock; - for (const SupportedProperty& property : properties) { + for (const SupportedProperty& property : qAsConst(properties)) { if (!decl || !decl->abstractType() || !decl->context() || !decl->context()->owner()) { continue; } if (!decl->abstractType()->toString().contains(property.typeContains)) { continue; } if (!decl->context()->owner()->toString().contains(property.classContains)) { continue; } return new PropertyPreviewWidget(doc, keyRange, valueRange, property, value); } return nullptr; } void PropertyPreviewWidget::updateValue() { QString newValue = view->rootObject()->property("value").toString(); // set the cursor to the edited range, otherwise the view will jump if we call doc->endEditing() //document->activeView()->setCursorPosition(KTextEditor::Cursor(valueRange.start.line, valueRange.start.column)); if (valueRange.end().column() - valueRange.start().column() == newValue.size()) { document->replaceText(valueRange, newValue); } else { // the length of the text changed so don't replace it but remove the old // and insert the new text. KTextEditor::Document::EditingTransaction transaction(document); document->removeText(valueRange); document->insertText(valueRange.start(), newValue); valueRange.setRange( valueRange.start(), KTextEditor::Cursor(valueRange.start().line(), valueRange.start().column() + newValue.size()) ); } } PropertyPreviewWidget::~PropertyPreviewWidget() { } PropertyPreviewWidget::PropertyPreviewWidget(KTextEditor::Document* doc, const KTextEditor::Range& keyRange, const KTextEditor::Range& valueRange, const SupportedProperty& property, const QString& value) : QWidget() , view(new QQuickWidget) , document(doc) , keyRange(keyRange) , valueRange(valueRange) , property(property) { //setup kdeclarative library KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(view->engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(view->engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); //binds things like kconfig and icons #endif // Configure layout auto l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); setLayout(l); // see docstring for ILanguageSupport::specialLanguageObjectNavigationWidget setProperty("DoNotCloseOnCursorMove", true); view->setSource(property.qmlfile); if (!view->rootObject()) { // don't crash because of a syntax error or missing QML file l->addWidget(new QLabel(i18n("Error loading QML file: %1", property.qmlfile.path()))); delete view; return; } // set the initial value read from the document view->rootObject()->setProperty("initialValue", value); // connect to the slot which has to be emitted from QML when the value changes QObject::connect(view->rootObject(), SIGNAL(valueChanged()), this, SLOT(updateValue())); l->addWidget(view); } diff --git a/plugins/qmljs/qmljsparsejob.cpp b/plugins/qmljs/qmljsparsejob.cpp index 8db638e842..b064b568fa 100644 --- a/plugins/qmljs/qmljsparsejob.cpp +++ b/plugins/qmljs/qmljsparsejob.cpp @@ -1,205 +1,207 @@ /************************************************************************************* * Copyright (C) 2012 by Aleix Pol * * Copyright (C) 2012 by Milian Wolff * * * * 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 "qmljsparsejob.h" #include #include #include #include #include #include #include #include #include #include #include #include "duchain/cache.h" #include "duchain/declarationbuilder.h" #include "duchain/parsesession.h" #include "duchain/usebuilder.h" #include "debug.h" #include using namespace KDevelop; /* * This function has been copied from kdev-clang * * Copyright 2013 Olivier de Gaalon and Milian Wolff * Licensed under the GPL v2+ */ ProjectFileItem* findProjectFileItem(const IndexedString& url) { ProjectFileItem* file = nullptr; - for (auto project: ICore::self()->projectController()->projects()) { + const auto& projects = ICore::self()->projectController()->projects(); + for (auto project: projects) { auto files = project->filesForPath(url); if (files.isEmpty()) { continue; } file = files.last(); // A file might be defined in different targets. // Prefer file items defined inside a target with non-empty includes. for (auto f: files) { if (!dynamic_cast(f->parent())) { continue; } file = f; if (!IDefinesAndIncludesManager::manager()->includes(f, IDefinesAndIncludesManager::ProjectSpecific).isEmpty()) { break; } } } return file; } QmlJsParseJob::QmlJsParseJob(const IndexedString& url, ILanguageSupport* languageSupport) : ParseJob(url, languageSupport) { // Tell the cache that this file has custom include directories if (auto file = findProjectFileItem(url)) { QmlJS::Cache::instance().setFileCustomIncludes( url, IDefinesAndIncludesManager::manager()->includes(file, IDefinesAndIncludesManager::Type( IDefinesAndIncludesManager::ProjectSpecific | IDefinesAndIncludesManager::UserDefined)) ); } else { QmlJS::Cache::instance().setFileCustomIncludes( url, IDefinesAndIncludesManager::manager()->includes(url.str(), IDefinesAndIncludesManager::ProjectSpecific) ); } } void QmlJsParseJob::run(ThreadWeaver::JobPointer pointer, ThreadWeaver::Thread* thread) { Q_UNUSED(pointer) Q_UNUSED(thread) UrlParseLock urlLock(document()); if (abortRequested() || !isUpdateRequired(ParseSession::languageString())) { return; } // Don't parse this file if one of its dependencies is not up to date - for (auto dependency : QmlJS::Cache::instance().dependencies(document())) { + const auto& dependencies = QmlJS::Cache::instance().dependencies(document()); + for (auto& dependency : dependencies) { if (!QmlJS::Cache::instance().isUpToDate(dependency)) { QmlJS::Cache::instance().setUpToDate(document(), false); return; } } qCDebug(KDEV_QMLJS) << "parsing" << document().str(); ProblemPointer p = readContents(); if (p) { //TODO: associate problem with topducontext return; } ParseSession session(document(), contents().contents, priority()); if (abortRequested()) { return; } ReferencedTopDUContext context; { DUChainReadLocker lock; context = DUChainUtils::standardContextForUrl(document().toUrl()); } if (context) { translateDUChainToRevision(context); context->setRange(RangeInRevision(0, 0, INT_MAX, INT_MAX)); } if (session.ast()) { QReadLocker parseLock(languageSupport()->parseLock()); if (abortRequested()) { return abortJob(); } DeclarationBuilder builder(&session); context = builder.build(document(), session.ast(), context); if (abortRequested()) { abortJob(); return; } if ( context && minimumFeatures() & TopDUContext::AllDeclarationsContextsAndUses ) { UseBuilder useBuilder(&session); useBuilder.buildUses(session.ast()); } } if (abortRequested()) { return abortJob(); } if (!context) { DUChainWriteLocker lock; ParsingEnvironmentFile *file = new ParsingEnvironmentFile(document()); file->setLanguage(ParseSession::languageString()); context = new TopDUContext(document(), RangeInRevision(0, 0, INT_MAX, INT_MAX), file); DUChain::self()->addDocumentChain(context); } setDuChain(context); // If the file has become up to date, reparse its importers bool dependenciesOk = session.allDependenciesSatisfied(); QmlJS::Cache::instance().setUpToDate(document(), dependenciesOk); if (dependenciesOk) { session.reparseImporters(); } { DUChainWriteLocker lock; context->setProblems(session.problems()); context->setFeatures(minimumFeatures()); ParsingEnvironmentFilePointer file = context->parsingEnvironmentFile(); Q_ASSERT(file); file->setModificationRevision(contents().modification); DUChain::self()->updateContextEnvironment( context->topContext(), file.data() ); } highlightDUChain(); DUChain::self()->emitUpdateReady(document(), duChain()); if (session.isParsedCorrectly()) { qCDebug(KDEV_QMLJS) << "===Success===" << document().str(); } else { qCDebug(KDEV_QMLJS) << "===Failed===" << document().str() << session.problems(); } } diff --git a/plugins/quickopen/tests/test_quickopen.cpp b/plugins/quickopen/tests/test_quickopen.cpp index b3ab32c690..b477d72419 100644 --- a/plugins/quickopen/tests/test_quickopen.cpp +++ b/plugins/quickopen/tests/test_quickopen.cpp @@ -1,392 +1,393 @@ /* * Copyright Milian Wolff * * 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 "test_quickopen.h" #include #include #include #include QTEST_MAIN(TestQuickOpen) using namespace KDevelop; using ItemList = QVector; using StringList = QVector; TestQuickOpen::TestQuickOpen(QObject* parent) : QuickOpenTestBase(Core::Default, parent) { } void TestQuickOpen::testDuchainFilter() { QFETCH(ItemList, items); QFETCH(QString, filter); QFETCH(ItemList, filtered); auto toStringList = [](const ItemList& items) { QStringList result; for (const DUChainItem& item: items) { result << item.m_text; } return result; }; TestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filter); QCOMPARE(toStringList(filterItems.filteredItems()), toStringList(filtered)); } void TestQuickOpen::testDuchainFilter_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); auto i = [](const QString& text) { auto item = DUChainItem(); item.m_text = text; return item; }; auto items = ItemList() << i(QStringLiteral("KTextEditor::Cursor")) << i(QStringLiteral("void KTextEditor::Cursor::explode()")) << i(QStringLiteral("QVector SomeNamespace::SomeClass::func(int)")); QTest::newRow("prefix") << items << "KTE" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_mismatch") << items << "KTEY" << (ItemList()); QTest::newRow("prefix_colon") << items << "KTE:" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_colon_mismatch") << items << "KTE:Y" << (ItemList()); QTest::newRow("prefix_colon_mismatch2") << items << "XKTE:" << (ItemList()); QTest::newRow("prefix_two_colon") << items << "KTE::" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_two_colon_mismatch") << items << "KTE::Y" << (ItemList()); QTest::newRow("prefix_two_colon_mismatch2") << items << "XKTE::" << (ItemList()); QTest::newRow("suffix") << items << "Curs" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("suffix2") << items << "curs" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("mid") << items << "SomeClass" << (ItemList() << items.at(2)); QTest::newRow("mid_abbrev") << items << "SClass" << (ItemList() << items.at(2)); } void TestQuickOpen::testAbbreviations() { QFETCH(StringList, items); QFETCH(QString, filter); QFETCH(StringList, filtered); PathTestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filter.split('/', QString::SkipEmptyParts)); QCOMPARE(filterItems.filteredItems(), filtered); } void TestQuickOpen::testAbbreviations_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); const StringList items = { QStringLiteral("/foo/bar/caz/a.h"), QStringLiteral("/KateThing/CMakeLists.txt"), QStringLiteral("/FooBar/FooBar/Footestfoo.h") }; QTest::newRow("path_segments") << items << "fbc" << StringList(); QTest::newRow("path_segment_abbrev") << items << "cmli" << StringList({ items.at(1) }); QTest::newRow("path_segment_old") << items << "kate/cmake" << StringList({ items.at(1) }); QTest::newRow("path_segment_multi_mixed") << items << "ftfoo.h" << StringList({ items.at(2) }); } void TestQuickOpen::testSorting() { QFETCH(StringList, items); QFETCH(QString, filter); QFETCH(StringList, filtered); const auto filterList = filter.split('/', QString::SkipEmptyParts); PathTestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filterList); QEXPECT_FAIL("bar7", "empty parts are skipped", Abort); if (filterItems.filteredItems() != filtered) qWarning() << filterItems.filteredItems() << filtered; QCOMPARE(filterItems.filteredItems(), filtered); // check whether sorting is stable filterItems.setFilter(filterList); QCOMPARE(filterItems.filteredItems(), filtered); } void TestQuickOpen::testSorting_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); const StringList items({ QStringLiteral("/foo/a.h"), QStringLiteral("/foo/ab.h"), QStringLiteral("/foo/bc.h"), QStringLiteral("/bar/a.h")}); { QTest::newRow("no-filter") << items << QString() << items; } { const StringList filtered = { QStringLiteral("/bar/a.h") }; QTest::newRow("bar1") << items << QStringLiteral("bar") << filtered; QTest::newRow("bar2") << items << QStringLiteral("/bar") << filtered; QTest::newRow("bar3") << items << QStringLiteral("/bar/") << filtered; QTest::newRow("bar4") << items << QStringLiteral("bar/") << filtered; QTest::newRow("bar5") << items << QStringLiteral("ar/") << filtered; QTest::newRow("bar6") << items << QStringLiteral("r/") << filtered; QTest::newRow("bar7") << items << QStringLiteral("b/") << filtered; QTest::newRow("bar8") << items << QStringLiteral("b/a") << filtered; QTest::newRow("bar9") << items << QStringLiteral("b/a.h") << filtered; QTest::newRow("bar10") << items << QStringLiteral("b/a.") << filtered; } { const StringList filtered = { QStringLiteral("/foo/a.h"), QStringLiteral("/foo/ab.h") }; QTest::newRow("foo_a1") << items << QStringLiteral("foo/a") << filtered; QTest::newRow("foo_a2") << items << QStringLiteral("/f/a") << filtered; } { // now matches ab.h too because of abbreviation matching, but should be sorted last const StringList filtered = { QStringLiteral("/foo/a.h"), QStringLiteral("/bar/a.h"), QStringLiteral("/foo/ab.h") }; QTest::newRow("a_h") << items << QStringLiteral("a.h") << filtered; } { const StringList base = { QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b") }; const StringList sorted = { QStringLiteral("/foo/test_b"), QStringLiteral("/foo/test_b_1") }; QTest::newRow("prefer_exact") << base << QStringLiteral("test_b") << sorted; } { // from commit: 769491f06a4560a4798592ff060675ffb0d990a6 const QString file = QStringLiteral("/myProject/someStrangePath/anItem.cpp"); const StringList base = { QStringLiteral("/foo/a"), file }; const StringList filtered = { file }; QTest::newRow("strange") << base << QStringLiteral("strange/item") << filtered; } { const StringList base = { QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b"), QStringLiteral("/foo/test/a") }; const StringList sorted = { QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b"), QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test/a") }; QTest::newRow("prefer_start1") << base << QStringLiteral("test") << sorted; QTest::newRow("prefer_start2") << base << QStringLiteral("foo/test") << sorted; } { const StringList base = { QStringLiteral("/muh/kuh/asdf/foo"), QStringLiteral("/muh/kuh/foo/asdf") }; const StringList reverse = { QStringLiteral("/muh/kuh/foo/asdf"), QStringLiteral("/muh/kuh/asdf/foo") }; QTest::newRow("prefer_start3") << base << QStringLiteral("f") << base; QTest::newRow("prefer_start4") << base << QStringLiteral("/fo") << base; QTest::newRow("prefer_start5") << base << QStringLiteral("/foo") << base; QTest::newRow("prefer_start6") << base << QStringLiteral("a") << reverse; QTest::newRow("prefer_start7") << base << QStringLiteral("/a") << reverse; QTest::newRow("prefer_start8") << base << QStringLiteral("uh/as") << reverse; QTest::newRow("prefer_start9") << base << QStringLiteral("asdf") << reverse; } { QTest::newRow("duplicate") << StringList({ QStringLiteral("/muh/kuh/asdf/foo") }) << QStringLiteral("kuh/kuh") << StringList(); } { const StringList fuzzyItems = { QStringLiteral("/foo/bar.h"), QStringLiteral("/foo/fooXbar.h"), QStringLiteral("/foo/fXoXoXbXaXr.h"), QStringLiteral("/bar/FOOxBAR.h") }; QTest::newRow("fuzzy1") << fuzzyItems << QStringLiteral("br") << fuzzyItems; QTest::newRow("fuzzy2") << fuzzyItems << QStringLiteral("foo/br") << StringList({ QStringLiteral("/foo/bar.h"), QStringLiteral("/foo/fooXbar.h"), QStringLiteral("/foo/fXoXoXbXaXr.h") }); QTest::newRow("fuzzy3") << fuzzyItems << QStringLiteral("b/br") << StringList({ QStringLiteral("/bar/FOOxBAR.h") }); QTest::newRow("fuzzy4") << fuzzyItems << QStringLiteral("br/br") << StringList(); QTest::newRow("fuzzy5") << fuzzyItems << QStringLiteral("foo/bar") << StringList({ QStringLiteral("/foo/bar.h"), QStringLiteral("/foo/fooXbar.h"), QStringLiteral("/foo/fXoXoXbXaXr.h") }); QTest::newRow("fuzzy6") << fuzzyItems << QStringLiteral("foobar") << StringList({ QStringLiteral("/foo/fooXbar.h"), QStringLiteral("/foo/fXoXoXbXaXr.h"), QStringLiteral("/bar/FOOxBAR.h") }); } { const StringList a = { QStringLiteral("/home/user/src/code/user/something"), QStringLiteral("/home/user/src/code/home/else"), }; const StringList b = { QStringLiteral("/home/user/src/code/home/else"), QStringLiteral("/home/user/src/code/user/something"), }; QTest::newRow("prefer_multimatch_a_home") << a << QStringLiteral("home") << b; QTest::newRow("prefer_multimatch_b_home") << b << QStringLiteral("home") << b; QTest::newRow("prefer_multimatch_a_user") << a << QStringLiteral("user") << a; QTest::newRow("prefer_multimatch_b_user") << b << QStringLiteral("user") << a; } { const StringList a = { QStringLiteral("/home/user/project/A/file"), QStringLiteral("/home/user/project/B/project/A/file"), QStringLiteral("/home/user/project/user/C/D/E"), }; const StringList b = { QStringLiteral("/home/user/project/B/project/A/file"), QStringLiteral("/home/user/project/A/file"), }; const StringList c = { QStringLiteral("/home/user/project/user/C/D/E"), QStringLiteral("/home/user/project/A/file"), QStringLiteral("/home/user/project/B/project/A/file"), }; QTest::newRow("prefer_multimatch_a_project/file") << a << QStringLiteral("project/file") << b; QTest::newRow("prefer_multimatch_b_project/file") << b << QStringLiteral("project/file") << b; QTest::newRow("prefer_multimatch_a_project/le") << a << QStringLiteral("project/le") << b; QTest::newRow("prefer_multimatch_b_project/le") << b << QStringLiteral("project/le") << b; QTest::newRow("prefer_multimatch_a_project/a/file") << a << QStringLiteral("project/a/file") << b; QTest::newRow("prefer_multimatch_b_project/a/file") << b << QStringLiteral("project/a/file") << b; QTest::newRow("prefer_multimatch_a_project_user") << a << QStringLiteral("user") << c; QTest::newRow("prefer_multimatch_c_project_user") << c << QStringLiteral("user") << c; } } void TestQuickOpen::testStableSort() { const StringList items = { QStringLiteral("a/c/CMakeLists.txt"), QStringLiteral("a/d/CMakeLists.txt"), QStringLiteral("b/e/CMakeLists.txt"), QStringLiteral("b/f/CMakeLists.txt") }; PathTestFilter filterItems; filterItems.setItems(items); QStringList filter = {QString()}; - for (auto c : QStringLiteral("CMakeLists.txt")) { + const auto cmakeListsString = QStringLiteral("CMakeLists.txt"); + for (auto c : cmakeListsString) { filter[0].append(c); filterItems.setFilter(filter); QCOMPARE(filterItems.filteredItems(), items); } } void TestQuickOpen::testProjectFileFilter() { QTemporaryDir dir; TestProject* project = new TestProject(Path(dir.path())); ProjectFolderItem* foo = createChild(project->projectItem(), QStringLiteral("foo")); createChild(foo, QStringLiteral("bar")); createChild(foo, QStringLiteral("asdf")); createChild(foo, QStringLiteral("space bar")); ProjectFolderItem* asdf = createChild(project->projectItem(), QStringLiteral("asdf")); createChild(asdf, QStringLiteral("bar")); QTemporaryFile tmpFile; tmpFile.setFileName(dir.path() + "/aaaa"); QVERIFY(tmpFile.open()); ProjectFileItem* aaaa = new ProjectFileItem(QStringLiteral("aaaa"), project->projectItem()); QCOMPARE(project->fileSet().size(), 5); ProjectFileDataProvider provider; QCOMPARE(provider.itemCount(), 0u); projectController->addProject(project); const QStringList original = QStringList() << QStringLiteral("aaaa") << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar"); // lazy load QCOMPARE(provider.itemCount(), 0u); provider.reset(); QCOMPARE(items(provider), original); QCOMPARE(provider.itemPath(provider.items().first()), aaaa->path()); QCOMPARE(provider.data(0)->text(), QStringLiteral("aaaa")); // don't show opened file QVERIFY(core->documentController()->openDocument(QUrl::fromLocalFile(tmpFile.fileName()))); // lazy load again QCOMPARE(items(provider), original); provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); // prefer files starting with filter provider.setFilterText(QStringLiteral("as")); qDebug() << items(provider); QCOMPARE(items(provider), QStringList() << QStringLiteral("foo/asdf") << QStringLiteral("asdf/bar")); // clear filter provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); // update on document close, lazy load again core->documentController()->closeAllDocuments(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); provider.reset(); QCOMPARE(items(provider), original); ProjectFileItem* blub = createChild(project->projectItem(), QStringLiteral("blub")); // lazy load QCOMPARE(provider.itemCount(), 5u); provider.reset(); QCOMPARE(provider.itemCount(), 6u); // ensure we don't add stuff multiple times QMetaObject::invokeMethod(&provider, "fileAddedToSet", Q_ARG(KDevelop::ProjectFileItem*, blub)); QCOMPARE(provider.itemCount(), 6u); provider.reset(); QCOMPARE(provider.itemCount(), 6u); // lazy load in this implementation delete blub; QCOMPARE(provider.itemCount(), 6u); provider.reset(); QCOMPARE(provider.itemCount(), 5u); QCOMPARE(items(provider), original); // allow filtering by path to project provider.setFilterText(dir.path()); QCOMPARE(items(provider), original); Path buildFolderItem(project->path().parent(), QStringLiteral(".build/generated.h")); new ProjectFileItem(project, buildFolderItem, project->projectItem()); // lazy load QCOMPARE(items(provider), original); provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("aaaa") << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar") << QStringLiteral("../.build/generated.h")); projectController->closeProject(project); provider.reset(); QVERIFY(!provider.itemCount()); }