diff --git a/addons/project/kateproject.cpp b/addons/project/kateproject.cpp index c4472351b..d1ace8481 100644 --- a/addons/project/kateproject.cpp +++ b/addons/project/kateproject.cpp @@ -1,441 +1,451 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * 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 "kateproject.h" #include "kateprojectworker.h" +#include "kateprojectplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include -KateProject::KateProject(ThreadWeaver::Queue *weaver) +KateProject::KateProject(ThreadWeaver::Queue *weaver, KateProjectPlugin *plugin) : QObject() , m_fileLastModified() , m_notesDocument(nullptr) , m_untrackedDocumentsRoot(nullptr) , m_weaver(weaver) + , m_plugin(plugin) { } KateProject::~KateProject() { saveNotesDocument(); } bool KateProject::loadFromFile(const QString &fileName) { /** * bail out if already fileName set! */ if (!m_fileName.isEmpty()) { return false; } /** * set new filename and base directory */ m_fileName = fileName; m_baseDir = QFileInfo(m_fileName).canonicalPath(); /** * trigger reload */ return reload(); } bool KateProject::reload(bool force) { QVariantMap map = readProjectFile(); if (map.isEmpty()) { m_fileLastModified = QDateTime(); } else { m_fileLastModified = QFileInfo(m_fileName).lastModified(); m_globalProject = map; } return load(m_globalProject, force); } QVariantMap KateProject::readProjectFile() const { QFile file(m_fileName); if (!file.open(QFile::ReadOnly)) { return QVariantMap(); } /** * parse the whole file, bail out again on error! */ const QByteArray jsonData = file.readAll(); QJsonParseError parseError; QJsonDocument project(QJsonDocument::fromJson(jsonData, &parseError)); if (parseError.error != QJsonParseError::NoError) { return QVariantMap(); } /** * convenience; align with auto-generated projects * generate 'name' and 'files' if not specified explicitly, * so those parts need not be given if only wishes to specify additional * project configuration (e.g. build, ctags) */ if (project.isObject()) { auto dir = QFileInfo(m_fileName).dir(); auto object = project.object(); auto name = object[QStringLiteral("name")]; if (name.isUndefined() || name.isNull()) { name = dir.dirName(); } auto files = object[QStringLiteral("files")]; if (files.isUndefined() || files.isNull()) { // support all we can, could try to detect, // but it will be sorted out upon loading anyway QJsonArray afiles; for (const auto &t : {QStringLiteral("git"), QStringLiteral("hg"), QStringLiteral("svn"), QStringLiteral("darcs")}) { afiles.push_back(QJsonObject {{t, true}}); } files = afiles; } project.setObject(object); } return project.toVariant().toMap(); } bool KateProject::loadFromData(const QVariantMap &globalProject, const QString &directory) { m_baseDir = directory; m_fileName = QDir(directory).filePath(QStringLiteral(".kateproject")); m_globalProject = globalProject; return load(globalProject); } bool KateProject::load(const QVariantMap &globalProject, bool force) { /** * no name, bad => bail out */ if (globalProject[QStringLiteral("name")].toString().isEmpty()) { return false; } /** * support out-of-source project files */ if (!globalProject[QStringLiteral("directory")].toString().isEmpty()) { m_baseDir = QFileInfo(globalProject[QStringLiteral("directory")].toString()).canonicalFilePath(); } /** * anything changed? * else be done without forced reload! */ if (!force && (m_projectMap == globalProject)) { return true; } /** * setup global attributes in this object */ m_projectMap = globalProject; // emit that we changed stuff emit projectMapChanged(); // trigger loading of project in background thread - auto w = new KateProjectWorker(m_baseDir, m_projectMap); + QString indexDir; + if (m_plugin->getIndexEnabled()) { + indexDir = m_plugin->getIndexDirectory().toLocalFile(); + // if empty, use regular tempdir + if (indexDir.isEmpty()) { + indexDir = QDir::tempPath(); + } + } + auto w = new KateProjectWorker(m_baseDir, indexDir, m_projectMap, force); connect(w, &KateProjectWorker::loadDone, this, &KateProject::loadProjectDone); connect(w, &KateProjectWorker::loadIndexDone, this, &KateProject::loadIndexDone); m_weaver->stream() << w; // we are done here return true; } void KateProject::loadProjectDone(const KateProjectSharedQStandardItem &topLevel, KateProjectSharedQMapStringItem file2Item) { m_model.clear(); m_model.invisibleRootItem()->appendColumn(topLevel->takeColumn(0)); m_file2Item = std::move(file2Item); /** * readd the documents that are open atm */ m_untrackedDocumentsRoot = nullptr; for (auto i = m_documents.constBegin(); i != m_documents.constEnd(); i++) { registerDocument(i.key()); } emit modelChanged(); } void KateProject::loadIndexDone(KateProjectSharedProjectIndex projectIndex) { /** * move to our project */ m_projectIndex = std::move(projectIndex); /** * notify external world that data is available */ emit indexChanged(); } QString KateProject::projectLocalFileName(const QString &suffix) const { /** * nothing on empty file names for project * should not happen */ if (m_baseDir.isEmpty() || suffix.isEmpty()) { return QString(); } /** * compute full file name */ return m_baseDir + QStringLiteral(".kateproject.") + suffix; } QTextDocument *KateProject::notesDocument() { /** * already there? */ if (m_notesDocument) { return m_notesDocument; } /** * else create it */ m_notesDocument = new QTextDocument(this); m_notesDocument->setDocumentLayout(new QPlainTextDocumentLayout(m_notesDocument)); /** * get file name */ const QString notesFileName = projectLocalFileName(QStringLiteral("notes")); if (notesFileName.isEmpty()) { return m_notesDocument; } /** * and load text if possible */ QFile inFile(notesFileName); if (inFile.open(QIODevice::ReadOnly)) { QTextStream inStream(&inFile); inStream.setCodec("UTF-8"); m_notesDocument->setPlainText(inStream.readAll()); } /** * and be done */ return m_notesDocument; } void KateProject::saveNotesDocument() { /** * no notes document, nothing to do */ if (!m_notesDocument) { return; } /** * get content & filename */ const QString content = m_notesDocument->toPlainText(); const QString notesFileName = projectLocalFileName(QStringLiteral("notes")); if (notesFileName.isEmpty()) { return; } /** * no content => unlink file, if there */ if (content.isEmpty()) { if (QFile::exists(notesFileName)) { QFile::remove(notesFileName); } return; } /** * else: save content to file */ QFile outFile(projectLocalFileName(QStringLiteral("notes"))); if (outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QTextStream outStream(&outFile); outStream.setCodec("UTF-8"); outStream << content; } } void KateProject::slotModifiedChanged(KTextEditor::Document *document) { KateProjectItem *item = itemForFile(m_documents.value(document)); if (!item) { return; } item->slotModifiedChanged(document); } void KateProject::slotModifiedOnDisk(KTextEditor::Document *document, bool isModified, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) { KateProjectItem *item = itemForFile(m_documents.value(document)); if (!item) { return; } item->slotModifiedOnDisk(document, isModified, reason); } void KateProject::registerDocument(KTextEditor::Document *document) { // remember the document, if not already there if (!m_documents.contains(document)) { m_documents[document] = document->url().toLocalFile(); } // try to get item for the document KateProjectItem *item = itemForFile(document->url().toLocalFile()); // if we got one, we are done, else create a dummy! if (item) { disconnect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); disconnect(document, SIGNAL(modifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(slotModifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason))); item->slotModifiedChanged(document); /*FIXME item->slotModifiedOnDisk(document,document->isModified(),qobject_cast(document)->modifiedOnDisk()); FIXME*/ connect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); connect(document, SIGNAL(modifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(slotModifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason))); return; } registerUntrackedDocument(document); } void KateProject::registerUntrackedDocument(KTextEditor::Document *document) { // perhaps create the parent item if (!m_untrackedDocumentsRoot) { m_untrackedDocumentsRoot = new KateProjectItem(KateProjectItem::Directory, i18n("")); m_model.insertRow(0, m_untrackedDocumentsRoot); } // create document item QFileInfo fileInfo(document->url().toLocalFile()); KateProjectItem *fileItem = new KateProjectItem(KateProjectItem::File, fileInfo.fileName()); fileItem->setData(document->url().toLocalFile(), Qt::ToolTipRole); fileItem->slotModifiedChanged(document); connect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); connect(document, SIGNAL(modifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(slotModifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason))); bool inserted = false; for (int i = 0; i < m_untrackedDocumentsRoot->rowCount(); ++i) { if (m_untrackedDocumentsRoot->child(i)->data(Qt::UserRole).toString() > document->url().toLocalFile()) { m_untrackedDocumentsRoot->insertRow(i, fileItem); inserted = true; break; } } if (!inserted) { m_untrackedDocumentsRoot->appendRow(fileItem); } fileItem->setData(document->url().toLocalFile(), Qt::UserRole); fileItem->setData(QVariant(true), Qt::UserRole + 3); if (!m_file2Item) { m_file2Item = KateProjectSharedQMapStringItem(new QMap()); } (*m_file2Item)[document->url().toLocalFile()] = fileItem; } void KateProject::unregisterDocument(KTextEditor::Document *document) { if (!m_documents.contains(document)) { return; } disconnect(document, &KTextEditor::Document::modifiedChanged, this, &KateProject::slotModifiedChanged); const QString &file = m_documents.value(document); if (m_untrackedDocumentsRoot) { KateProjectItem *item = static_cast(itemForFile(file)); if (item && item->data(Qt::UserRole + 3).toBool()) { unregisterUntrackedItem(item); m_file2Item->remove(file); } } m_documents.remove(document); } void KateProject::unregisterUntrackedItem(const KateProjectItem *item) { for (int i = 0; i < m_untrackedDocumentsRoot->rowCount(); ++i) { if (m_untrackedDocumentsRoot->child(i) == item) { m_untrackedDocumentsRoot->removeRow(i); break; } } if (m_untrackedDocumentsRoot->rowCount() < 1) { m_model.removeRow(0); m_untrackedDocumentsRoot = nullptr; } } diff --git a/addons/project/kateproject.h b/addons/project/kateproject.h index 1786e651b..1511f6dfc 100644 --- a/addons/project/kateproject.h +++ b/addons/project/kateproject.h @@ -1,312 +1,319 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_H #define KATE_PROJECT_H #include #include #include #include #include #include "kateprojectindex.h" #include "kateprojectitem.h" /** * Shared pointer data types. * Used to pass pointers over queued connected slots */ typedef QSharedPointer KateProjectSharedQStandardItem; Q_DECLARE_METATYPE(KateProjectSharedQStandardItem) typedef QSharedPointer> KateProjectSharedQMapStringItem; Q_DECLARE_METATYPE(KateProjectSharedQMapStringItem) typedef QSharedPointer KateProjectSharedProjectIndex; Q_DECLARE_METATYPE(KateProjectSharedProjectIndex) namespace ThreadWeaver { class Queue; } +class KateProjectPlugin; + /** * Class representing a project. * Holds project properties like name, groups, contained files, ... */ class KateProject : public QObject { Q_OBJECT public: /** * construct empty project */ - KateProject(ThreadWeaver::Queue *weaver); + KateProject(ThreadWeaver::Queue *weaver, KateProjectPlugin *plugin); /** * deconstruct project */ ~KateProject() override; /** * Load a project from project file * Only works once, afterwards use reload(). * @param fileName name of project file * @return success */ bool loadFromFile(const QString &fileName); bool loadFromData(const QVariantMap &globalProject, const QString &directory); /** * Try to reload a project. * If the reload fails, e.g. because the file is not readable or corrupt, nothing will happen! * @param force will enforce the worker to update files list and co even if the content of the file was not changed! * @return success */ bool reload(bool force = false); /** * Accessor to file name. * @return file name */ const QString &fileName() const { return m_fileName; } /** * Return the base directory of this project. * @return base directory of project, might not be the directory of the fileName! */ const QString &baseDir() const { return m_baseDir; } /** * Return the time when the project file has been modified last. * @return QFileInfo::lastModified() */ QDateTime fileLastModified() const { return m_fileLastModified; } /** * Accessor to project map containing the whole project info. * @return project info */ const QVariantMap &projectMap() const { return m_projectMap; } /** * Accessor to project name. * @return project name */ QString name() const { // MSVC doesn't support QStringLiteral here return m_projectMap[QStringLiteral("name")].toString(); } /** * Accessor for the model. * @return model of this project */ QStandardItemModel *model() { return &m_model; } /** * Flat list of all files in the project * @return list of files in project */ QStringList files() { return m_file2Item ? m_file2Item->keys() : QStringList(); } /** * get item for file * @param file file to get item for * @return item for given file or 0 */ KateProjectItem *itemForFile(const QString &file) { return m_file2Item ? m_file2Item->value(file) : nullptr; } /** * Access to project index. * May be null. * Don't store this pointer, might change. * @return project index */ KateProjectIndex *projectIndex() { return m_projectIndex.data(); } /** * Computes a suitable file name for the given suffix. * If you e.g. want to store a "notes" file, you could pass "notes" and get * the full path to projectbasedir/.kateproject.notes * @param suffix suffix for the file * @return full path for project local file, on error => empty string */ QString projectLocalFileName(const QString &suffix) const; /** * Document with project local notes. * Will be stored in a projectLocalFile "notes.txt". * @return notes document */ QTextDocument *notesDocument(); /** * Save the notes document to "notes.txt" if any document around. */ void saveNotesDocument(); /** * Register a document for this project. * @param document document to register */ void registerDocument(KTextEditor::Document *document); /** * Unregister a document for this project. * @param document document to unregister */ void unregisterDocument(KTextEditor::Document *document); private Q_SLOTS: bool load(const QVariantMap &globalProject, bool force = false); /** * Used for worker to send back the results of project loading * @param topLevel new toplevel element for model * @param file2Item new file => item mapping */ void loadProjectDone(const KateProjectSharedQStandardItem &topLevel, KateProjectSharedQMapStringItem file2Item); /** * Used for worker to send back the results of index loading * @param projectIndex new project index */ void loadIndexDone(KateProjectSharedProjectIndex projectIndex); void slotModifiedChanged(KTextEditor::Document *); void slotModifiedOnDisk(KTextEditor::Document *document, bool isModified, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason); Q_SIGNALS: /** * Emitted on project map changes. * This includes the name! */ void projectMapChanged(); /** * Emitted on model changes. * This includes the files list, itemForFile mapping! */ void modelChanged(); /** * Emitted when the index creation is finished. * This includes the ctags index. */ void indexChanged(); private: void registerUntrackedDocument(KTextEditor::Document *document); void unregisterUntrackedItem(const KateProjectItem *item); QVariantMap readProjectFile() const; private: /** * Last modification time of the project file */ QDateTime m_fileLastModified; /** * project file name */ QString m_fileName; /** * base directory of the project */ QString m_baseDir; /** * project name */ QString m_name; /** * variant map representing the project */ QVariantMap m_projectMap; /** * standard item model with content of this project */ QStandardItemModel m_model; /** * mapping files => items */ KateProjectSharedQMapStringItem m_file2Item; /** * project index, if any */ KateProjectSharedProjectIndex m_projectIndex; /** * notes buffer for project local notes */ QTextDocument *m_notesDocument; /** * Set of existing documents for this project. */ QMap m_documents; /** * Parent item for existing documents that are not in the project tree */ QStandardItem *m_untrackedDocumentsRoot; ThreadWeaver::Queue *m_weaver; /** * project configuration (read from file or injected) */ QVariantMap m_globalProject; + + /** + * Project plugin (configuration) + */ + KateProjectPlugin *m_plugin; }; #endif diff --git a/addons/project/kateprojectconfigpage.cpp b/addons/project/kateprojectconfigpage.cpp index 9916159b0..cf99c7e31 100644 --- a/addons/project/kateprojectconfigpage.cpp +++ b/addons/project/kateprojectconfigpage.cpp @@ -1,105 +1,128 @@ /* This file is part of the KDE project 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 "kateprojectconfigpage.h" #include "kateprojectplugin.h" #include #include #include #include #include +#include KateProjectConfigPage::KateProjectConfigPage(QWidget *parent, KateProjectPlugin *plugin) : KTextEditor::ConfigPage(parent) , m_plugin(plugin) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); QVBoxLayout *vbox = new QVBoxLayout; QGroupBox *group = new QGroupBox(i18nc("Groupbox title", "Autoload Repositories"), this); group->setWhatsThis( i18n("Project plugin is able to autoload repository working copies when " "there is no .kateproject file defined yet.")); m_cbAutoGit = new QCheckBox(i18n("&Git"), this); vbox->addWidget(m_cbAutoGit); m_cbAutoSubversion = new QCheckBox(i18n("&Subversion"), this); vbox->addWidget(m_cbAutoSubversion); m_cbAutoMercurial = new QCheckBox(i18n("&Mercurial"), this); vbox->addWidget(m_cbAutoMercurial); vbox->addStretch(1); group->setLayout(vbox); layout->addWidget(group); + + vbox = new QVBoxLayout(); + group = new QGroupBox(i18nc("Groupbox title", "Project Index"), this); + group->setWhatsThis(i18n("Project ctags index settings")); + m_cbIndexEnabled = new QCheckBox(i18n("Enable indexing"), this); + vbox->addWidget(m_cbIndexEnabled); + auto label = new QLabel(this); + label->setText(QStringLiteral("Directory for index files")); + vbox->addWidget(label); + m_indexPath = new KUrlRequester(this); + m_indexPath->setToolTip(QStringLiteral("The system tempory directory is used if not specified, which may overflow for very large repositories")); + vbox->addWidget(m_indexPath); + vbox->addStretch(1); + group->setLayout(vbox); + layout->addWidget(group); + layout->insertStretch(-1, 10); reset(); connect(m_cbAutoGit, &QCheckBox::stateChanged, this, &KateProjectConfigPage::slotMyChanged); connect(m_cbAutoSubversion, &QCheckBox::stateChanged, this, &KateProjectConfigPage::slotMyChanged); connect(m_cbAutoMercurial, &QCheckBox::stateChanged, this, &KateProjectConfigPage::slotMyChanged); + connect(m_cbIndexEnabled, &QCheckBox::stateChanged, this, &KateProjectConfigPage::slotMyChanged); + connect(m_indexPath, &KUrlRequester::textChanged, this, &KateProjectConfigPage::slotMyChanged); + connect(m_indexPath, &KUrlRequester::urlSelected, this, &KateProjectConfigPage::slotMyChanged); } QString KateProjectConfigPage::name() const { return i18n("Projects"); } QString KateProjectConfigPage::fullName() const { return i18nc("Groupbox title", "Projects Properties"); } QIcon KateProjectConfigPage::icon() const { return QIcon::fromTheme(QLatin1String("view-list-tree")); } void KateProjectConfigPage::apply() { if (!m_changed) { return; } m_changed = false; m_plugin->setAutoRepository(m_cbAutoGit->checkState() == Qt::Checked, m_cbAutoSubversion->checkState() == Qt::Checked, m_cbAutoMercurial->checkState() == Qt::Checked); + m_plugin->setIndex(m_cbIndexEnabled->checkState() == Qt::Checked, m_indexPath->url()); } void KateProjectConfigPage::reset() { m_cbAutoGit->setCheckState(m_plugin->autoGit() ? Qt::Checked : Qt::Unchecked); m_cbAutoSubversion->setCheckState(m_plugin->autoSubversion() ? Qt::Checked : Qt::Unchecked); m_cbAutoMercurial->setCheckState(m_plugin->autoMercurial() ? Qt::Checked : Qt::Unchecked); + m_cbIndexEnabled->setCheckState(m_plugin->getIndexEnabled() ? Qt::Checked : Qt::Unchecked); + m_indexPath->setUrl(m_plugin->getIndexDirectory()); m_changed = false; } void KateProjectConfigPage::defaults() { reset(); } void KateProjectConfigPage::slotMyChanged() { m_changed = true; emit changed(); } diff --git a/addons/project/kateprojectconfigpage.h b/addons/project/kateprojectconfigpage.h index 96d786255..4f800796a 100644 --- a/addons/project/kateprojectconfigpage.h +++ b/addons/project/kateprojectconfigpage.h @@ -1,57 +1,60 @@ /* This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_CONFIGPAGE_H #define KATE_PROJECT_CONFIGPAGE_H #include class KateProjectPlugin; class QWidget; class QCheckBox; +class KUrlRequester; class KateProjectConfigPage : public KTextEditor::ConfigPage { Q_OBJECT public: explicit KateProjectConfigPage(QWidget *parent = nullptr, KateProjectPlugin *plugin = nullptr); ~KateProjectConfigPage() override { } QString name() const override; QString fullName() const override; QIcon icon() const override; public Q_SLOTS: void apply() override; void defaults() override; void reset() override; private Q_SLOTS: void slotMyChanged(); private: QCheckBox *m_cbAutoGit; QCheckBox *m_cbAutoSubversion; QCheckBox *m_cbAutoMercurial; + QCheckBox *m_cbIndexEnabled; + KUrlRequester *m_indexPath; KateProjectPlugin *m_plugin; bool m_changed = false; }; #endif /* KATE_PROJECT_CONFIGPAGE_H */ diff --git a/addons/project/kateprojectindex.cpp b/addons/project/kateprojectindex.cpp index 477c9f150..e2f69f31e 100644 --- a/addons/project/kateprojectindex.cpp +++ b/addons/project/kateprojectindex.cpp @@ -1,204 +1,239 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * 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 "kateprojectindex.h" #include #include /** * include ctags reading */ #include "ctags/readtags.c" -KateProjectIndex::KateProjectIndex(const QStringList &files, const QVariantMap &ctagsMap) - : m_ctagsIndexFile(QDir::tempPath() + QStringLiteral("/kate.project.ctags")) - , m_ctagsIndexHandle(nullptr) +KateProjectIndex::KateProjectIndex(const QString &baseDir, const QString &indexDir, const QStringList &files, const QVariantMap &ctagsMap, bool force) + : m_ctagsIndexHandle(nullptr) { + // allow project to override and specify a (re-usable) indexfile + // otherwise fall-back to a temporary file if nothing specified + auto ctagsFile = ctagsMap.value(QStringLiteral("indexfile")); + if (ctagsFile.userType() == QMetaType::QString) { + auto path = ctagsFile.toString(); + if (!QDir::isAbsolutePath(path)) { + path = QDir(baseDir).absoluteFilePath(path); + } + m_ctagsIndexFile.reset(new QFile(path)); + } else { + // indexDir is typically QDir::tempPath() or otherwise specified in configuration + m_ctagsIndexFile.reset(new QTemporaryFile(indexDir + QStringLiteral("/kate.project.ctags"))); + } + /** * load ctags */ - loadCtags(files, ctagsMap); + loadCtags(files, ctagsMap, force); } KateProjectIndex::~KateProjectIndex() { /** * delete ctags handle if any */ if (m_ctagsIndexHandle) { tagsClose(m_ctagsIndexHandle); m_ctagsIndexHandle = nullptr; } } -void KateProjectIndex::loadCtags(const QStringList &files, const QVariantMap &ctagsMap) +void KateProjectIndex::loadCtags(const QStringList &files, const QVariantMap &ctagsMap, bool force) { + /** + * only overwrite existing index upon reload + * (a temporary index file will never exist) + */ + if (m_ctagsIndexFile->exists() && !force) { + openCtags(); + return; + } + /** * create temporary file * if not possible, fail */ - if (!m_ctagsIndexFile.open()) { + if (!m_ctagsIndexFile->open(QIODevice::ReadWrite)) { return; } /** * close file again, other process will use it */ - m_ctagsIndexFile.close(); + m_ctagsIndexFile->close(); /** * try to run ctags for all files in this project * output to our ctags index file */ QProcess ctags; QStringList args; - args << QStringLiteral("-L") << QStringLiteral("-") << QStringLiteral("-f") << m_ctagsIndexFile.fileName() << QStringLiteral("--fields=+K+n"); + args << QStringLiteral("-L") << QStringLiteral("-") << QStringLiteral("-f") << m_ctagsIndexFile->fileName() << QStringLiteral("--fields=+K+n"); const QString keyOptions = QStringLiteral("options"); for (const QVariant &optVariant : ctagsMap[keyOptions].toList()) { args << optVariant.toString(); } ctags.start(QStringLiteral("ctags"), args); if (!ctags.waitForStarted()) { return; } /** * write files list and close write channel */ ctags.write(files.join(QLatin1Char('\n')).toLocal8Bit()); ctags.closeWriteChannel(); /** * wait for done */ if (!ctags.waitForFinished(-1)) { return; } + openCtags(); +} + +void KateProjectIndex::openCtags() +{ /** * file not openable, bad */ - if (!m_ctagsIndexFile.open()) { + if (!m_ctagsIndexFile->open(QIODevice::ReadOnly)) { return; } /** * get size */ - qint64 size = m_ctagsIndexFile.size(); + qint64 size = m_ctagsIndexFile->size(); /** * close again */ - m_ctagsIndexFile.close(); + m_ctagsIndexFile->close(); /** * empty file, bad */ if (!size) { return; } + /** + * close current + */ + if (m_ctagsIndexHandle) { + tagsClose(m_ctagsIndexHandle); + m_ctagsIndexHandle = nullptr; + } + /** * try to open ctags file */ tagFileInfo info; memset(&info, 0, sizeof(tagFileInfo)); - m_ctagsIndexHandle = tagsOpen(m_ctagsIndexFile.fileName().toLocal8Bit().constData(), &info); + m_ctagsIndexHandle = tagsOpen(m_ctagsIndexFile->fileName().toLocal8Bit().constData(), &info); } void KateProjectIndex::findMatches(QStandardItemModel &model, const QString &searchWord, MatchType type) { /** * abort if no ctags index */ if (!m_ctagsIndexHandle) { return; } /** * word to complete * abort if empty */ QByteArray word = searchWord.toLocal8Bit(); if (word.isEmpty()) { return; } /** * try to search entry * fail if none found */ tagEntry entry; if (tagsFind(m_ctagsIndexHandle, &entry, word.constData(), TAG_PARTIALMATCH | TAG_OBSERVECASE) != TagSuccess) { return; } /** * set to show words only once for completion matches */ QSet guard; /** * loop over all found tags * first one is filled by above find, others by find next */ do { /** * skip if no name */ if (!entry.name) { continue; } /** * get name */ QString name(QString::fromLocal8Bit(entry.name)); /** * construct right items */ switch (type) { case CompletionMatches: /** * add new completion item, if new name */ if (!guard.contains(name)) { model.appendRow(new QStandardItem(name)); guard.insert(name); } break; case FindMatches: /** * add new find item, contains of multiple columns */ QList items; items << new QStandardItem(name); items << new QStandardItem(entry.kind ? QString::fromLocal8Bit(entry.kind) : QString()); items << new QStandardItem(entry.file ? QString::fromLocal8Bit(entry.file) : QString()); items << new QStandardItem(QString::number(entry.address.lineNumber)); model.appendRow(items); break; } } while (tagsFindNext(m_ctagsIndexHandle, &entry) == TagSuccess); } diff --git a/addons/project/kateprojectindex.h b/addons/project/kateprojectindex.h index e5ef7c1be..c2cf7917e 100644 --- a/addons/project/kateprojectindex.h +++ b/addons/project/kateprojectindex.h @@ -1,113 +1,118 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_INDEX_H #define KATE_PROJECT_INDEX_H #include #include #include #include #include /** * ctags reading */ #include "ctags/readtags.h" /** * Class representing the index of a project. * This includes knowledge from ctags and Co. * Allows you to search for stuff and to get some useful auto-completion. * Is created in Worker thread in the background, then passed to project in * the main thread for usage. */ class KateProjectIndex { public: /** * construct new index for given files * @param files files to index * @param ctagsMap ctags section for extra options */ - KateProjectIndex(const QStringList &files, const QVariantMap &ctagsMap); + KateProjectIndex(const QString &baseDir, const QString &indexDir, const QStringList &files, const QVariantMap &ctagsMap, bool force); /** * deconstruct project */ ~KateProjectIndex(); /** * Which kind of match items should be created in the passed model * of the findMatches function? */ enum MatchType { /** * Completion matches, containing only name and same name only once */ CompletionMatches, /** * Find matches, containing name, kind, file, line, ... */ FindMatches }; /** * Fill in completion matches for given view/range. * Uses e.g. ctags index. * @param model model to fill with matches * @param searchWord word to search for * @param type type of matches */ void findMatches(QStandardItemModel &model, const QString &searchWord, MatchType type); /** * Check if running ctags was successful. This can be used * as indicator whether ctags is installed or not. * @return true if a valid index exists, otherwise false */ bool isValid() const { return m_ctagsIndexHandle; } private: /** * Load ctags tags. * @param files files to index * @param ctagsMap ctags section for extra options */ - void loadCtags(const QStringList &files, const QVariantMap &ctagsMap); + void loadCtags(const QStringList &files, const QVariantMap &ctagsMap, bool force); + + /** + * Open ctags tags. + */ + void openCtags(); private: /** * ctags index file */ - QTemporaryFile m_ctagsIndexFile; + QScopedPointer m_ctagsIndexFile; /** * handle to ctags file for querying, if possible */ tagFile *m_ctagsIndexHandle; }; #endif diff --git a/addons/project/kateprojectplugin.cpp b/addons/project/kateprojectplugin.cpp index 16a327d8c..bf58d283a 100644 --- a/addons/project/kateprojectplugin.cpp +++ b/addons/project/kateprojectplugin.cpp @@ -1,422 +1,445 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * 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 "kateprojectplugin.h" #include "kateproject.h" #include "kateprojectconfigpage.h" #include "kateprojectpluginview.h" #include #include #include #include // delete, when we depend on KF 5.63 #include #include #include #include #include #include #include #ifdef HAVE_CTERMID #include #include #include #include #include #endif namespace { const QString ProjectFileName = QStringLiteral(".kateproject"); const QString GitFolderName = QStringLiteral(".git"); const QString SubversionFolderName = QStringLiteral(".svn"); const QString MercurialFolderName = QStringLiteral(".hg"); const QString GitConfig = QStringLiteral("git"); const QString SubversionConfig = QStringLiteral("subversion"); const QString MercurialConfig = QStringLiteral("mercurial"); const QStringList DefaultConfig = QStringList() << GitConfig << SubversionConfig << MercurialConfig; } KateProjectPlugin::KateProjectPlugin(QObject *parent, const QList &) : KTextEditor::Plugin(parent) , m_completion(this) , m_autoGit(true) , m_autoSubversion(true) , m_autoMercurial(true) , m_weaver(new ThreadWeaver::Queue(this)) { qRegisterMetaType("KateProjectSharedQStandardItem"); qRegisterMetaType("KateProjectSharedQMapStringItem"); qRegisterMetaType("KateProjectSharedProjectIndex"); connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentCreated, this, &KateProjectPlugin::slotDocumentCreated); connect(&m_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &KateProjectPlugin::slotDirectoryChanged); // read configuration prior to cwd project setup below readConfig(); #ifdef HAVE_CTERMID /** * open project for our current working directory, if this kate has a terminal * http://stackoverflow.com/questions/1312922/detect-if-stdin-is-a-terminal-or-pipe-in-c-c-qt */ char tty[L_ctermid + 1] = {0}; ctermid(tty); int fd = ::open(tty, O_RDONLY); if (fd >= 0) { projectForDir(QDir::current()); ::close(fd); } #endif for (auto document : KTextEditor::Editor::instance()->application()->documents()) { slotDocumentCreated(document); } registerVariables(); } KateProjectPlugin::~KateProjectPlugin() { unregisterVariables(); for (KateProject *project : m_projects) { m_fileWatcher.removePath(QFileInfo(project->fileName()).canonicalPath()); delete project; } m_projects.clear(); m_weaver->shutDown(); delete m_weaver; } QObject *KateProjectPlugin::createView(KTextEditor::MainWindow *mainWindow) { return new KateProjectPluginView(this, mainWindow); } int KateProjectPlugin::configPages() const { return 1; } KTextEditor::ConfigPage *KateProjectPlugin::configPage(int number, QWidget *parent) { if (number != 0) { return nullptr; } return new KateProjectConfigPage(parent, this); } KateProject *KateProjectPlugin::createProjectForFileName(const QString &fileName) { - KateProject *project = new KateProject(m_weaver); + KateProject *project = new KateProject(m_weaver, this); if (!project->loadFromFile(fileName)) { delete project; return nullptr; } m_projects.append(project); m_fileWatcher.addPath(QFileInfo(fileName).canonicalPath()); emit projectCreated(project); return project; } KateProject *KateProjectPlugin::projectForDir(QDir dir) { /** * search project file upwards * with recursion guard * do this first for all level and only after this fails try to invent projects * otherwise one e.g. invents projects for .kateproject tree structures with sub .git clones */ QSet seenDirectories; std::vector directoryStack; while (!seenDirectories.contains(dir.absolutePath())) { // update guard seenDirectories.insert(dir.absolutePath()); // remember directory for later project creation based on other criteria directoryStack.push_back(dir.absolutePath()); // check for project and load it if found const QString canonicalPath = dir.canonicalPath(); const QString canonicalFileName = dir.filePath(ProjectFileName); for (KateProject *project : m_projects) { if (project->baseDir() == canonicalPath || project->fileName() == canonicalFileName) { return project; } } // project file found => done if (dir.exists(ProjectFileName)) { return createProjectForFileName(canonicalFileName); } // else: cd up, if possible or abort if (!dir.cdUp()) { break; } } /** * if we arrive here, we found no .kateproject * => we want to invent a project based on e.g. version control system info */ for (const QString &dir : directoryStack) { // try to invent project based on version control stuff KateProject *project = nullptr; if ((project = detectGit(dir)) || (project = detectSubversion(dir)) || (project = detectMercurial(dir))) { return project; } } // no project found, bad luck return nullptr; } KateProject *KateProjectPlugin::projectForUrl(const QUrl &url) { if (url.isEmpty() || !url.isLocalFile()) { return nullptr; } return projectForDir(QFileInfo(url.toLocalFile()).absoluteDir()); } void KateProjectPlugin::slotDocumentCreated(KTextEditor::Document *document) { connect(document, &KTextEditor::Document::documentUrlChanged, this, &KateProjectPlugin::slotDocumentUrlChanged); connect(document, &KTextEditor::Document::destroyed, this, &KateProjectPlugin::slotDocumentDestroyed); slotDocumentUrlChanged(document); } void KateProjectPlugin::slotDocumentDestroyed(QObject *document) { if (KateProject *project = m_document2Project.value(document)) { project->unregisterDocument(static_cast(document)); } m_document2Project.remove(document); } void KateProjectPlugin::slotDocumentUrlChanged(KTextEditor::Document *document) { KateProject *project = projectForUrl(document->url()); if (KateProject *project = m_document2Project.value(document)) { project->unregisterDocument(document); } if (!project) { m_document2Project.remove(document); } else { m_document2Project[document] = project; } if (KateProject *project = m_document2Project.value(document)) { project->registerDocument(document); } } void KateProjectPlugin::slotDirectoryChanged(const QString &path) { QString fileName = QDir(path).filePath(ProjectFileName); for (KateProject *project : m_projects) { if (project->fileName() == fileName) { QDateTime lastModified = QFileInfo(fileName).lastModified(); if (project->fileLastModified().isNull() || (lastModified > project->fileLastModified())) { project->reload(); } break; } } } KateProject *KateProjectPlugin::detectGit(const QDir &dir) { // allow .git as dir and file (file for git worktree stuff, https://git-scm.com/docs/git-worktree) if (m_autoGit && dir.exists(GitFolderName)) { return createProjectForRepository(QStringLiteral("git"), dir); } return nullptr; } KateProject *KateProjectPlugin::detectSubversion(const QDir &dir) { if (m_autoSubversion && dir.exists(SubversionFolderName) && QFileInfo(dir, SubversionFolderName).isDir()) { return createProjectForRepository(QStringLiteral("svn"), dir); } return nullptr; } KateProject *KateProjectPlugin::detectMercurial(const QDir &dir) { if (m_autoMercurial && dir.exists(MercurialFolderName) && QFileInfo(dir, MercurialFolderName).isDir()) { return createProjectForRepository(QStringLiteral("hg"), dir); } return nullptr; } KateProject *KateProjectPlugin::createProjectForRepository(const QString &type, const QDir &dir) { QVariantMap cnf, files; files[type] = 1; cnf[QStringLiteral("name")] = dir.dirName(); cnf[QStringLiteral("files")] = (QVariantList() << files); - KateProject *project = new KateProject(m_weaver); + KateProject *project = new KateProject(m_weaver, this); project->loadFromData(cnf, dir.canonicalPath()); m_projects.append(project); emit projectCreated(project); return project; } void KateProjectPlugin::setAutoRepository(bool onGit, bool onSubversion, bool onMercurial) { m_autoGit = onGit; m_autoSubversion = onSubversion; m_autoMercurial = onMercurial; writeConfig(); } bool KateProjectPlugin::autoGit() const { return m_autoGit; } bool KateProjectPlugin::autoSubversion() const { return m_autoSubversion; } bool KateProjectPlugin::autoMercurial() const { return m_autoMercurial; } +void KateProjectPlugin::setIndex(bool enabled, const QUrl &directory) +{ + m_indexEnabled = enabled; + m_indexDirectory = directory; + writeConfig(); +} + +bool KateProjectPlugin::getIndexEnabled() const +{ + return m_indexEnabled; +} + +QUrl KateProjectPlugin::getIndexDirectory() const +{ + return m_indexDirectory; +} + void KateProjectPlugin::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "project"); QStringList autorepository = config.readEntry("autorepository", DefaultConfig); m_autoGit = m_autoSubversion = m_autoMercurial = false; if (autorepository.contains(GitConfig)) { m_autoGit = true; } if (autorepository.contains(SubversionConfig)) { m_autoSubversion = true; } if (autorepository.contains(MercurialConfig)) { m_autoMercurial = true; } + + m_indexEnabled = config.readEntry("index", false); + m_indexDirectory = config.readEntry("indexDirectory", QUrl()); } void KateProjectPlugin::writeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "project"); QStringList repos; if (m_autoGit) { repos << GitConfig; } if (m_autoSubversion) { repos << SubversionConfig; } if (m_autoMercurial) { repos << MercurialConfig; } config.writeEntry("autorepository", repos); + + config.writeEntry("index", m_indexEnabled); + config.writeEntry("indexDirectory", m_indexDirectory); } #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 63, 0) static KateProjectPlugin *findProjectPlugin() { auto plugin = KTextEditor::Editor::instance()->application()->plugin(QStringLiteral("kateprojectplugin")); return qobject_cast(plugin); } #endif void KateProjectPlugin::registerVariables() { #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 63, 0) auto editor = KTextEditor::Editor::instance(); editor->registerVariableMatch(QStringLiteral("Project:Path"), i18n("Full path to current project excluding the file name."), [](const QStringView &, KTextEditor::View *view) { if (!view) { return QString(); } auto projectPlugin = findProjectPlugin(); if (!projectPlugin) { return QString(); } auto kateProject = findProjectPlugin()->projectForUrl(view->document()->url()); if (!kateProject) { return QString(); } return QDir(kateProject->baseDir()).absolutePath(); }); editor->registerVariableMatch(QStringLiteral("Project:NativePath"), i18n("Full path to current project excluding the file name, with native path separator (backslash on Windows)."), [](const QStringView &, KTextEditor::View *view) { if (!view) { return QString(); } auto projectPlugin = findProjectPlugin(); if (!projectPlugin) { return QString(); } auto kateProject = findProjectPlugin()->projectForUrl(view->document()->url()); if (!kateProject) { return QString(); } return QDir::toNativeSeparators(QDir(kateProject->baseDir()).absolutePath()); }); #endif } void KateProjectPlugin::unregisterVariables() { #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 63, 0) auto editor = KTextEditor::Editor::instance(); editor->unregisterVariableMatch(QStringLiteral("Project:Path")); editor->unregisterVariableMatch(QStringLiteral("Project:NativePath")); #endif } diff --git a/addons/project/kateprojectplugin.h b/addons/project/kateprojectplugin.h index 8a00cbe78..df935da87 100644 --- a/addons/project/kateprojectplugin.h +++ b/addons/project/kateprojectplugin.h @@ -1,186 +1,192 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef _KATE_PROJECT_PLUGIN_H_ #define _KATE_PROJECT_PLUGIN_H_ #include #include #include #include #include #include #include "kateproject.h" #include "kateprojectcompletion.h" namespace ThreadWeaver { class Queue; } class KateProjectPlugin : public KTextEditor::Plugin { Q_OBJECT public: explicit KateProjectPlugin(QObject *parent = nullptr, const QList & = QList()); ~KateProjectPlugin() override; QObject *createView(KTextEditor::MainWindow *mainWindow) override; int configPages() const override; KTextEditor::ConfigPage *configPage(int number = 0, QWidget *parent = nullptr) override; /** * Create new project for given project filename. * Null pointer if no project can be opened. * File name will be canonicalized! * @param fileName canonicalized file name for the project * @return project or null if not openable */ KateProject *createProjectForFileName(const QString &fileName); /** * Search and open project for given dir, if possible. * Will search upwards for .kateproject file. * Will use internally projectForFileName if project file is found. * @param dir dir to search matching project for * @return project or null if not openable */ KateProject *projectForDir(QDir dir); /** * Search and open project that contains given url, if possible. * Will search upwards for .kateproject file, if the url is a local file. * Will use internally projectForDir. * @param url url to search matching project for * @return project or null if not openable */ KateProject *projectForUrl(const QUrl &url); /** * get list of all current open projects * @return list of all open projects */ QList projects() const { return m_projects; } /** * Get global code completion. * @return global completion object for KTextEditor::View */ KateProjectCompletion *completion() { return &m_completion; } /** * Map current open documents to projects. * @param document document we want to know which project it belongs to * @return project or 0 if none found for this document */ KateProject *projectForDocument(KTextEditor::Document *document) { return m_document2Project.value(document); } void setAutoRepository(bool onGit, bool onSubversion, bool onMercurial); bool autoGit() const; bool autoSubversion() const; bool autoMercurial() const; + void setIndex(bool enabled, const QUrl &directory); + bool getIndexEnabled() const; + QUrl getIndexDirectory() const; + Q_SIGNALS: /** * Signal that a new project got created. * @param project new created project */ void projectCreated(KateProject *project); public Q_SLOTS: /** * New document got created, we need to update our connections * @param document new created document */ void slotDocumentCreated(KTextEditor::Document *document); /** * Document got destroyed. * @param document deleted document */ void slotDocumentDestroyed(QObject *document); /** * Url changed, to auto-load projects */ void slotDocumentUrlChanged(KTextEditor::Document *document); /** * did some project file change? * @param path name of directory that did change */ void slotDirectoryChanged(const QString &path); private: KateProject *createProjectForRepository(const QString &type, const QDir &dir); KateProject *detectGit(const QDir &dir); KateProject *detectSubversion(const QDir &dir); KateProject *detectMercurial(const QDir &dir); void readConfig(); void writeConfig(); void registerVariables(); void unregisterVariables(); private: /** * open plugins, maps project base directory => project */ QList m_projects; /** * filesystem watcher to keep track of all project files * and auto-reload */ QFileSystemWatcher m_fileWatcher; /** * Mapping document => project */ QHash m_document2Project; /** * Project completion */ KateProjectCompletion m_completion; bool m_autoGit : 1; bool m_autoSubversion : 1; bool m_autoMercurial : 1; + bool m_indexEnabled : 1; + QUrl m_indexDirectory; ThreadWeaver::Queue *m_weaver; }; #endif diff --git a/addons/project/kateprojectworker.cpp b/addons/project/kateprojectworker.cpp index c49dbb9e0..e2e52a797 100644 --- a/addons/project/kateprojectworker.cpp +++ b/addons/project/kateprojectworker.cpp @@ -1,483 +1,492 @@ /* This file is part of the Kate project. * * Copyright (C) 2012 Christoph Cullmann * * 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 "kateprojectworker.h" #include "kateproject.h" #include #include #include #include #include #include #include #include #include -KateProjectWorker::KateProjectWorker(const QString &baseDir, const QVariantMap &projectMap) +KateProjectWorker::KateProjectWorker(const QString &baseDir, const QString &indexDir, const QVariantMap &projectMap, bool force) : QObject() , ThreadWeaver::Job() , m_baseDir(baseDir) + , m_indexDir(indexDir) , m_projectMap(projectMap) + , m_force(force) { Q_ASSERT(!m_baseDir.isEmpty()); } void KateProjectWorker::run(ThreadWeaver::JobPointer, ThreadWeaver::Thread *) { /** * Create dummy top level parent item and empty map inside shared pointers * then load the project recursively */ KateProjectSharedQStandardItem topLevel(new QStandardItem()); KateProjectSharedQMapStringItem file2Item(new QMap()); loadProject(topLevel.data(), m_projectMap, file2Item.data()); /** * create some local backup of some data we need for further processing! */ QStringList files = file2Item->keys(); emit loadDone(topLevel, file2Item); // trigger index loading, will internally handle enable/disabled - loadIndex(files); + loadIndex(files, m_force); } void KateProjectWorker::loadProject(QStandardItem *parent, const QVariantMap &project, QMap *file2Item) { /** * recurse to sub-projects FIRST */ QVariantList subGroups = project[QStringLiteral("projects")].toList(); for (const QVariant &subGroupVariant : subGroups) { /** * convert to map and get name, else skip */ QVariantMap subProject = subGroupVariant.toMap(); const QString keyName = QStringLiteral("name"); if (subProject[keyName].toString().isEmpty()) { continue; } /** * recurse */ QStandardItem *subProjectItem = new KateProjectItem(KateProjectItem::Project, subProject[keyName].toString()); loadProject(subProjectItem, subProject, file2Item); parent->appendRow(subProjectItem); } /** * load all specified files */ const QString keyFiles = QStringLiteral("files"); QVariantList files = project[keyFiles].toList(); for (const QVariant &fileVariant : files) { loadFilesEntry(parent, fileVariant.toMap(), file2Item); } } /** * small helper to construct directory parent items * @param dir2Item map for path => item * @param path current path we need item for * @return correct parent item for given path, will reuse existing ones */ static QStandardItem *directoryParent(QMap &dir2Item, QString path) { /** * throw away simple / */ if (path == QLatin1String("/")) { path = QString(); } /** * quick check: dir already seen? */ if (dir2Item.contains(path)) { return dir2Item[path]; } /** * else: construct recursively */ int slashIndex = path.lastIndexOf(QLatin1Char('/')); /** * no slash? * simple, no recursion, append new item toplevel */ if (slashIndex < 0) { dir2Item[path] = new KateProjectItem(KateProjectItem::Directory, path); dir2Item[QString()]->appendRow(dir2Item[path]); return dir2Item[path]; } /** * else, split and recurse */ const QString leftPart = path.left(slashIndex); const QString rightPart = path.right(path.size() - (slashIndex + 1)); /** * special handling if / with nothing on one side are found */ if (leftPart.isEmpty() || rightPart.isEmpty()) { return directoryParent(dir2Item, leftPart.isEmpty() ? rightPart : leftPart); } /** * else: recurse on left side */ dir2Item[path] = new KateProjectItem(KateProjectItem::Directory, rightPart); directoryParent(dir2Item, leftPart)->appendRow(dir2Item[path]); return dir2Item[path]; } void KateProjectWorker::loadFilesEntry(QStandardItem *parent, const QVariantMap &filesEntry, QMap *file2Item) { QDir dir(m_baseDir); if (!dir.cd(filesEntry[QStringLiteral("directory")].toString())) { return; } QStringList files = findFiles(dir, filesEntry); if (files.isEmpty()) { return; } files.sort(Qt::CaseInsensitive); /** * construct paths first in tree and items in a map */ QMap dir2Item; dir2Item[QString()] = parent; QList> item2ParentPath; for (const QString &filePath : files) { /** * skip dupes */ if (file2Item->contains(filePath)) { continue; } /** * get file info and skip NON-files */ QFileInfo fileInfo(filePath); if (!fileInfo.isFile()) { continue; } /** * construct the item with right directory prefix * already hang in directories in tree */ KateProjectItem *fileItem = new KateProjectItem(KateProjectItem::File, fileInfo.fileName()); fileItem->setData(filePath, Qt::ToolTipRole); // get the directory's relative path to the base directory QString dirRelPath = dir.relativeFilePath(fileInfo.absolutePath()); // if the relative path is ".", clean it up if (dirRelPath == QLatin1Char('.')) { dirRelPath = QString(); } item2ParentPath.append(QPair(fileItem, directoryParent(dir2Item, dirRelPath))); fileItem->setData(filePath, Qt::UserRole); (*file2Item)[filePath] = fileItem; } /** * plug in the file items to the tree */ auto i = item2ParentPath.constBegin(); while (i != item2ParentPath.constEnd()) { i->second->appendRow(i->first); ++i; } } QStringList KateProjectWorker::findFiles(const QDir &dir, const QVariantMap &filesEntry) { const bool recursive = !filesEntry.contains(QLatin1String("recursive")) || filesEntry[QStringLiteral("recursive")].toBool(); if (filesEntry[QStringLiteral("git")].toBool()) { return filesFromGit(dir, recursive); } else if (filesEntry[QStringLiteral("hg")].toBool()) { return filesFromMercurial(dir, recursive); } else if (filesEntry[QStringLiteral("svn")].toBool()) { return filesFromSubversion(dir, recursive); } else if (filesEntry[QStringLiteral("darcs")].toBool()) { return filesFromDarcs(dir, recursive); } else { QStringList files = filesEntry[QStringLiteral("list")].toStringList(); if (files.empty()) { QStringList filters = filesEntry[QStringLiteral("filters")].toStringList(); files = filesFromDirectory(dir, recursive, filters); } return files; } } QStringList KateProjectWorker::filesFromGit(const QDir &dir, bool recursive) { /** * query files via ls-files and make them absolute afterwards */ const QStringList relFiles = gitLsFiles(dir); QStringList files; for (const QString &relFile : relFiles) { if (!recursive && (relFile.indexOf(QLatin1Char('/')) != -1)) { continue; } files.append(dir.absolutePath() + QLatin1Char('/') + relFile); } return files; } QStringList KateProjectWorker::gitLsFiles(const QDir &dir) { /** * git ls-files -z results a bytearray where each entry is \0-terminated. * NOTE: Without -z, Umlauts such as "Der Bäcker/Das Brötchen.txt" do not work (#389415) * * use --recurse-submodules, there since git 2.11 (released 2016) * our own submodules handling code leads to file duplicates */ QStringList args; args << QStringLiteral("ls-files") << QStringLiteral("-z") << QStringLiteral("--recurse-submodules") << QStringLiteral("."); QProcess git; git.setWorkingDirectory(dir.absolutePath()); git.start(QStringLiteral("git"), args); QStringList files; if (!git.waitForStarted() || !git.waitForFinished(-1)) { return files; } const QList byteArrayList = git.readAllStandardOutput().split('\0'); for (const QByteArray &byteArray : byteArrayList) { files << QString::fromUtf8(byteArray); } return files; } QStringList KateProjectWorker::filesFromMercurial(const QDir &dir, bool recursive) { QStringList files; QProcess hg; hg.setWorkingDirectory(dir.absolutePath()); QStringList args; args << QStringLiteral("manifest") << QStringLiteral("."); hg.start(QStringLiteral("hg"), args); if (!hg.waitForStarted() || !hg.waitForFinished(-1)) { return files; } const QStringList relFiles = QString::fromLocal8Bit(hg.readAllStandardOutput()).split(QRegularExpression(QStringLiteral("[\n\r]")), QString::SkipEmptyParts); for (const QString &relFile : relFiles) { if (!recursive && (relFile.indexOf(QLatin1Char('/')) != -1)) { continue; } files.append(dir.absolutePath() + QLatin1Char('/') + relFile); } return files; } QStringList KateProjectWorker::filesFromSubversion(const QDir &dir, bool recursive) { QStringList files; QProcess svn; svn.setWorkingDirectory(dir.absolutePath()); QStringList args; args << QStringLiteral("status") << QStringLiteral("--verbose") << QStringLiteral("."); if (recursive) { args << QStringLiteral("--depth=infinity"); } else { args << QStringLiteral("--depth=files"); } svn.start(QStringLiteral("svn"), args); if (!svn.waitForStarted() || !svn.waitForFinished(-1)) { return files; } /** * get output and split up into lines */ const QStringList lines = QString::fromLocal8Bit(svn.readAllStandardOutput()).split(QRegularExpression(QStringLiteral("[\n\r]")), QString::SkipEmptyParts); /** * remove start of line that is no filename, sort out unknown and ignore */ bool first = true; int prefixLength = -1; for (const QString &line : lines) { /** * get length of stuff to cut */ if (first) { /** * try to find ., else fail */ prefixLength = line.lastIndexOf(QLatin1Char('.')); if (prefixLength < 0) { break; } /** * skip first */ first = false; continue; } /** * get file, if not unknown or ignored * prepend directory path */ if ((line.size() > prefixLength) && line[0] != QLatin1Char('?') && line[0] != QLatin1Char('I')) { files.append(dir.absolutePath() + QLatin1Char('/') + line.right(line.size() - prefixLength)); } } return files; } QStringList KateProjectWorker::filesFromDarcs(const QDir &dir, bool recursive) { QStringList files; const QString cmd = QStringLiteral("darcs"); QString root; { QProcess darcs; darcs.setWorkingDirectory(dir.absolutePath()); QStringList args; args << QStringLiteral("list") << QStringLiteral("repo"); darcs.start(cmd, args); if (!darcs.waitForStarted() || !darcs.waitForFinished(-1)) return files; auto str = QString::fromLocal8Bit(darcs.readAllStandardOutput()); QRegularExpression exp(QStringLiteral("Root: ([^\\n\\r]*)")); auto match = exp.match(str); if (!match.hasMatch()) return files; root = match.captured(1); } QStringList relFiles; { QProcess darcs; QStringList args; darcs.setWorkingDirectory(dir.absolutePath()); args << QStringLiteral("list") << QStringLiteral("files") << QStringLiteral("--no-directories") << QStringLiteral("--pending"); darcs.start(cmd, args); if (!darcs.waitForStarted() || !darcs.waitForFinished(-1)) return files; relFiles = QString::fromLocal8Bit(darcs.readAllStandardOutput()).split(QRegularExpression(QStringLiteral("[\n\r]")), QString::SkipEmptyParts); } for (const QString &relFile : relFiles) { const QString path = dir.relativeFilePath(root + QLatin1String("/") + relFile); if ((!recursive && (relFile.indexOf(QLatin1Char('/')) != -1)) || (recursive && (relFile.indexOf(QLatin1String("..")) == 0))) { continue; } files.append(dir.absoluteFilePath(path)); } return files; } QStringList KateProjectWorker::filesFromDirectory(const QDir &_dir, bool recursive, const QStringList &filters) { QStringList files; QDir dir(_dir); dir.setFilter(QDir::Files); if (!filters.isEmpty()) { dir.setNameFilters(filters); } /** * construct flags for iterator */ QDirIterator::IteratorFlags flags = QDirIterator::NoIteratorFlags; if (recursive) { flags = flags | QDirIterator::Subdirectories; } /** * create iterator and collect all files */ QDirIterator dirIterator(dir, flags); while (dirIterator.hasNext()) { dirIterator.next(); files.append(dirIterator.filePath()); } return files; } -void KateProjectWorker::loadIndex(const QStringList &files) +void KateProjectWorker::loadIndex(const QStringList &files, bool force) { /** * load index, if enabled * before this was default on, which is dangerous for large repositories, e.g. out-of-memory or out-of-disk + * if specified in project map; use that setting, otherwise fall back to global setting */ - if (!m_projectMap[QStringLiteral("index")].toBool()) { + bool indexEnabled = !m_indexDir.isEmpty(); + auto indexValue = m_projectMap[QStringLiteral("index")]; + if (!indexValue.isNull()) { + indexEnabled = indexValue.toBool(); + } + if (!indexEnabled) { emit loadIndexDone(KateProjectSharedProjectIndex()); return; } /** * create new index, this will do the loading in the constructor * wrap it into shared pointer for transfer to main thread */ const QString keyCtags = QStringLiteral("ctags"); - KateProjectSharedProjectIndex index(new KateProjectIndex(files, m_projectMap[keyCtags].toMap())); + KateProjectSharedProjectIndex index(new KateProjectIndex(m_baseDir, m_indexDir, files, m_projectMap[keyCtags].toMap(), force)); + emit loadIndexDone(index); } diff --git a/addons/project/kateprojectworker.h b/addons/project/kateprojectworker.h index 908b2c66b..ce14d584c 100644 --- a/addons/project/kateprojectworker.h +++ b/addons/project/kateprojectworker.h @@ -1,103 +1,110 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_WORKER_H #define KATE_PROJECT_WORKER_H #include "kateprojectitem.h" #include "kateproject.h" #include #include #include class QDir; /** * Class representing a project background worker. * This worker will build up the model for the project on load and do other stuff in the background. */ class KateProjectWorker : public QObject, public ThreadWeaver::Job { Q_OBJECT public: /** * Type for QueuedConnection */ typedef QMap MapString2Item; - explicit KateProjectWorker(const QString &baseDir, const QVariantMap &projectMap); + explicit KateProjectWorker(const QString &baseDir, const QString &indexDir, const QVariantMap &projectMap, bool force); void run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) override; Q_SIGNALS: void loadDone(KateProjectSharedQStandardItem topLevel, KateProjectSharedQMapStringItem file2Item); void loadIndexDone(KateProjectSharedProjectIndex index); private: /** * Load one project inside the project tree. * Fill data from JSON storage to model and recurse to sub-projects. * @param parent parent standard item in the model * @param project variant map for this group * @param file2Item mapping file => item, will be filled */ void loadProject(QStandardItem *parent, const QVariantMap &project, QMap *file2Item); /** * Load one files entry in the current parent item. * @param parent parent standard item in the model * @param filesEntry one files entry specification to load * @param file2Item mapping file => item, will be filled */ void loadFilesEntry(QStandardItem *parent, const QVariantMap &filesEntry, QMap *file2Item); /** * Load index for whole project. * @param files list of all project files to index */ - void loadIndex(const QStringList &files); + void loadIndex(const QStringList &files, bool force); QStringList findFiles(const QDir &dir, const QVariantMap &filesEntry); QStringList filesFromGit(const QDir &dir, bool recursive); QStringList filesFromMercurial(const QDir &dir, bool recursive); QStringList filesFromSubversion(const QDir &dir, bool recursive); QStringList filesFromDarcs(const QDir &dir, bool recursive); QStringList filesFromDirectory(const QDir &dir, bool recursive, const QStringList &filters); QStringList gitLsFiles(const QDir &dir); private: /** * our project, only as QObject, we only send messages back and forth! */ QObject *m_project; /** * project base directory name */ const QString m_baseDir; + + /** + * index directory + */ + const QString m_indexDir; + const QVariantMap m_projectMap; + const bool m_force; }; #endif