diff --git a/languages/clang/duchain/missingincludepathproblem.h b/languages/clang/duchain/missingincludepathproblem.h --- a/languages/clang/duchain/missingincludepathproblem.h +++ b/languages/clang/duchain/missingincludepathproblem.h @@ -34,6 +34,12 @@ MissingIncludePathProblem(CXDiagnostic diagnostic, CXTranslationUnit unit); virtual KDevelop::IAssistant::Ptr solutionAssistant() const override; + + /** + * @return the full name of the included file as it appears in the source, + * or an empty string if unsuccessful + */ + QString includeName() const; }; #endif // MISSINGINCLUDEPATHPROBLEM_H diff --git a/languages/clang/duchain/missingincludepathproblem.cpp b/languages/clang/duchain/missingincludepathproblem.cpp --- a/languages/clang/duchain/missingincludepathproblem.cpp +++ b/languages/clang/duchain/missingincludepathproblem.cpp @@ -28,36 +28,42 @@ #include -namespace -{ -void openConfigurationPage(const QString& path) -{ - KDevelop::IDefinesAndIncludesManager::manager()->openConfigurationDialog(path); -} -} - class AddCustomIncludePathAction : public KDevelop::IAssistantAction { Q_OBJECT public: - AddCustomIncludePathAction(const KDevelop::IndexedString& path) + AddCustomIncludePathAction(const KDevelop::IndexedString& path, + const QString& includeName) : m_path(path) + , m_includeName(includeName) {} QString description() const override { - return i18n("Add Custom Include Path"); + if (!m_includeName.isEmpty()) + return i18n("Add Custom Include Path for %1", m_includeName); + else + return i18n("Add Custom Include Path"); } void execute() override { - openConfigurationPage(m_path.str()); + KDevelop::IDefinesAndIncludesManager* dim = + KDevelop::IDefinesAndIncludesManager::manager(); + + if (!m_includeName.isEmpty()) { + dim->openAddIncludePathDialog(m_path, m_includeName); + } else { + dim->openConfigurationDialog(m_path.str()); + } + emit executed(this); } private: KDevelop::IndexedString m_path; + QString m_includeName; }; class OpenProjectForFileAssistant : public KDevelop::IAssistantAction @@ -85,9 +91,12 @@ class MissingIncludePathAssistant : public ClangFixitAssistant { public: - MissingIncludePathAssistant(const QString& title, const KDevelop::IndexedString& path) + MissingIncludePathAssistant(const QString& title, + const KDevelop::IndexedString& path, + const QString& includeName) : ClangFixitAssistant(title, {}) , m_path(path) + , m_includeName(includeName) {} void createActions() override @@ -97,11 +106,14 @@ if (!project) { addAction(KDevelop::IAssistantAction::Ptr(new OpenProjectForFileAssistant(m_path))); } - addAction(KDevelop::IAssistantAction::Ptr(new AddCustomIncludePathAction(m_path))); + + addAction(KDevelop::IAssistantAction::Ptr( + new AddCustomIncludePathAction(m_path, project ? m_includeName : QString()))); } private: KDevelop::IndexedString m_path; + QString m_includeName; }; MissingIncludePathProblem::MissingIncludePathProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) @@ -111,7 +123,25 @@ KDevelop::IAssistant::Ptr MissingIncludePathProblem::solutionAssistant() const { return KDevelop::IAssistant::Ptr(new MissingIncludePathAssistant( - description(), compilationUnit())); + description(), compilationUnit(), includeName())); +} + +QString MissingIncludePathProblem::includeName() const +{ + // Ideally, we would retrieve this from the ClangDiagnostic, but there + // seems to be no API. So parse it out of the description instead. + const QString d = description(); + const int firstTick = d.indexOf(QLatin1Char('\'')); + const int lastTick = d.lastIndexOf(QLatin1Char('\'')); + + if (firstTick == lastTick) + return {}; // no tick or only one tick + + const QString name = d.mid(firstTick + 1, lastTick - firstTick - 1); + if (name.indexOf(QLatin1Char('\'')) >= 0) + return {}; + + return name; } #include "missingincludepathproblem.moc" diff --git a/languages/plugins/custom-definesandincludes/CMakeLists.txt b/languages/plugins/custom-definesandincludes/CMakeLists.txt --- a/languages/plugins/custom-definesandincludes/CMakeLists.txt +++ b/languages/plugins/custom-definesandincludes/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(noprojectincludesanddefines) set( kdevdefinesandincludesmanager_SRCS + addmissingincludepath.cpp definesandincludesmanager.cpp debugarea.cpp kcm_widget/projectpathsmodel.cpp @@ -19,6 +20,7 @@ ) ki18n_wrap_ui(kdevdefinesandincludesmanager_SRCS + addmissingincludepath.ui kcm_widget/batchedit.ui kcm_widget/includeswidget.ui kcm_widget/defineswidget.ui diff --git a/languages/plugins/custom-definesandincludes/addmissingincludepath.h b/languages/plugins/custom-definesandincludes/addmissingincludepath.h new file mode 100644 --- /dev/null +++ b/languages/plugins/custom-definesandincludes/addmissingincludepath.h @@ -0,0 +1,91 @@ +/* + * Copyright 2016 Nicolai Hähnle + * + * 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 . + * + */ + +#ifndef ADDMISSINGINCLUDEPATH_H +#define ADDMISSINGINCLUDEPATH_H + +#include + +namespace Ui +{ + class AddMissingIncludePathDialog; +} + +namespace KDevelop +{ + class Path; + class ProjectFolderItem; +} + +/** + * Dialog that allows the user to choose an include path to be added to resolve + * a missing include, as well as the project folder to which the custom include + * path should be added. + */ +class AddMissingIncludePath : public QDialog +{ + Q_OBJECT +public: + AddMissingIncludePath(QWidget* parent = nullptr); + + /** + * Set the include name that the user wants to resolve, e.g. 'foo/bar.h'. + */ + void setIncludeName(const QString& includeName); + + /** + * Add a candidate suggestion for an include path that can be used to + * resolve the include name, e.g. if the include name is 'foo/bar.h' and + * there is a file '.../project/quux/foo/bar.h', then '.../project/quux' + * would be a candidate. + */ + void addIncludePathCandidate(const KDevelop::Path& path); + + /** + * Populate the list of possible project folders to which the custom + * include path could be added, given \p file is the file that references + * the missing include. + */ + void populateFolders(const KDevelop::Path& file); + + /** + * \return the include path that was chosen by the user. + */ + KDevelop::Path includePath(); + + /** + * \return the project folder to which the custom include path should + * be added. + */ + KDevelop::Path folder(); + +private Q_SLOTS: + void choose(); + +private: + class PathsModel; + + Ui::AddMissingIncludePathDialog* m_ui; + PathsModel* m_candidates; + PathsModel* m_folders; +}; + +#endif diff --git a/languages/plugins/custom-definesandincludes/addmissingincludepath.cpp b/languages/plugins/custom-definesandincludes/addmissingincludepath.cpp new file mode 100644 --- /dev/null +++ b/languages/plugins/custom-definesandincludes/addmissingincludepath.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2016 Nicolai Hähnle + * + * 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 "addmissingincludepath.h" + +#include "ui_addmissingincludepath.h" + +#include +#include +#include + +#include +#include + +using namespace KDevelop; + +class AddMissingIncludePath::PathsModel : public QAbstractListModel +{ + QList m_paths; +public: + PathsModel(QObject* parent = nullptr) + : QAbstractListModel(parent) + { + } + + void addPath(const Path& path) + { + beginInsertRows(QModelIndex(), m_paths.size(), m_paths.size()); + m_paths.push_back(path); + endInsertRows(); + } + + Path path(int row) + { + if (row < 0 || row >= m_paths.size()) + return {}; + + return m_paths[row]; + } + + // QAbstractListModel implementation + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid() || role != Qt::DisplayRole) + return {}; + + if (index.row() >= 0 && index.row() < m_paths.size()) { + IProjectController* projectController = + ICore::self()->projectController(); + + return projectController->prettyFileName(m_paths[index.row()].toUrl(), + IProjectController::FormatPlain); + } + + return {}; + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + return parent.isValid() ? 0 : m_paths.size(); + } +}; + +AddMissingIncludePath::AddMissingIncludePath(QWidget* parent) + : QDialog(parent) + , m_ui(new Ui::AddMissingIncludePathDialog) + , m_candidates(new PathsModel(this)) + , m_folders(new PathsModel(this)) +{ + m_ui->setupUi(this); + + m_ui->includePath->setModel(m_candidates); + m_ui->folder->setModel(m_folders); + + m_ui->chooseCandidateButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); + connect(m_ui->chooseCandidateButton, &QPushButton::clicked, this, &AddMissingIncludePath::choose); +} + +void AddMissingIncludePath::setIncludeName(const QString& includeName) +{ + m_ui->includeName->setText(includeName); +} + +void AddMissingIncludePath::addIncludePathCandidate(const KDevelop::Path& path) +{ + m_candidates->addPath(path); +} + +void AddMissingIncludePath::populateFolders(const KDevelop::Path& file) +{ + IProjectController* projectController = ICore::self()->projectController(); + Path path = file.parent(); + + while (projectController->findProjectForUrl(path.toUrl())) { + m_folders->addPath(path); + path = path.parent(); + } +} + +KDevelop::Path AddMissingIncludePath::includePath() +{ + return m_candidates->path(m_ui->includePath->currentIndex()); +} + +KDevelop::Path AddMissingIncludePath::folder() +{ + return m_folders->path(m_ui->folder->currentIndex()); +} + +void AddMissingIncludePath::choose() +{ + const Path includeName(QUrl::fromLocalFile(m_ui->includeName->text())); + const QString candidate = + QFileDialog::getOpenFileName(this, tr("Choose include file"), QString(), + includeName.lastPathSegment()); + if (candidate.isEmpty()) + return; + + const Path candidatePath(QUrl::fromLocalFile(candidate)); + const auto candidateSegments = candidatePath.segments(); + const auto includeSegments = includeName.segments(); + bool matches = false; + + if (candidateSegments.size() > includeSegments.size()) { + matches = true; + + for (int i = 0; i < includeSegments.size(); ++i) { + int j = candidateSegments.size() - includeSegments.size() + i; + if (includeSegments[i] != candidateSegments[j]) { + matches = false; + break; + } + } + } + + if (matches) { + addIncludePathCandidate(candidatePath.ancestor(includeSegments.size())); + m_ui->includePath->setCurrentIndex(m_candidates->rowCount() - 1); + } else { + QMessageBox::information(nullptr, tr("Path does not match"), + tr("The chosen path does not match the desired include file.")); + } +} diff --git a/languages/plugins/custom-definesandincludes/addmissingincludepath.ui b/languages/plugins/custom-definesandincludes/addmissingincludepath.ui new file mode 100644 --- /dev/null +++ b/languages/plugins/custom-definesandincludes/addmissingincludepath.ui @@ -0,0 +1,133 @@ + + + AddMissingIncludePathDialog + + + + 0 + 0 + 485 + 146 + + + + Add Missing Include Path + + + + + + + + Missing include: + + + + + + + <html><head/><body><p>The filename or path that was not found, as written in the source.</p></body></html> + + + true + + + + + + + Provided by: + + + + + + + Add include path to: + + + + + + + <html><head/><body><p>Choose the project directory in which the added include path should be applied. It will automatically be applied in all subdirectories as well.</p></body></html> + + + + + + + + + <html><head/><body><p>Choose the file that should be included.</p><p>If the missing include is a path such as <span style=" font-style:italic;">'foo/bar.h'</span>, choose a file <span style=" font-style:italic;">bar.h</span> in a directory named <span style=" font-style:italic;">foo</span>. The correct parent directory will automatically be chosen as include path.</p></body></html> + + + + + + + + 32 + 32 + + + + <html><head/><body><p>Select the file that should provide the missing include.</p><p>For example, if the missing include is <span style=" font-style:italic;">'foo/bar.h'</span>, choose a file <span style=" font-style:italic;">bar.h</span> in a directory called <span style=" font-style:italic;">foo</span>.</p></body></html> + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + AddMissingIncludePathDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AddMissingIncludePathDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/languages/plugins/custom-definesandincludes/definesandincludesmanager.h b/languages/plugins/custom-definesandincludes/definesandincludesmanager.h --- a/languages/plugins/custom-definesandincludes/definesandincludesmanager.h +++ b/languages/plugins/custom-definesandincludes/definesandincludesmanager.h @@ -77,6 +77,16 @@ KDevelop::ConfigPage* configPage(int number, QWidget *parent) override; int configPages() const override; + void openAddIncludePathDialog(const KDevelop::IndexedString& pathToFile, + const QString& includeName) override; + + /** + * Add the custom include path \p includePath to the indicated project + * \p folder. + */ + void addIncludePath(const KDevelop::Path& folder, + const KDevelop::Path& includePath); + private: QVector m_providers; QVector m_backgroundProviders; diff --git a/languages/plugins/custom-definesandincludes/definesandincludesmanager.cpp b/languages/plugins/custom-definesandincludes/definesandincludesmanager.cpp --- a/languages/plugins/custom-definesandincludes/definesandincludesmanager.cpp +++ b/languages/plugins/custom-definesandincludes/definesandincludesmanager.cpp @@ -25,12 +25,15 @@ #include "compilerprovider/compilerprovider.h" #include "compilerprovider/widget/compilerswidget.h" #include "noprojectincludesanddefines/noprojectincludepathsmanager.h" +#include "addmissingincludepath.h" #include #include #include #include #include +#include +#include #include #include @@ -272,6 +275,98 @@ } } +void DefinesAndIncludesManager::openAddIncludePathDialog(const KDevelop::IndexedString& pathToFile, + const QString& includeName) +{ + AddMissingIncludePath dlg; + + // Determine the most-nested folder to which the new include path could + // be added. + QUrl url = pathToFile.toUrl(); + KDevelop::IProjectController* projectController = + KDevelop::ICore::self()->projectController(); + if (!projectController->findProjectForUrl(url)) { + m_noProjectIPM->openConfigurationDialog(pathToFile.str()); + return; + } + + dlg.populateFolders(KDevelop::Path(url)); + dlg.setIncludeName(includeName); + + // Find candidate include paths among all opened projects. + const Path includePath(QUrl::fromLocalFile(includeName)); + for (IProject *project : projectController->projects()) { + for (ProjectFileItem *item : KDevelop::allFiles(project->projectItem())) { + const Path path = item->path(); + + if (path.segments().size() < includePath.segments().size()) + continue; + + const unsigned pathBaseSegments = + path.segments().size() - includePath.segments().size(); + unsigned i; + for (i = includePath.segments().size(); i > 0; --i) { + if (includePath.segments()[i - 1] != + path.segments()[pathBaseSegments + i - 1]) + break; + } + if (i > 0) + continue; + + Path candidate = path.ancestor(includePath.segments().size()); + dlg.addIncludePathCandidate(candidate); + } + } + + // Now show the dialog and process results + if (dlg.exec() == QDialog::Accepted) { + Path chosenIncludePath = dlg.includePath(); + Path folderPath = dlg.folder(); + + if (!chosenIncludePath.isEmpty() && !folderPath.isEmpty()) + addIncludePath(folderPath, chosenIncludePath); + } +} + +void DefinesAndIncludesManager::addIncludePath(const KDevelop::Path& folder, + const KDevelop::Path& includePath) +{ + KDevelop::IProjectController* projectController = + KDevelop::ICore::self()->projectController(); + KDevelop::IProject* project = projectController->findProjectForUrl(folder.toUrl()); + + if (!project) + return; + + QString relativePath = project->path().relativePath(folder); + if (relativePath.isEmpty()) + relativePath = QStringLiteral("."); + + auto settings = SettingsManager::globalInstance(); + auto cfg = project->projectConfiguration(); + QList paths = settings->readPaths(cfg.data()); + ConfigEntry* path = nullptr; + + for (ConfigEntry& p : paths) { + if (p.path == relativePath) { + path = &p; + break; + } + } + if (!path) { + paths.append(ConfigEntry(relativePath)); + path = &paths.back(); + } + + path->includes.append(includePath.path()); + + settings->writePaths(cfg.data(), paths); + + if (settings->needToReparseCurrentProject(cfg.data())) + projectController->reparseProject(project, true); +} + + Path::List DefinesAndIncludesManager::includesInBackground(const QString& path) const { Path::List includes; diff --git a/languages/plugins/custom-definesandincludes/idefinesandincludesmanager.h b/languages/plugins/custom-definesandincludes/idefinesandincludesmanager.h --- a/languages/plugins/custom-definesandincludes/idefinesandincludesmanager.h +++ b/languages/plugins/custom-definesandincludes/idefinesandincludesmanager.h @@ -36,6 +36,7 @@ namespace KDevelop { +class IndexedString; class ProjectBaseItem; class IProject; @@ -196,6 +197,17 @@ /// Opens a configuration dialog for @p pathToFile to modify include directories/files and defined macros. virtual void openConfigurationDialog(const QString& pathToFile) = 0; + + /** + * Opens a dialog in which the user is prompted to choose a file matching + * @p includeName, so that the corresponding path can automatically be + * added as a custom include path that will apply to @p pathToFile. + * + * @param pathToFile the file that contains an include statement + * @param includeName the file name or path fragment in the include statement + */ + virtual void openAddIncludePathDialog(const KDevelop::IndexedString& pathToFile, + const QString& includeName) = 0; }; inline IDefinesAndIncludesManager* IDefinesAndIncludesManager::manager()