diff --git a/debugger/util/treemodel.h b/debugger/util/treemodel.h index b14addeb43..e3d5148ca0 100644 --- a/debugger/util/treemodel.h +++ b/debugger/util/treemodel.h @@ -1,91 +1,90 @@ /* * This file is part of KDevelop * * Copyright 2008 Vladimir Prus * * 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. */ #ifndef KDEVPLATFORM_TREEMODEL_H #define KDEVPLATFORM_TREEMODEL_H #include #include #include #include #include #include namespace KDevelop { class TreeItem; class KDEVPLATFORMDEBUGGER_EXPORT TreeModel : public QAbstractItemModel { Q_OBJECT public: explicit TreeModel(const QVector& headers, QObject *parent = 0); void setRootItem(TreeItem *item); ~TreeModel() override; void expanded(const QModelIndex &index); void collapsed(const QModelIndex &index); void clicked(const QModelIndex &index); void setEditable(bool); TreeItem* root() const; enum { ItemRole = Qt::UserRole, }; public: // QAbstractItemModel overrides QVariant data(const QModelIndex &index, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; Q_SIGNALS: void itemChildrenReady(); public: TreeItem* itemForIndex(const QModelIndex& index) const; QModelIndex indexForItem(TreeItem *item, int column) const; using QAbstractItemModel::beginInsertRows; using QAbstractItemModel::endInsertRows; using QAbstractItemModel::beginRemoveRows; using QAbstractItemModel::endRemoveRows; using QAbstractItemModel::dataChanged; private: QVector headers_; TreeItem *root_; - bool editable_; }; } #endif diff --git a/language/codecompletion/codecompletionhelper.cpp b/language/codecompletion/codecompletionhelper.cpp index c72ba9bc6e..0daef5fb2a 100644 --- a/language/codecompletion/codecompletionhelper.cpp +++ b/language/codecompletion/codecompletionhelper.cpp @@ -1,104 +1,105 @@ /* Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "codecompletionhelper.h" #include "../duchain/duchain.h" #include "../duchain/declaration.h" #include "../duchain/duchainlock.h" #include "../duchain/types/functiontype.h" #include "../duchain/types/integraltype.h" #include #include namespace KDevelop { void insertFunctionParenText(KTextEditor::View* view, const KTextEditor::Cursor& pos, DeclarationPointer declaration, bool jumpForbidden) { bool spaceBeforeParen = false; ///@todo Take this from some astyle config or something bool spaceBetweenParens = false; bool spaceBetweenEmptyParens = false; KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); bool haveArguments = false; if(!declaration) return; TypePtr< FunctionType > funcType = declaration->type(); if( declaration->kind() == Declaration::Type || (funcType && funcType->indexedArgumentsSize()) ) haveArguments = true; if( declaration->kind() == Declaration::Instance && !declaration->isFunctionDeclaration()) haveArguments = true; //probably a constructor initializer //Need to have a paren behind QString suffix = view->document()->text( KTextEditor::Range( pos, pos + KTextEditor::Cursor(1, 0) ) ); if( suffix.trimmed().startsWith('(') ) { //Move the cursor behind the opening paren if( view ) view->setCursorPosition( pos + KTextEditor::Cursor( 0, suffix.indexOf('(')+1 ) ); }else{ //We need to insert an opening paren QString openingParen; if( spaceBeforeParen ) openingParen = QStringLiteral(" ("); else openingParen = '('; if( spaceBetweenParens && (haveArguments || spaceBetweenEmptyParens) ) openingParen += ' '; QString closingParen; if( spaceBetweenParens && (haveArguments) ) { closingParen = QStringLiteral(" )"); } else closingParen = ')'; KTextEditor::Cursor jumpPos = pos + KTextEditor::Cursor( 0, openingParen.length() ); // when function returns void, also add a semicolon if (funcType) { if (IntegralType::Ptr type = funcType->returnType().cast()) { if (type->dataType() == IntegralType::TypeVoid) { const QChar nextChar = view->document()->characterAt(pos); if (nextChar != ';' && nextChar != ')' && nextChar != ',') { closingParen += ';'; } } } } //If no arguments, move the cursor behind the closing paren (or semicolon) if( !haveArguments ) jumpPos += KTextEditor::Cursor( 0, closingParen.length() ); lock.unlock(); - view->document()->insertText( pos, openingParen + closingParen ); - if(!jumpForbidden) { - if( view ) - view->setCursorPosition( jumpPos ); + if(view) { + view->document()->insertText( pos, openingParen + closingParen ); + if(!jumpForbidden) { + view->setCursorPosition( jumpPos ); + } } } } } diff --git a/language/duchain/duchain.cpp b/language/duchain/duchain.cpp index 9599dd240c..9153b1ae26 100644 --- a/language/duchain/duchain.cpp +++ b/language/duchain/duchain.cpp @@ -1,1713 +1,1731 @@ /* This is part of KDevelop Copyright 2006-2008 Hamish Rodda Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "duchain.h" #include "duchainlock.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../interfaces/ilanguagesupport.h" #include "../interfaces/icodehighlighting.h" #include "../backgroundparser/backgroundparser.h" #include "util/debug.h" #include "topducontext.h" #include "topducontextdata.h" #include "topducontextdynamicdata.h" #include "parsingenvironment.h" #include "declaration.h" #include "definitions.h" #include "duchainutils.h" #include "use.h" #include "uses.h" #include "abstractfunctiondeclaration.h" #include "duchainregister.h" #include "persistentsymboltable.h" #include "serialization/itemrepository.h" #include "waitforupdate.h" #include "importers.h" #ifdef HAVE_MALLOC_TRIM #include "malloc.h" #endif namespace { //Additional "soft" cleanup steps that are done before the actual cleanup. //During "soft" cleanup, the consistency is not guaranteed. The repository is //marked to be updating during soft cleanup, so if kdevelop crashes, it will be cleared. //The big advantage of the soft cleanup steps is, that the duchain is always only locked for //short times, which leads to no lockup in the UI. const int SOFT_CLEANUP_STEPS = 1; const uint cleanupEverySeconds = 200; ///Approximate maximum count of top-contexts that are checked during final cleanup const uint maxFinalCleanupCheckContexts = 2000; const uint minimumFinalCleanupCheckContextsPercentage = 10; //Check at least n% of all top-contexts during cleanup //Set to true as soon as the duchain is deleted } namespace KDevelop { bool DUChain::m_deleted = false; ///Must be locked through KDevelop::SpinLock before using chainsByIndex ///This lock should be locked only for very short times QMutex DUChain::chainsByIndexLock; std::vector DUChain::chainsByIndex; //This thing is not actually used, but it's needed for compiling DEFINE_LIST_MEMBER_HASH(EnvironmentInformationListItem, items, uint) //An entry for the item-repository that holds some meta-data. Behind this entry, the actual ParsingEnvironmentFileData is stored. class EnvironmentInformationItem { public: EnvironmentInformationItem(uint topContext, uint size) : m_topContext(topContext), m_size(size) { } ~EnvironmentInformationItem() { } unsigned int hash() const { return m_topContext; } unsigned int itemSize() const { return sizeof(*this) + m_size; } uint m_topContext; uint m_size;//Size of the data behind, that holds the actual item }; struct ItemRepositoryIndexHash { uint operator()(unsigned int __x) const { return 173*(__x>>2) + 11 * (__x >> 16); } }; class EnvironmentInformationRequest { public: ///This constructor should only be used for lookup EnvironmentInformationRequest(uint topContextIndex) : m_file(0), m_index(topContextIndex) { } EnvironmentInformationRequest(const ParsingEnvironmentFile* file) : m_file(file), m_index(file->indexedTopContext().index()) { } enum { AverageSize = 32 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_index; } uint itemSize() const { return sizeof(EnvironmentInformationItem) + DUChainItemSystem::self().dynamicSize(*m_file->d_func()); } void createItem(EnvironmentInformationItem* item) const { new (item) EnvironmentInformationItem(m_index, DUChainItemSystem::self().dynamicSize(*m_file->d_func())); Q_ASSERT(m_file->d_func()->m_dynamic); DUChainBaseData* data = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); DUChainItemSystem::self().copy(*m_file->d_func(), *data, true); Q_ASSERT(data->m_range == m_file->d_func()->m_range); Q_ASSERT(data->classId == m_file->d_func()->classId); Q_ASSERT(data->m_dynamic == false); } static void destroy(EnvironmentInformationItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationItem(); //We don't need to call the destructor, because that's done in DUChainBase::makeDynamic() //We just need to make sure that every environment-file is dynamic when it's deleted // DUChainItemSystem::self().callDestructor((DUChainBaseData*)(((char*)item) + sizeof(EnvironmentInformationItem))); } static bool persistent(const EnvironmentInformationItem* ) { //Cleanup done separately return true; } bool equals(const EnvironmentInformationItem* item) const { return m_index == item->m_topContext; } const ParsingEnvironmentFile* m_file; uint m_index; }; ///A list of environment-information/top-contexts mapped to a file-name class EnvironmentInformationListItem { public: EnvironmentInformationListItem() { initializeAppendedLists(true); } EnvironmentInformationListItem(const EnvironmentInformationListItem& rhs, bool dynamic = true) { initializeAppendedLists(dynamic); m_file = rhs.m_file; copyListsFrom(rhs); } ~EnvironmentInformationListItem() { freeAppendedLists(); } unsigned int hash() const { //We only compare the declaration. This allows us implementing a map, although the item-repository //originally represents a set. return m_file.hash(); } unsigned short int itemSize() const { return dynamicSize(); } IndexedString m_file; uint classSize() const { return sizeof(*this); } START_APPENDED_LISTS(EnvironmentInformationListItem); ///Contains the index of each contained environment-item APPENDED_LIST_FIRST(EnvironmentInformationListItem, uint, items); END_APPENDED_LISTS(EnvironmentInformationListItem, items); }; class EnvironmentInformationListRequest { public: ///This constructor should only be used for lookup EnvironmentInformationListRequest(const IndexedString& file) : m_file(file), m_item(0) { } ///This is used to actually construct the information in the repository EnvironmentInformationListRequest(const IndexedString& file, const EnvironmentInformationListItem& item) : m_file(file), m_item(&item) { } enum { AverageSize = 160 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_file.hash(); } uint itemSize() const { return m_item->itemSize(); } void createItem(EnvironmentInformationListItem* item) const { Q_ASSERT(m_item->m_file == m_file); new (item) EnvironmentInformationListItem(*m_item, false); } static void destroy(EnvironmentInformationListItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationListItem(); } static bool persistent(const EnvironmentInformationListItem*) { //Cleanup is done separately return true; } bool equals(const EnvironmentInformationListItem* item) const { return m_file == item->m_file; } IndexedString m_file; const EnvironmentInformationListItem* m_item; }; class DUChainPrivate; static DUChainPrivate* duChainPrivateSelf = 0; class DUChainPrivate { class CleanupThread : public QThread { public: CleanupThread(DUChainPrivate* data) : m_stopRunning(false), m_data(data) { } void stopThread() { { QMutexLocker lock(&m_waitMutex); m_stopRunning = true; m_wait.wakeAll(); //Wakes the thread up, so it notices it should exit } wait(); } private: void run() override { while(1) { for(uint s = 0; s < cleanupEverySeconds; ++s) { if(m_stopRunning) break; QMutexLocker lock(&m_waitMutex); m_wait.wait(&m_waitMutex, 1000); } if(m_stopRunning) break; //Just to make sure the cache is cleared periodically ModificationRevisionSet::clearCache(); - m_data->doMoreCleanup(SOFT_CLEANUP_STEPS); + m_data->doMoreCleanup(SOFT_CLEANUP_STEPS, TryLock); if(m_stopRunning) break; } } bool m_stopRunning; QWaitCondition m_wait; QMutex m_waitMutex; DUChainPrivate* m_data; }; public: DUChainPrivate() : m_chainsMutex(QMutex::Recursive), m_cleanupMutex(QMutex::Recursive), instance(0), m_cleanupDisabled(false), m_destroyed(false), m_environmentListInfo(QStringLiteral("Environment Lists")), m_environmentInfo(QStringLiteral("Environment Information")) { #if defined(TEST_NO_CLEANUP) m_cleanupDisabled = true; #endif duChainPrivateSelf = this; qRegisterMetaType("KDevelop::DUChainBasePointer"); qRegisterMetaType("KDevelop::DUContextPointer"); qRegisterMetaType("KDevelop::TopDUContextPointer"); qRegisterMetaType("KDevelop::DeclarationPointer"); qRegisterMetaType("KDevelop::FunctionDeclarationPointer"); qRegisterMetaType("KDevelop::IndexedString"); qRegisterMetaType("KDevelop::IndexedTopDUContext"); qRegisterMetaType("KDevelop::ReferencedTopDUContext"); instance = new DUChain(); m_cleanup = new CleanupThread(this); m_cleanup->start(); DUChain::m_deleted = false; ///Loading of some static data: { ///@todo Solve this more duchain-like QFile f(globalItemRepositoryRegistry().path() + "/parsing_environment_data"); bool opened = f.open(QIODevice::ReadOnly); ///FIXME: ugh, so ugly ParsingEnvironmentFile::m_staticData = reinterpret_cast( new char[sizeof(StaticParsingEnvironmentData)]); if(opened) { qCDebug(LANGUAGE) << "reading parsing-environment static data"; //Read f.read((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); }else{ qCDebug(LANGUAGE) << "creating new parsing-environment static data"; //Initialize new (ParsingEnvironmentFile::m_staticData) StaticParsingEnvironmentData(); } } ///Read in the list of available top-context indices { QFile f(globalItemRepositoryRegistry().path() + "/available_top_context_indices"); bool opened = f.open(QIODevice::ReadOnly); if(opened) { Q_ASSERT( (f.size() % sizeof(uint)) == 0); m_availableTopContextIndices.resize(f.size()/(int)sizeof(uint)); f.read((char*)m_availableTopContextIndices.data(), f.size()); } } } ~DUChainPrivate() { qCDebug(LANGUAGE) << "Destroying"; DUChain::m_deleted = true; m_cleanup->stopThread(); delete m_cleanup; delete instance; } void clear() { if(!m_cleanupDisabled) doMoreCleanup(); DUChainWriteLocker writeLock(DUChain::lock()); QMutexLocker l(&m_chainsMutex); foreach(TopDUContext* top, m_chainsByUrl.values()) removeDocumentChainFromMemory(top); m_indexEnvironmentInformations.clear(); m_fileEnvironmentInformations.clear(); Q_ASSERT(m_fileEnvironmentInformations.isEmpty()); Q_ASSERT(m_chainsByUrl.isEmpty()); } ///DUChain must be write-locked ///Also removes from the environment-manager if the top-context is not on disk void removeDocumentChainFromMemory(TopDUContext* context) { QMutexLocker l(&m_chainsMutex); { QMutexLocker l(&m_referenceCountsMutex); if(m_referenceCounts.contains(context)) { //This happens during shutdown, since everything is unloaded qCDebug(LANGUAGE) << "removed a top-context that was reference-counted:" << context->url().str() << context->ownIndex(); m_referenceCounts.remove(context); } } uint index = context->ownIndex(); // qCDebug(LANGUAGE) << "duchain: removing document" << context->url().str(); Q_ASSERT(hasChainForIndex(index)); Q_ASSERT(m_chainsByUrl.contains(context->url(), context)); m_chainsByUrl.remove(context->url(), context); if(!context->isOnDisk()) instance->removeFromEnvironmentManager(context); l.unlock(); //DUChain is write-locked, so we can do whatever we want on the top-context, including deleting it context->deleteSelf(); l.relock(); Q_ASSERT(hasChainForIndex(index)); QMutexLocker lock(&DUChain::chainsByIndexLock); DUChain::chainsByIndex[index] = 0; } ///Must be locked before accessing content of this class. ///Should be released during expensive disk-operations and such. QMutex m_chainsMutex; QMutex m_cleanupMutex; CleanupThread* m_cleanup; DUChain* instance; DUChainLock lock; QMultiMap m_chainsByUrl; //Must be locked before accessing m_referenceCounts QMutex m_referenceCountsMutex; QHash m_referenceCounts; Definitions m_definitions; Uses m_uses; QSet m_loading; bool m_cleanupDisabled; //List of available top-context indices, protected by m_chainsMutex QVector m_availableTopContextIndices; ///Used to keep alive the top-context that belong to documents loaded in the editor QSet m_openDocumentContexts; bool m_destroyed; ///The item must not be stored yet ///m_chainsMutex should not be locked, since this can trigger I/O void addEnvironmentInformation(ParsingEnvironmentFilePointer info) { Q_ASSERT(!findInformation(info->indexedTopContext().index())); Q_ASSERT(m_environmentInfo.findIndex(info->indexedTopContext().index()) == 0); QMutexLocker lock(&m_chainsMutex); m_fileEnvironmentInformations.insert(info->url(), info); m_indexEnvironmentInformations.insert(info->indexedTopContext().index(), info); Q_ASSERT(info->d_func()->classId); } ///The item must be managed currently ///m_chainsMutex does not need to be locked void removeEnvironmentInformation(ParsingEnvironmentFilePointer info) { info->makeDynamic(); //By doing this, we make sure the data is actually being destroyed in the destructor bool removed = false; bool removed2 = false; { QMutexLocker lock(&m_chainsMutex); removed = m_fileEnvironmentInformations.remove(info->url(), info); removed2 = m_indexEnvironmentInformations.remove(info->indexedTopContext().index()); } { //Remove it from the environment information lists if it was there QMutexLocker lock(m_environmentListInfo.mutex()); uint index = m_environmentListInfo.findIndex(info->url()); if(index) { EnvironmentInformationListItem item(*m_environmentListInfo.itemFromIndex(index)); if(item.itemsList().removeOne(info->indexedTopContext().index())) { m_environmentListInfo.deleteItem(index); if(!item.itemsList().isEmpty()) m_environmentListInfo.index(EnvironmentInformationListRequest(info->url(), item)); } } } QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(info->indexedTopContext().index()); if(index) { m_environmentInfo.deleteItem(index); } Q_UNUSED(removed); Q_UNUSED(removed2); Q_ASSERT(index || (removed && removed2)); Q_ASSERT(!findInformation(info->indexedTopContext().index())); } ///m_chainsMutex should _not_ be locked, because this may trigger I/O QList getEnvironmentInformation(IndexedString url) { QList ret; uint listIndex = m_environmentListInfo.findIndex(url); if(listIndex) { KDevVarLengthArray topContextIndices; { //First store all the possible intices into the KDevVarLengthArray, so we can unlock the mutex before processing them. QMutexLocker lock(m_environmentListInfo.mutex()); //Lock the mutex to make sure the item isn't changed while it's being iterated const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(listIndex); FOREACH_FUNCTION(uint topContextIndex, item->items) topContextIndices << topContextIndex; } //Process the indices in a separate step after copying them from the array, so we don't need m_environmentListInfo.mutex locked, //and can call loadInformation(..) safely, which else might lead to a deadlock. foreach (uint topContextIndex, topContextIndices) { QExplicitlySharedDataPointer< ParsingEnvironmentFile > p = ParsingEnvironmentFilePointer(loadInformation(topContextIndex)); if(p) { ret << p; }else{ qCDebug(LANGUAGE) << "Failed to load enviromment-information for" << TopDUContextDynamicData::loadUrl(topContextIndex).str(); } } } QMutexLocker l(&m_chainsMutex); //Add those information that have not been added to the stored lists yet foreach(const ParsingEnvironmentFilePointer& file, m_fileEnvironmentInformations.values(url)) if(!ret.contains(file)) ret << file; return ret; } ///Must be called _without_ the chainsByIndex spin-lock locked static inline bool hasChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); return (DUChain::chainsByIndex.size() > index) && DUChain::chainsByIndex[index]; } ///Must be called _without_ the chainsByIndex spin-lock locked. Returns the top-context if it is loaded. static inline TopDUContext* readChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() > index) return DUChain::chainsByIndex[index]; else return 0; } ///Makes sure that the chain with the given index is loaded ///@warning m_chainsMutex must NOT be locked when this is called void loadChain(uint index, QSet& loaded) { QMutexLocker l(&m_chainsMutex); if(!hasChainForIndex(index)) { if(m_loading.contains(index)) { //It's probably being loaded by another thread. So wait until the load is ready while(m_loading.contains(index)) { l.unlock(); qCDebug(LANGUAGE) << "waiting for another thread to load index" << index; QThread::usleep(50000); l.relock(); } loaded.insert(index); return; } m_loading.insert(index); loaded.insert(index); l.unlock(); qCDebug(LANGUAGE) << "loading top-context" << index; TopDUContext* chain = TopDUContextDynamicData::load(index); if(chain) { chain->setParsingEnvironmentFile(loadInformation(chain->ownIndex())); if(!chain->usingImportsCache()) { //Eventually also load all the imported chains, so the import-structure is built foreach(const DUContext::Import &import, chain->DUContext::importedParentContexts()) { if(!loaded.contains(import.topContextIndex())) { loadChain(import.topContextIndex(), loaded); } } } chain->rebuildDynamicImportStructure(); chain->setInDuChain(true); instance->addDocumentChain(chain); } l.relock(); m_loading.remove(index); } } ///Stores all environment-information ///Also makes sure that all information that stays is referenced, so it stays alive. ///@param atomic If this is false, the write-lock will be released time by time void storeAllInformation(bool atomic, DUChainWriteLocker& locker) { uint cnt = 0; QList urls; { QMutexLocker lock(&m_chainsMutex); urls += m_fileEnvironmentInformations.keys(); } foreach(const IndexedString &url, urls) { QList check; { QMutexLocker lock(&m_chainsMutex); check = m_fileEnvironmentInformations.values(url); } foreach(ParsingEnvironmentFilePointer file, check) { EnvironmentInformationRequest req(file.data()); QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(req); if(file->d_func()->isDynamic()) { //This item has been changed, or isn't in the repository yet //Eventually remove an old entry if(index) m_environmentInfo.deleteItem(index); //Add the new entry to the item repository index = m_environmentInfo.index(req); Q_ASSERT(index); EnvironmentInformationItem* item = const_cast(m_environmentInfo.itemFromIndex(index)); DUChainBaseData* theData = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); Q_ASSERT(theData->m_range == file->d_func()->m_range); Q_ASSERT(theData->m_dynamic == false); Q_ASSERT(theData->classId == file->d_func()->classId); file->setData( theData ); ++cnt; }else{ m_environmentInfo.itemFromIndex(index); //Prevent unloading of the data, by accessing the item } } ///We must not release the lock while holding a reference to a ParsingEnvironmentFilePointer, else we may miss the deletion of an ///information, and will get crashes. if(!atomic && (cnt % 100 == 0)) { //Release the lock on a regular basis locker.unlock(); locker.lock(); } storeInformationList(url); //Access the data in the repository, so the bucket isn't unloaded uint index = m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)); if(index) { m_environmentListInfo.itemFromIndex(index); }else{ QMutexLocker lock(&m_chainsMutex); qCDebug(LANGUAGE) << "Did not find stored item for" << url.str() << "count:" << m_fileEnvironmentInformations.values(url); } if(!atomic) { locker.unlock(); locker.lock(); } } } QMutex& cleanupMutex() { return m_cleanupMutex; } + /// defines how we interact with the ongoing language parse jobs + enum LockFlag { + /// no locking required, only used when we locked previously + NoLock = 0, + /// lock all parse jobs and block until we succeeded. required at shutdown + BlockingLock = 1, + /// only try to lock and abort on failure, good for the intermittent cleanups + TryLock = 2, + }; ///@param retries When this is nonzero, then doMoreCleanup will do the specified amount of cycles ///doing the cleanup without permanently locking the du-chain. During these steps the consistency ///of the disk-storage is not guaranteed, but only few changes will be done during these steps, ///so the final step where the duchain is permanently locked is much faster. - void doMoreCleanup(int retries = 0, bool needLockRepository = true) { + void doMoreCleanup(int retries = 0, LockFlag lockFlag = BlockingLock) { if(m_cleanupDisabled) return; //This mutex makes sure that there's never 2 threads at he same time trying to clean up QMutexLocker lockCleanupMutex(&cleanupMutex()); if(m_destroyed || m_cleanupDisabled) return; Q_ASSERT(!instance->lock()->currentThreadHasReadLock() && !instance->lock()->currentThreadHasWriteLock()); DUChainWriteLocker writeLock(instance->lock()); - PersistentSymbolTable::self().clearCache(); //This is used to stop all parsing before starting to do the cleanup. This way less happens during the //soft cleanups, and we have a good chance that during the "hard" cleanup only few data has to be written. - QList lockedParseMutexes; - QList locked; - if(needLockRepository) { - + if (lockFlag != NoLock) { + QList languages; if (ICore* core = ICore::self()) if (ILanguageController* lc = core->languageController()) - lockedParseMutexes = lc->loadedLanguages(); + languages = lc->loadedLanguages(); writeLock.unlock(); //Here we wait for all parsing-threads to stop their processing - foreach(const auto language, lockedParseMutexes) { - language->parseLock()->lockForWrite(); + foreach(const auto language, languages) { + if (lockFlag == TryLock) { + if (!language->parseLock()->tryLockForWrite()) { + qCDebug(LANGUAGE) << "Aborting cleanup because language plugin is still parsing:" << language->name(); + // some language is still parsing, don't interfere with the cleanup + foreach(auto* lock, locked) { + lock->unlock(); + } + return; + } + } else { + language->parseLock()->lockForWrite(); + } locked << language->parseLock(); } writeLock.lock(); globalItemRepositoryRegistry().lockForWriting(); qCDebug(LANGUAGE) << "starting cleanup"; } QTime startTime = QTime::currentTime(); + PersistentSymbolTable::self().clearCache(); storeAllInformation(!retries, writeLock); //Puts environment-information into a repository //We don't need to increase the reference-count, since the cleanup-mutex is locked QSet workOnContexts; { QMutexLocker l(&m_chainsMutex); foreach(TopDUContext* top, m_chainsByUrl.values()) { workOnContexts << top; Q_ASSERT(hasChainForIndex(top->ownIndex())); } } foreach(TopDUContext* context, workOnContexts) { context->m_dynamicData->store(); if(retries) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } //Unload all top-contexts that don't have a reference-count and that are not imported by a referenced one QSet unloadedNames; bool unloadedOne = true; bool unloadAllUnreferenced = !retries; //Now unload contexts, but only ones that are not imported by any other currently loaded context //The complication: Since during the lock-break new references may be added, we must never keep //the du-chain in an invalid state. Thus we can only unload contexts that are not imported by any //currently loaded contexts. In case of loops, we have to unload everything at once. while(unloadedOne) { unloadedOne = false; int hadUnloadable = 0; unloadContexts: foreach(TopDUContext* unload, workOnContexts) { bool hasReference = false; { QMutexLocker l(&m_referenceCountsMutex); //Test if the context is imported by a referenced one foreach(TopDUContext* context, m_referenceCounts.keys()) { if(context == unload || context->imports(unload, CursorInRevision())) { workOnContexts.remove(unload); hasReference = true; } } } if(!hasReference) ++hadUnloadable; //We have found a context that is not referenced else continue; //This context is referenced bool isImportedByLoaded = !unload->loadedImporters().isEmpty(); //If we unload a context that is imported by other contexts, we create a bad loaded state if(isImportedByLoaded && !unloadAllUnreferenced) continue; unloadedNames.insert(unload->url()); //Since we've released the write-lock in between, we've got to call store() again to be sure that none of the data is dynamic //If nothing has changed, it is only a low-cost call. unload->m_dynamicData->store(); Q_ASSERT(!unload->d_func()->m_dynamic); removeDocumentChainFromMemory(unload); workOnContexts.remove(unload); unloadedOne = true; if(!unloadAllUnreferenced) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } if(hadUnloadable && !unloadedOne) { Q_ASSERT(!unloadAllUnreferenced); //This can happen in case of loops. We have o unload everything at one time. qCDebug(LANGUAGE) << "found" << hadUnloadable << "unloadable contexts, but could not unload separately. Unloading atomically."; unloadAllUnreferenced = true; hadUnloadable = 0; //Reset to 0, so we cannot loop forever goto unloadContexts; } } if(retries == 0) { QMutexLocker lock(&m_chainsMutex); //Do this atomically, since we must be sure that _everything_ is already saved for(QMultiMap::iterator it = m_fileEnvironmentInformations.begin(); it != m_fileEnvironmentInformations.end(); ) { ParsingEnvironmentFile* f = it->data(); Q_ASSERT(f->d_func()->classId); if(f->ref.load() == 1) { Q_ASSERT(!f->d_func()->isDynamic()); //It cannot be dynamic, since we have stored before //The ParsingEnvironmentFilePointer is only referenced once. This means that it does not belong to any //loaded top-context, so just remove it to save some memory and processing time. ///@todo use some kind of timeout before removing it = m_fileEnvironmentInformations.erase(it); }else{ ++it; } } } if(retries) writeLock.unlock(); //This must be the last step, due to the on-disk reference counting globalItemRepositoryRegistry().store(); //Stores all repositories { //Store the static parsing-environment file data ///@todo Solve this more elegantly, using a general mechanism to store static duchain-like data Q_ASSERT(ParsingEnvironmentFile::m_staticData); QFile f(globalItemRepositoryRegistry().path() + "/parsing_environment_data"); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); } ///Write out the list of available top-context indices { QMutexLocker lock(&m_chainsMutex); QFile f(globalItemRepositoryRegistry().path() + "/available_top_context_indices"); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)m_availableTopContextIndices.data(), m_availableTopContextIndices.size() * sizeof(uint)); } if(retries) { - doMoreCleanup(retries-1, false); + doMoreCleanup(retries-1, NoLock); writeLock.lock(); } - if(needLockRepository) { + if(lockFlag != NoLock) { globalItemRepositoryRegistry().unlockForWriting(); int elapsedSeconds = startTime.secsTo(QTime::currentTime()); qCDebug(LANGUAGE) << "seconds spent doing cleanup: " << elapsedSeconds << "top-contexts still open:" << m_chainsByUrl.size(); } if(!retries) { int elapesedMilliSeconds = startTime.msecsTo(QTime::currentTime()); qCDebug(LANGUAGE) << "milliseconds spent doing cleanup with locked duchain: " << elapesedMilliSeconds; } foreach(QReadWriteLock* lock, locked) lock->unlock(); #ifdef HAVE_MALLOC_TRIM // trim unused memory but keep a pad buffer of about 50 MB // this can greatly decrease the perceived memory consumption of kdevelop // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(50 * 1024 * 1024); #endif } ///Checks whether the information is already loaded. ParsingEnvironmentFile* findInformation(uint topContextIndex) { QMutexLocker lock(&m_chainsMutex); QHash::iterator it = m_indexEnvironmentInformations.find(topContextIndex); if(it != m_indexEnvironmentInformations.end()) return (*it).data(); return 0; } ///Loads/gets the environment-information for the given top-context index, or returns zero if none exists ///@warning m_chainsMutex should NOT be locked when this is called, because it triggers I/O ///@warning no other mutexes should be locked, as that may lead to a dedalock ParsingEnvironmentFile* loadInformation(uint topContextIndex) { ParsingEnvironmentFile* alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; //Step two: Check if it is on disk, and if is, load it uint dataIndex = m_environmentInfo.findIndex(EnvironmentInformationRequest(topContextIndex)); if(!dataIndex) { //No environment-information stored for this top-context return 0; } const EnvironmentInformationItem& item(*m_environmentInfo.itemFromIndex(dataIndex)); QMutexLocker lock(&m_chainsMutex); //Due to multi-threading, we must do this check after locking the mutex, so we can be sure we don't create the same item twice at the same time alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; ///FIXME: ugly, and remove const_cast ParsingEnvironmentFile* ret = dynamic_cast(DUChainItemSystem::self().create( const_cast(reinterpret_cast(reinterpret_cast(&item) + sizeof(EnvironmentInformationItem))) )); if(ret) { Q_ASSERT(ret->d_func()->classId); Q_ASSERT(ret->indexedTopContext().index() == topContextIndex); ParsingEnvironmentFilePointer retPtr(ret); m_fileEnvironmentInformations.insert(ret->url(), retPtr); Q_ASSERT(!m_indexEnvironmentInformations.contains(ret->indexedTopContext().index())); m_indexEnvironmentInformations.insert(ret->indexedTopContext().index(), retPtr); } return ret; } struct CleanupListVisitor { QList checkContexts; bool operator()(const EnvironmentInformationItem* item) { checkContexts << item->m_topContext; return true; } }; ///Will check a selection of all top-contexts for up-to-date ness, and remove them if out of date void cleanupTopContexts() { DUChainWriteLocker lock( DUChain::lock() ); qCDebug(LANGUAGE) << "cleaning top-contexts"; CleanupListVisitor visitor; uint startPos = 0; m_environmentInfo.visitAllItems(visitor); int checkContextsCount = maxFinalCleanupCheckContexts; int percentageOfContexts = (visitor.checkContexts.size() * 100) / minimumFinalCleanupCheckContextsPercentage; if(checkContextsCount < percentageOfContexts) checkContextsCount = percentageOfContexts; if(visitor.checkContexts.size() > (int)checkContextsCount) startPos = qrand() % (visitor.checkContexts.size() - checkContextsCount); int endPos = startPos + maxFinalCleanupCheckContexts; if(endPos > visitor.checkContexts.size()) endPos = visitor.checkContexts.size(); QSet< uint > check; for(int a = startPos; a < endPos && check.size() < checkContextsCount; ++a) if(check.size() < checkContextsCount) addContextsForRemoval(check, IndexedTopDUContext(visitor.checkContexts[a])); foreach(uint topIndex, check) { IndexedTopDUContext top(topIndex); if(top.data()) { qCDebug(LANGUAGE) << "removing top-context for" << top.data()->url().str() << "because it is out of date"; instance->removeDocumentChain(top.data()); } } qCDebug(LANGUAGE) << "check ready"; } private: void addContextsForRemoval(QSet& topContexts, IndexedTopDUContext top) { if(topContexts.contains(top.index())) return; QExplicitlySharedDataPointer info( instance->environmentFileForDocument(top) ); ///@todo Also check if the context is "useful"(Not a duplicate context, imported by a useful one, ...) if(info && info->needsUpdate()) { //This context will be removed }else{ return; } topContexts.insert(top.index()); if(info) { //Check whether importers need to be removed as well QList< QExplicitlySharedDataPointer > importers = info->importers(); QSet< QExplicitlySharedDataPointer > checkNext; //Do breadth first search, so less imports/importers have to be loaded, and a lower depth is reached for(QList< QExplicitlySharedDataPointer >::iterator it = importers.begin(); it != importers.end(); ++it) { IndexedTopDUContext c = (*it)->indexedTopContext(); if(!topContexts.contains(c.index())) { topContexts.insert(c.index()); //Prevent useless recursion checkNext.insert(*it); } } for(QSet< QExplicitlySharedDataPointer >::const_iterator it = checkNext.begin(); it != checkNext.end(); ++it) { topContexts.remove((*it)->indexedTopContext().index()); //Enable full check again addContextsForRemoval(topContexts, (*it)->indexedTopContext()); } } } ///Stores the environment-information for the given url void storeInformationList(IndexedString url) { QMutexLocker lock(m_environmentListInfo.mutex()); EnvironmentInformationListItem newItem; newItem.m_file = url; QSet newItems; { QMutexLocker lock(&m_chainsMutex); QMultiMap::iterator start = m_fileEnvironmentInformations.lowerBound(url); QMultiMap::iterator end = m_fileEnvironmentInformations.upperBound(url); for(QMultiMap::iterator it = start; it != end; ++it) { uint topContextIndex = (*it)->indexedTopContext().index(); newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } uint index = m_environmentListInfo.findIndex(url); if(index) { //We only handle adding items here, since we can never be sure whether everything is loaded //Removal is handled directly in removeEnvironmentInformation const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(index); QSet oldItems; FOREACH_FUNCTION(uint topContextIndex, item->items) { oldItems.insert(topContextIndex); if(!newItems.contains(topContextIndex)) { newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } if(oldItems == newItems) return; ///Update/insert a new list m_environmentListInfo.deleteItem(index); //Remove the previous item } Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)) == 0); //Insert the new item m_environmentListInfo.index(EnvironmentInformationListRequest(url, newItem)); Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url))); } //Loaded environment-informations. Protected by m_chainsMutex QMultiMap m_fileEnvironmentInformations; QHash m_indexEnvironmentInformations; ///The following repositories are thread-safe, and m_chainsMutex should not be locked when using them, because ///they may trigger I/O. Still it may be required to lock their local mutexes. ///Maps filenames to a list of top-contexts/environment-information. ItemRepository m_environmentListInfo; ///Maps top-context-indices to environment-information item. ItemRepository m_environmentInfo; }; Q_GLOBAL_STATIC(DUChainPrivate, sdDUChainPrivate) DUChain::DUChain() { Q_ASSERT(ICore::self()); connect(ICore::self()->documentController(), &IDocumentController::documentLoadedPrepare, this, &DUChain::documentLoadedPrepare); connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this, &DUChain::documentRenamed); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &DUChain::documentActivated); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &DUChain::documentClosed); } DUChain::~DUChain() { DUChain::m_deleted = true; } DUChain* DUChain::self() { return sdDUChainPrivate->instance; } extern void initModificationRevisionSetRepository(); extern void initDeclarationRepositories(); extern void initIdentifierRepository(); extern void initTypeRepository(); extern void initInstantiationInformationRepository(); void DUChain::initialize() { // Initialize the global item repository as first thing after loading the session Q_ASSERT(ICore::self()); Q_ASSERT(ICore::self()->activeSession()); ItemRepositoryRegistry::initialize(ICore::self()->activeSessionLock()); initReferenceCounting(); // This needs to be initialized here too as the function is not threadsafe, but can // sometimes be called from different threads. This results in the underlying QFile // being 0 and hence crashes at some point later when accessing the contents via // read. See https://bugs.kde.org/show_bug.cgi?id=250779 RecursiveImportRepository::repository(); RecursiveImportCacheRepository::repository(); // similar to above, see https://bugs.kde.org/show_bug.cgi?id=255323 initDeclarationRepositories(); initModificationRevisionSetRepository(); initIdentifierRepository(); initTypeRepository(); initInstantiationInformationRepository(); Importers::self(); globalImportIdentifier(); globalIndexedImportIdentifier(); globalAliasIdentifier(); globalIndexedAliasIdentifier(); } DUChainLock* DUChain::lock() { return &sdDUChainPrivate->lock; } QList DUChain::allChains() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); return sdDUChainPrivate->m_chainsByUrl.values(); } void DUChain::updateContextEnvironment( TopDUContext* context, ParsingEnvironmentFile* file ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); removeFromEnvironmentManager( context ); context->setParsingEnvironmentFile( file ); addToEnvironmentManager( context ); } void DUChain::removeDocumentChain( TopDUContext* context ) { ENSURE_CHAIN_WRITE_LOCKED; IndexedTopDUContext indexed(context->indexed()); Q_ASSERT(indexed.data() == context); ///This assertion fails if you call removeDocumentChain(..) on a document that has not been added to the du-chain context->m_dynamicData->deleteOnDisk(); Q_ASSERT(indexed.data() == context); sdDUChainPrivate->removeDocumentChainFromMemory(context); Q_ASSERT(!indexed.data()); Q_ASSERT(!environmentFileForDocument(indexed)); QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); sdDUChainPrivate->m_availableTopContextIndices.push_back(indexed.index()); } void DUChain::addDocumentChain( TopDUContext * chain ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // qCDebug(LANGUAGE) << "duchain: adding document" << chain->url().str() << " " << chain; Q_ASSERT(chain); Q_ASSERT(!sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() <= chain->ownIndex()) DUChain::chainsByIndex.resize(chain->ownIndex() + 100, 0); DUChain::chainsByIndex[chain->ownIndex()] = chain; } { Q_ASSERT(DUChain::chainsByIndex[chain->ownIndex()]); } Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); sdDUChainPrivate->m_chainsByUrl.insert(chain->url(), chain); Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); chain->setInDuChain(true); l.unlock(); addToEnvironmentManager(chain); // This function might be called during shutdown by stale parse jobs // Make sure we don't access null-pointers here if (ICore::self() && ICore::self()->languageController() && ICore::self()->languageController()->backgroundParser()->trackerForUrl(chain->url())) { //Make sure the context stays alive at least as long as the context is open ReferencedTopDUContext ctx(chain); sdDUChainPrivate->m_openDocumentContexts.insert(ctx); } } void DUChain::addToEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage Q_ASSERT(file->indexedTopContext().index() == chain->ownIndex()); if(ParsingEnvironmentFile* alreadyHave = sdDUChainPrivate->findInformation(file->indexedTopContext().index())) { ///If this triggers, there has already been another environment-information registered for this top-context. ///removeFromEnvironmentManager should have been called before to remove the old environment-information. Q_ASSERT(alreadyHave == file.data()); Q_UNUSED(alreadyHave); return; } sdDUChainPrivate->addEnvironmentInformation(file); } void DUChain::removeFromEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage sdDUChainPrivate->removeEnvironmentInformation(file); } TopDUContext* DUChain::chainForDocument(const QUrl& document, bool proxyContext) const { return chainForDocument(IndexedString(document), proxyContext); } bool DUChain::isInMemory(uint topContextIndex) const { return DUChainPrivate::hasChainForIndex(topContextIndex); } IndexedString DUChain::urlForIndex(uint index) const { { TopDUContext* chain = DUChainPrivate::readChainForIndex(index); if(chain) return chain->url(); } return TopDUContextDynamicData::loadUrl(index); } TopDUContext* DUChain::loadChain(uint index) { QSet loaded; sdDUChainPrivate->loadChain(index, loaded); { QMutexLocker lock(&chainsByIndexLock); if(chainsByIndex.size() > index) { TopDUContext* top = chainsByIndex[index]; if(top) return top; } } return 0; } TopDUContext* DUChain::chainForDocument(const KDevelop::IndexedString& document, bool proxyContext) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return 0; QList list = sdDUChainPrivate->getEnvironmentInformation(document); foreach(const ParsingEnvironmentFilePointer &file, list) if(isInMemory(file->indexedTopContext().index()) && file->isProxyContext() == proxyContext) { return file->topContext(); } foreach(const ParsingEnvironmentFilePointer &file, list) if(proxyContext == file->isProxyContext()) { return file->topContext(); } //Allow selecting a top-context even if there is no ParsingEnvironmentFile QList< TopDUContext* > ret = chainsForDocument(document); foreach(TopDUContext* ctx, ret) { if(!ctx->parsingEnvironmentFile() || (ctx->parsingEnvironmentFile()->isProxyContext() == proxyContext)) return ctx; } return 0; } QList DUChain::chainsForDocument(const QUrl& document) const { return chainsForDocument(IndexedString(document)); } QList DUChain::chainsForDocument(const IndexedString& document) const { QList chains; if(sdDUChainPrivate->m_destroyed) return chains; QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // Match all parsed versions of this document for (auto it = sdDUChainPrivate->m_chainsByUrl.lowerBound(document); it != sdDUChainPrivate->m_chainsByUrl.end(); ++it) { if (it.key() == document) chains << it.value(); else break; } return chains; } TopDUContext* DUChain::chainForDocument( const QUrl& document, const KDevelop::ParsingEnvironment* environment, bool proxyContext ) const { return chainForDocument( IndexedString(document), environment, proxyContext ); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return ParsingEnvironmentFilePointer(); QList< ParsingEnvironmentFilePointer> list = sdDUChainPrivate->getEnvironmentInformation(document); // qCDebug(LANGUAGE) << document.str() << ": matching" << list.size() << (onlyProxyContexts ? "proxy-contexts" : (noProxyContexts ? "content-contexts" : "contexts")); auto it = list.constBegin(); while(it != list.constEnd()) { if(*it && ((*it)->isProxyContext() == proxyContext) && (*it)->matchEnvironment(environment) && // Verify that the environment-file and its top-context are "good": The top-context must exist, // and there must be a content-context associated to the proxy-context. (*it)->topContext() && (!proxyContext || DUChainUtils::contentContextFromProxyContext((*it)->topContext())) ) { return *it; } ++it; } return ParsingEnvironmentFilePointer(); } QList DUChain::allEnvironmentFiles(const IndexedString& document) { return sdDUChainPrivate->getEnvironmentInformation(document); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument(IndexedTopDUContext topContext) const { if(topContext.index() == 0) return ParsingEnvironmentFilePointer(); return ParsingEnvironmentFilePointer(sdDUChainPrivate->loadInformation(topContext.index())); } TopDUContext* DUChain::chainForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { if(sdDUChainPrivate->m_destroyed) return 0; ParsingEnvironmentFilePointer envFile = environmentFileForDocument(document, environment, proxyContext); if(envFile) { return envFile->topContext(); }else{ return 0; } } QList DUChain::documents() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl.values()) { ret << top->url().toUrl(); } return ret; } QList DUChain::indexedDocuments() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl.values()) { ret << top->url(); } return ret; } void DUChain::documentActivated(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; //Check whether the document has an attached environment-manager, and whether that one thinks the document needs to be updated. //If yes, update it. DUChainReadLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); TopDUContext* ctx = DUChainUtils::standardContextForUrl(doc->url(), true); if(ctx && ctx->parsingEnvironmentFile()) if(ctx->parsingEnvironmentFile()->needsUpdate()) ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url())); } void DUChain::documentClosed(IDocument* document) { if(sdDUChainPrivate->m_destroyed) return; IndexedString url(document->url()); foreach(const ReferencedTopDUContext &top, sdDUChainPrivate->m_openDocumentContexts) if(top->url() == url) sdDUChainPrivate->m_openDocumentContexts.remove(top); } void DUChain::documentLoadedPrepare(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; const IndexedString url(doc->url()); DUChainWriteLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(doc->url()); QList chains = chainsForDocument(url); auto languages = ICore::self()->languageController()->languagesForUrl(doc->url()); if(standardContext) { Q_ASSERT(chains.contains(standardContext)); //We have just loaded it Q_ASSERT((standardContext->url() == url)); sdDUChainPrivate->m_openDocumentContexts.insert(standardContext); bool needsUpdate = standardContext->parsingEnvironmentFile() && standardContext->parsingEnvironmentFile()->needsUpdate(); if(!needsUpdate) { //Only apply the highlighting if we don't need to update, else we might highlight total crap //Do instant highlighting only if all imports are loaded, to make sure that we don't block the user-interface too long //Else the highlighting will be done in the background-thread //This is not exactly right, as the direct imports don't necessarily equal the real imports used by uses //but it approximates the correct behavior. bool allImportsLoaded = true; foreach(const DUContext::Import& import, standardContext->importedParentContexts()) if(!import.indexedContext().indexedTopContext().isLoaded()) allImportsLoaded = false; if(allImportsLoaded) { l.unlock(); lock.unlock(); foreach(const auto language, languages) { if(language->codeHighlighting()) { language->codeHighlighting()->highlightDUChain(standardContext); } } qCDebug(LANGUAGE) << "highlighted" << doc->url() << "in foreground"; return; } }else{ qCDebug(LANGUAGE) << "not highlighting the duchain because the documents needs an update"; } if(needsUpdate || !(standardContext->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); return; } } //Add for highlighting etc. ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), TopDUContext::AllDeclarationsContextsAndUses); } void DUChain::documentRenamed(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; if(!doc->url().isValid()) { ///Maybe this happens when a file was deleted? qCWarning(LANGUAGE) << "Strange, url of renamed document is invalid!"; }else{ ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); } } Uses* DUChain::uses() { return &sdDUChainPrivate->m_uses; } Definitions* DUChain::definitions() { return &sdDUChainPrivate->m_definitions; } static void finalCleanup() { DUChainWriteLocker writeLock(DUChain::lock()); qCDebug(LANGUAGE) << "doing final cleanup"; int cleaned = 0; while((cleaned = globalItemRepositoryRegistry().finalCleanup())) { qCDebug(LANGUAGE) << "cleaned" << cleaned << "B"; if(cleaned < 1000) { qCDebug(LANGUAGE) << "cleaned enough"; break; } } qCDebug(LANGUAGE) << "final cleanup ready"; } void DUChain::shutdown() { // if core is not shutting down, we can end up in deadlocks or crashes // since language plugins might still try to access static duchain stuff Q_ASSERT(!ICore::self() || ICore::self()->shuttingDown()); qCDebug(LANGUAGE) << "Cleaning up and shutting down DUChain"; QMutexLocker lock(&sdDUChainPrivate->cleanupMutex()); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); sdDUChainPrivate->cleanupTopContexts(); globalItemRepositoryRegistry().unlockForWriting(); } sdDUChainPrivate->doMoreCleanup(); //Must be done _before_ finalCleanup, else we may be deleting yet needed data sdDUChainPrivate->m_openDocumentContexts.clear(); sdDUChainPrivate->m_destroyed = true; sdDUChainPrivate->clear(); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); finalCleanup(); globalItemRepositoryRegistry().unlockForWriting(); } globalItemRepositoryRegistry().shutdown(); } uint DUChain::newTopContextIndex() { { QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); if(!sdDUChainPrivate->m_availableTopContextIndices.isEmpty()) { uint ret = sdDUChainPrivate->m_availableTopContextIndices.back(); sdDUChainPrivate->m_availableTopContextIndices.pop_back(); if(TopDUContextDynamicData::fileExists(ret)) { qCWarning(LANGUAGE) << "Problem in the management of availalbe top-context indices"; return newTopContextIndex(); } return ret; } } static QAtomicInt& currentId( globalItemRepositoryRegistry().getCustomCounter(QStringLiteral("Top-Context Counter"), 1) ); return currentId.fetchAndAddRelaxed(1); } void DUChain::refCountUp(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); if(!sdDUChainPrivate->m_referenceCounts.contains(top)) sdDUChainPrivate->m_referenceCounts.insert(top, 1); else ++sdDUChainPrivate->m_referenceCounts[top]; } bool DUChain::deleted() { return m_deleted; } void DUChain::refCountDown(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); if(!sdDUChainPrivate->m_referenceCounts.contains(top)) { //qCWarning(LANGUAGE) << "tried to decrease reference-count for" << top->url().str() << "but this top-context is not referenced"; return; } --sdDUChainPrivate->m_referenceCounts[top]; if(!sdDUChainPrivate->m_referenceCounts[top]) sdDUChainPrivate->m_referenceCounts.remove(top); } void DUChain::emitDeclarationSelected(const DeclarationPointer& decl) { if(sdDUChainPrivate->m_destroyed) return; emit declarationSelected(decl); } void DUChain::emitUpdateReady(const IndexedString& url, const ReferencedTopDUContext& topContext) { if(sdDUChainPrivate->m_destroyed) return; emit updateReady(url, topContext); } KDevelop::ReferencedTopDUContext DUChain::waitForUpdate(const KDevelop::IndexedString& document, KDevelop::TopDUContext::Features minFeatures, bool proxyContext) { Q_ASSERT(!lock()->currentThreadHasReadLock() && !lock()->currentThreadHasWriteLock()); WaitForUpdate waiter; updateContextForUrl(document, minFeatures, &waiter); // waiter.m_waitMutex.lock(); // waiter.m_dataMutex.unlock(); while(!waiter.m_ready) { // we might have been shut down in the meanwhile if (!ICore::self()) { return 0; } QMetaObject::invokeMethod(ICore::self()->languageController()->backgroundParser(), "parseDocuments"); QApplication::processEvents(); QThread::usleep(1000); } if(!proxyContext) { DUChainReadLocker readLock(DUChain::lock()); return DUChainUtils::contentContextFromProxyContext(waiter.m_topContext); } return waiter.m_topContext; } void DUChain::updateContextForUrl(const IndexedString& document, TopDUContext::Features minFeatures, QObject* notifyReady, int priority) const { DUChainReadLocker lock( DUChain::lock() ); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(document.toUrl()); if(standardContext && standardContext->parsingEnvironmentFile() && !standardContext->parsingEnvironmentFile()->needsUpdate() && standardContext->parsingEnvironmentFile()->featuresSatisfied(minFeatures)) { lock.unlock(); if(notifyReady) QMetaObject::invokeMethod(notifyReady, "updateReady", Qt::DirectConnection, Q_ARG(KDevelop::IndexedString, document), Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext(standardContext))); }else{ ///Start a parse-job for the given document ICore::self()->languageController()->backgroundParser()->addDocument(document, minFeatures, priority, notifyReady); } } void DUChain::disablePersistentStorage(bool disable) { sdDUChainPrivate->m_cleanupDisabled = disable; } void DUChain::storeToDisk() { bool wasDisabled = sdDUChainPrivate->m_cleanupDisabled; sdDUChainPrivate->m_cleanupDisabled = false; sdDUChainPrivate->doMoreCleanup(); sdDUChainPrivate->m_cleanupDisabled = wasDisabled; } bool DUChain::compareToDisk() { DUChainWriteLocker writeLock(DUChain::lock()); ///Step 1: Compare the repositories return true; } } diff --git a/language/duchain/navigation/useswidget.cpp b/language/duchain/navigation/useswidget.cpp index ad02101aa2..7b477a0d27 100644 --- a/language/duchain/navigation/useswidget.cpp +++ b/language/duchain/navigation/useswidget.cpp @@ -1,655 +1,655 @@ /* Copyright 2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "useswidget.h" #include "util/debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; const int tooltipContextSize = 2; //How many lines around the use are shown in the tooltip ///The returned text is fully escaped ///@param cutOff The total count of characters that should be cut of, all in all on both sides together. ///@param range The range that is highlighted, and that will be preserved during cutting, given that there is enough room beside it. QString highlightAndEscapeUseText(QString line, int cutOff, KTextEditor::Range range) { int leftCutRoom = range.start().column(); int rightCutRoom = line.length() - range.end().column(); if(range.start().column() < 0 || range.end().column() > line.length() || cutOff > leftCutRoom + rightCutRoom) return QString(); //Not enough room for cutting off on sides int leftCut = 0; int rightCut = 0; if(leftCutRoom < rightCutRoom) { if(leftCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. leftCut = cutOff / 2; rightCut = cutOff - leftCut; }else{ //Not enough room in left side, but enough room all together leftCut = leftCutRoom; rightCut = cutOff - leftCut; } }else{ if(rightCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. rightCut = cutOff / 2; leftCut = cutOff - rightCut; }else{ //Not enough room in right side, but enough room all together rightCut = rightCutRoom; leftCut = cutOff - rightCut; } } Q_ASSERT(leftCut + rightCut <= cutOff); line = line.left(line.length() - rightCut); line = line.mid(leftCut); range += KTextEditor::Range(0, -leftCut, 0, -leftCut); Q_ASSERT(range.start().column() >= 0 && range.end().column() <= line.length()); //TODO: share code with context browser // mixing (255, 255, 0, 100) with white yields this: const QColor background(251, 250, 150); const QColor foreground(0, 0, 0); return "" + line.left(range.start().column()).toHtmlEscaped() + "" + line.mid(range.start().column(), range.end().column() - range.start().column()).toHtmlEscaped() + "" + line.mid(range.end().column(), line.length() - range.end().column()).toHtmlEscaped() + ""; } OneUseWidget::OneUseWidget(IndexedDeclaration declaration, IndexedString document, KTextEditor::Range range, const CodeRepresentation& code) : m_range(new PersistentMovingRange(range, document)), m_declaration(declaration), m_document(document) { //Make the sizing of this widget independent of the content, because we will adapt the content to the size setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); m_sourceLine = code.line(m_range->range().start().line()); m_layout = new QHBoxLayout(this); m_layout->setContentsMargins(0, 0, 0, 0); setLayout(m_layout); m_label = new QLabel(this); m_icon = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme(QStringLiteral("code-function")).pixmap(16)); connect(m_label, &QLabel::linkActivated, this, &OneUseWidget::jumpTo); DUChainReadLocker lock(DUChain::lock()); QString text = "" + i18nc("refers to a line in source code", "Line %1:", range.start().line()) + QStringLiteral(""); if(!m_sourceLine.isEmpty() && m_sourceLine.length() > m_range->range().end().column()) { text += "  " + highlightAndEscapeUseText(m_sourceLine, 0, m_range->range()); //Useful tooltip: int start = m_range->range().start().line() - tooltipContextSize; int end = m_range->range().end().line() + tooltipContextSize + 1; QString toolTipText; for(int a = start; a < end; ++a) { QString lineText = code.line(a).toHtmlEscaped(); if (m_range->range().start().line() <= a && m_range->range().end().line() >= a) { lineText = QStringLiteral("") + lineText + QStringLiteral(""); } if(!lineText.trimmed().isEmpty()) { toolTipText += lineText + "
"; } } if ( toolTipText.endsWith(QLatin1String("
")) ) { toolTipText.remove(toolTipText.length() - 4, 4); } setToolTip(QStringLiteral("
") + toolTipText + QStringLiteral("
")); } m_label->setText(text); m_layout->addWidget(m_icon); m_layout->addWidget(m_label); m_layout->setAlignment(Qt::AlignLeft); } void OneUseWidget::jumpTo() { //This is used to execute the slot delayed in the event-loop, so crashes are avoided ICore::self()->documentController()->openDocument(m_document.toUrl(), m_range->range().start()); } OneUseWidget::~OneUseWidget() { } void OneUseWidget::resizeEvent ( QResizeEvent * event ) { ///Adapt the content QSize size = event->size(); KTextEditor::Range range = m_range->range(); int cutOff = 0; int maxCutOff = m_sourceLine.length() - (range.end().column() - range.start().column()); //Reset so we also get more context while up-sizing m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); while(sizeHint().width() > size.width() && cutOff < maxCutOff) { //We've got to save space m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); cutOff += 5; } event->accept(); QWidget::resizeEvent(event); } void NavigatableWidgetList::setShowHeader(bool show) { if(show && !m_headerLayout->parent()) m_layout->insertLayout(0, m_headerLayout); else m_headerLayout->setParent(0); } NavigatableWidgetList::~NavigatableWidgetList() { delete m_headerLayout; } NavigatableWidgetList::NavigatableWidgetList(bool allowScrolling, uint maxHeight, bool vertical) : m_allowScrolling(allowScrolling) { m_layout = new QVBoxLayout; m_layout->setMargin(0); m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); m_layout->setSpacing(0); setBackgroundRole(QPalette::Base); m_useArrows = false; if(vertical) m_itemLayout = new QVBoxLayout; else m_itemLayout = new QHBoxLayout; m_itemLayout->setContentsMargins(0, 0, 0, 0); m_itemLayout->setMargin(0); m_itemLayout->setSpacing(0); // m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); // setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum); setWidgetResizable(true); m_headerLayout = new QHBoxLayout; m_headerLayout->setMargin(0); m_headerLayout->setSpacing(0); if(m_useArrows) { - m_previousButton = new QToolButton(); + auto m_previousButton = new QToolButton(); m_previousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); - m_nextButton = new QToolButton(); + auto m_nextButton = new QToolButton(); m_nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); m_headerLayout->addWidget(m_previousButton); m_headerLayout->addWidget(m_nextButton); } //hide these buttons for now, they're senseless m_layout->addLayout(m_headerLayout); QHBoxLayout* spaceLayout = new QHBoxLayout; spaceLayout->addSpacing(10); spaceLayout->addLayout(m_itemLayout); m_layout->addLayout(spaceLayout); if(maxHeight) setMaximumHeight(maxHeight); if(m_allowScrolling) { QWidget* contentsWidget = new QWidget; contentsWidget->setLayout(m_layout); setWidget(contentsWidget); }else{ setLayout(m_layout); } } void NavigatableWidgetList::deleteItems() { foreach(QWidget* item, items()) delete item; } void NavigatableWidgetList::addItem(QWidget* widget, int pos) { if(pos == -1) m_itemLayout->addWidget(widget); else m_itemLayout->insertWidget(pos, widget); } QList NavigatableWidgetList::items() const { QList ret; for(int a = 0; a < m_itemLayout->count(); ++a) { QWidgetItem* widgetItem = dynamic_cast(m_itemLayout->itemAt(a)); if(widgetItem) { ret << widgetItem->widget(); } } return ret; } bool NavigatableWidgetList::hasItems() const { return (bool)m_itemLayout->count(); } void NavigatableWidgetList::addHeaderItem(QWidget* widget, Qt::Alignment alignment) { if(m_useArrows) { Q_ASSERT(m_headerLayout->count() >= 2); //At least the 2 back/next buttons m_headerLayout->insertWidget(m_headerLayout->count()-1, widget, alignment); }else{ //We need to do this so the header doesn't get stretched widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); m_headerLayout->insertWidget(m_headerLayout->count(), widget, alignment); // widget->setMaximumHeight(20); } } ///Returns whether the uses in the child should be a new uses-group bool isNewGroup(DUContext* parent, DUContext* child) { if(parent->type() == DUContext::Other && child->type() == DUContext::Other) return false; else return true; } uint countUses(int usedDeclarationIndex, DUContext* context) { uint ret = 0; for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ++ret; foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += countUses(usedDeclarationIndex, child); return ret; } QList createUseWidgets(const CodeRepresentation& code, int usedDeclarationIndex, IndexedDeclaration decl, DUContext* context) { QList ret; VERIFY_FOREGROUND_LOCKED for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ret << new OneUseWidget(decl, context->url(), context->transformFromLocalRevision(context->uses()[useIndex].m_range), code); foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += createUseWidgets(code, usedDeclarationIndex, decl, child); return ret; } ContextUsesWidget::ContextUsesWidget(const CodeRepresentation& code, QList usedDeclarations, IndexedDUContext context) : m_context(context) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); QString headerText = i18n("Unknown context"); setUpdatesEnabled(false); if(context.data()) { DUContext* ctx = context.data(); if(ctx->scopeIdentifier(true).isEmpty()) headerText = i18n("Global"); else { headerText = ctx->scopeIdentifier(true).toString(); if(ctx->type() == DUContext::Function || (ctx->owner() && ctx->owner()->isFunctionDeclaration())) headerText += QLatin1String("(...)"); } QSet hadIndices; foreach(const IndexedDeclaration usedDeclaration, usedDeclarations) { int usedDeclarationIndex = ctx->topContext()->indexForUsedDeclaration(usedDeclaration.data(), false); if(hadIndices.contains(usedDeclarationIndex)) continue; hadIndices.insert(usedDeclarationIndex); if(usedDeclarationIndex != std::numeric_limits::max()) { foreach(OneUseWidget* widget, createUseWidgets(code, usedDeclarationIndex, usedDeclaration, ctx)) addItem(widget); } } } QLabel* headerLabel = new QLabel(i18nc("%1: source file", "In %1", "" + headerText.toHtmlEscaped() + ": ")); addHeaderItem(headerLabel); setUpdatesEnabled(true); connect(headerLabel, &QLabel::linkActivated, this, &ContextUsesWidget::linkWasActivated); } void ContextUsesWidget::linkWasActivated(QString link) { if ( link == QLatin1String("navigateToFunction") ) { DUChainReadLocker lock(DUChain::lock()); DUContext* context = m_context.context(); if(context) { CursorInRevision contextStart = context->range().start; KTextEditor::Cursor cursor(contextStart.line, contextStart.column); QUrl url = context->url().toUrl(); lock.unlock(); ForegroundLock fgLock; ICore::self()->documentController()->openDocument(url, cursor); } } } DeclarationWidget::DeclarationWidget(const CodeRepresentation& code, const IndexedDeclaration& decl) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); if (Declaration* dec = decl.data()) { QLabel* headerLabel = new QLabel(dec->isDefinition() ? i18n("Definition") : i18n("Declaration")); addHeaderItem(headerLabel); addItem(new OneUseWidget(decl, dec->url(), dec->rangeInCurrentRevision(), code)); } setUpdatesEnabled(true); } TopContextUsesWidget::TopContextUsesWidget(IndexedDeclaration declaration, QList allDeclarations, IndexedTopDUContext topContext) : m_topContext(topContext) , m_declaration(declaration) , m_allDeclarations(allDeclarations) , m_usesCount(0) { m_itemLayout->setContentsMargins(10, 0, 0, 5); setFrameShape(NoFrame); setUpdatesEnabled(false); DUChainReadLocker lock(DUChain::lock()); QHBoxLayout * labelLayout = new QHBoxLayout; labelLayout->setContentsMargins(0, -1, 0, 0); // let's keep the spacing *above* the line QWidget* headerWidget = new QWidget; headerWidget->setLayout(labelLayout); headerWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); QLabel* label = new QLabel(this); m_icon = new QLabel(this); m_toggleButton = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme(QStringLiteral("code-class")).pixmap(16)); labelLayout->addWidget(m_icon); labelLayout->addWidget(label); labelLayout->addWidget(m_toggleButton); labelLayout->setAlignment(Qt::AlignLeft); if(topContext.isLoaded()) m_usesCount = DUChainUtils::contextCountUses(topContext.data(), declaration.data()); QString labelText = i18ncp("%1: number of uses, %2: filename with uses", "%2: 1 use", "%2: %1 uses", m_usesCount, ICore::self()->projectController()->prettyFileName(topContext.url().toUrl())); label->setText(labelText); m_toggleButton->setText("   [" + i18nc("Refers to closing a UI element", "Collapse") + "]"); connect(m_toggleButton, &QLabel::linkActivated, this, &TopContextUsesWidget::labelClicked); addHeaderItem(headerWidget); setUpdatesEnabled(true); } int TopContextUsesWidget::usesCount() const { return m_usesCount; } QList buildContextUses(const CodeRepresentation& code, QList declarations, DUContext* context) { QList ret; if(!context->parentContext() || isNewGroup(context->parentContext(), context)) { ContextUsesWidget* created = new ContextUsesWidget(code, declarations, context); if(created->hasItems()) ret << created; else delete created; } foreach(DUContext* child, context->childContexts()) ret += buildContextUses(code, declarations, child); return ret; } void TopContextUsesWidget::setExpanded(bool expanded) { if(!expanded) { m_toggleButton->setText("   [" + i18nc("Refers to opening a UI element", "Expand") + "]"); deleteItems(); }else{ m_toggleButton->setText("   [" + i18nc("Refers to closing a UI element", "Collapse") + "]"); if(hasItems()) return; DUChainReadLocker lock(DUChain::lock()); TopDUContext* topContext = m_topContext.data(); if(topContext && m_declaration.data()) { CodeRepresentation::Ptr code = createCodeRepresentation(topContext->url()); setUpdatesEnabled(false); IndexedTopDUContext localTopContext(topContext); foreach(const IndexedDeclaration &decl, m_allDeclarations) { if(decl.indexedTopContext() == localTopContext) { addItem(new DeclarationWidget(*code, decl)); } } foreach(ContextUsesWidget* usesWidget, buildContextUses(*code, m_allDeclarations, topContext)) { addItem(usesWidget); } setUpdatesEnabled(true); } } } void TopContextUsesWidget::labelClicked() { if(hasItems()) { setExpanded(false); }else{ setExpanded(true); } } UsesWidget::~UsesWidget() { if (m_collector) { m_collector->setWidget(0); } } UsesWidget::UsesWidget(const IndexedDeclaration& declaration, QSharedPointer customCollector) : NavigatableWidgetList(true) { DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); m_headerLine = new QLabel; redrawHeaderLine(); connect(m_headerLine, &QLabel::linkActivated, this, &UsesWidget::headerLinkActivated); m_layout->insertWidget(0, m_headerLine, 0, Qt::AlignTop); m_layout->setAlignment(Qt::AlignTop); m_itemLayout->setAlignment(Qt::AlignTop); m_progressBar = new QProgressBar; addHeaderItem(m_progressBar); if (!customCollector) { m_collector = QSharedPointer(new UsesWidget::UsesWidgetCollector(declaration)); } else { m_collector = customCollector; } m_collector->setProcessDeclarations(true); m_collector->setWidget(this); m_collector->startCollecting(); setUpdatesEnabled(true); } void UsesWidget::redrawHeaderLine() { m_headerLine->setText(headerLineText()); } const QString UsesWidget::headerLineText() const { return i18np("1 use found", "%1 uses found", countAllUses()) + " • " "[" + i18n("Expand all") + "] • " "[" + i18n("Collapse all") + "]"; } unsigned int UsesWidget::countAllUses() const { unsigned int totalUses = 0; foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { totalUses += useWidget->usesCount(); } } return totalUses; } void UsesWidget::setAllExpanded(bool expanded) { foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { useWidget->setExpanded(expanded); } } } void UsesWidget::headerLinkActivated(QString linkName) { if(linkName == QLatin1String("expandAll")) { setAllExpanded(true); } else if(linkName == QLatin1String("collapseAll")) { setAllExpanded(false); } } UsesWidget::UsesWidgetCollector::UsesWidgetCollector(IndexedDeclaration decl) : UsesCollector(decl), m_widget(0) { } void UsesWidget::UsesWidgetCollector::setWidget(UsesWidget* widget ) { m_widget = widget; } void UsesWidget::UsesWidgetCollector::maximumProgress(uint max) { if (!m_widget) { return; } if(m_widget->m_progressBar) { m_widget->m_progressBar->setMaximum(max); m_widget->m_progressBar->setMinimum(0); m_widget->m_progressBar->setValue(0); }else{ qCWarning(LANGUAGE) << "maximumProgress called twice"; } } void UsesWidget::UsesWidgetCollector::progress(uint processed, uint total) { if (!m_widget) { return; } m_widget->redrawHeaderLine(); if(m_widget->m_progressBar) { m_widget->m_progressBar->setValue(processed); if(processed == total) { m_widget->setUpdatesEnabled(false); delete m_widget->m_progressBar; m_widget->m_progressBar = 0; m_widget->setShowHeader(false); m_widget->setUpdatesEnabled(true); } }else{ qCWarning(LANGUAGE) << "progress() called too often"; } } void UsesWidget::UsesWidgetCollector::processUses( KDevelop::ReferencedTopDUContext topContext ) { if (!m_widget) { return; } DUChainReadLocker lock; qCDebug(LANGUAGE) << "processing" << topContext->url().str(); TopContextUsesWidget* widget = new TopContextUsesWidget(declaration(), declarations(), topContext.data()); // move to back if it's just the declaration/definition bool toBack = widget->usesCount() == 0; // move to front the item belonging to the current open document IDocument* doc = ICore::self()->documentController()->activeDocument(); bool toFront = doc && (doc->url() == topContext->url().toUrl()); widget->setExpanded(true); m_widget->addItem(widget, toFront ? 0 : toBack ? widget->items().size() : -1); m_widget->redrawHeaderLine(); } QSize KDevelop::UsesWidget::sizeHint() const { QSize ret = QWidget::sizeHint(); if(ret.height() < 300) ret.setHeight(300); return ret; } diff --git a/language/duchain/navigation/useswidget.h b/language/duchain/navigation/useswidget.h index 2fd7439b55..67eec70fd3 100644 --- a/language/duchain/navigation/useswidget.h +++ b/language/duchain/navigation/useswidget.h @@ -1,162 +1,161 @@ /* Copyright 2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_USESWIDGET_H #define KDEVPLATFORM_USESWIDGET_H #include #include #include #include #include #include #include "usescollector.h" #include class QLabel; class QToolButton; class QVBoxLayout; class QHBoxLayout; class QBoxLayout; class QPushButton; class QProgressBar; namespace KDevelop { class CodeRepresentation; class IndexedDeclaration; ///A widget representing one use of a Declaration in a speicific context class KDEVPLATFORMLANGUAGE_EXPORT OneUseWidget : public QWidget { Q_OBJECT public: OneUseWidget(IndexedDeclaration declaration, IndexedString document, KTextEditor::Range range, const CodeRepresentation& code); ~OneUseWidget() override; private slots: void jumpTo(); private: void resizeEvent ( QResizeEvent * event ) override; PersistentMovingRange::Ptr m_range; IndexedDeclaration m_declaration; IndexedString m_document; QString m_sourceLine; QLabel* m_label; QLabel* m_icon; QHBoxLayout* m_layout; }; class KDEVPLATFORMLANGUAGE_EXPORT NavigatableWidgetList : public QScrollArea { Q_OBJECT public: explicit NavigatableWidgetList(bool allowScrolling = false, uint maxHeight = 0, bool vertical = true); ~NavigatableWidgetList() override; void addItem(QWidget* widget, int pos = -1); void addHeaderItem(QWidget* widget, Qt::Alignment alignment = 0); ///Whether items were added to this list using addItem(..) bool hasItems() const; ///Deletes all items that were added using addItem void deleteItems(); QList items() const; void setShowHeader(bool show); protected: QBoxLayout* m_itemLayout; QVBoxLayout* m_layout; private: QHBoxLayout* m_headerLayout; - QToolButton *m_previousButton, *m_nextButton; bool m_allowScrolling, m_useArrows; }; class KDEVPLATFORMLANGUAGE_EXPORT ContextUsesWidget : public NavigatableWidgetList { Q_OBJECT public: ContextUsesWidget(const CodeRepresentation& code, QList usedDeclaration, IndexedDUContext context); Q_SIGNALS: void navigateDeclaration(KDevelop::IndexedDeclaration); private Q_SLOTS: void linkWasActivated(QString); private: IndexedDUContext m_context; }; class KDEVPLATFORMLANGUAGE_EXPORT DeclarationWidget : public NavigatableWidgetList { Q_OBJECT public: DeclarationWidget(const KDevelop::CodeRepresentation& code, const KDevelop::IndexedDeclaration& declaration); }; /** * Represents the uses of a declaration within one top-context */ class KDEVPLATFORMLANGUAGE_EXPORT TopContextUsesWidget : public NavigatableWidgetList { Q_OBJECT public: TopContextUsesWidget(IndexedDeclaration declaration, QList localDeclarations, IndexedTopDUContext topContext); void setExpanded(bool); int usesCount() const; private slots: void labelClicked(); private: IndexedTopDUContext m_topContext; IndexedDeclaration m_declaration; QLabel* m_icon; QLabel* m_toggleButton; QList m_allDeclarations; int m_usesCount; }; /** * A widget that allows browsing through all the uses of a declaration, and also through all declarations of it. */ class KDEVPLATFORMLANGUAGE_EXPORT UsesWidget : public NavigatableWidgetList { Q_OBJECT public: ///This class can be overridden to do additional processing while the uses-widget shows the uses. struct KDEVPLATFORMLANGUAGE_EXPORT UsesWidgetCollector : public UsesCollector { public: void setWidget(UsesWidget* widget ); UsesWidgetCollector(IndexedDeclaration decl); void processUses(KDevelop::ReferencedTopDUContext topContext) override; void maximumProgress(uint max) override; void progress(uint processed, uint total) override; UsesWidget* m_widget; }; QSize sizeHint () const override; ///@param customCollector allows specifying an own subclass of UsesWidgetCollector. explicit UsesWidget(const IndexedDeclaration& declaration, QSharedPointer customCollector = {}); ~UsesWidget() override; void setAllExpanded(bool expanded); unsigned int countAllUses() const; Q_SIGNALS: void navigateDeclaration(KDevelop::IndexedDeclaration); private: const QString headerLineText() const; QLabel* m_headerLine; QSharedPointer m_collector; QProgressBar* m_progressBar; public slots: void headerLinkActivated(QString linkName); void redrawHeaderLine(); }; } #endif diff --git a/language/duchain/types/containertypes.cpp b/language/duchain/types/containertypes.cpp index bdc083bbba..5428cde41d 100644 --- a/language/duchain/types/containertypes.cpp +++ b/language/duchain/types/containertypes.cpp @@ -1,152 +1,150 @@ /** * This file is part of KDevelop * Copyright (C) 2011-2014 Sven Brauch * * 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, see . **/ #include "containertypes.h" #include "typeutils.h" #include "typeregister.h" #include "../duchain.h" #include "../duchainlock.h" #include namespace KDevelop { REGISTER_TYPE(ListType); REGISTER_TYPE(MapType); ListType::ListType() : KDevelop::StructureType(createData()) { } ListType::ListType(StructureTypeData& data) : KDevelop::StructureType(data) { } ListType::ListType(const ListType& rhs) : KDevelop::StructureType(copyData(*rhs.d_func())) { } MapType::MapType() : ListType(createData()) { }; MapType::MapType(ListTypeData& data) : ListType(data) { }; MapType::MapType(const MapType& rhs) : ListType(copyData(*rhs.d_func())) { } void ListType::replaceContentType(AbstractType::Ptr newType) { d_func_dynamic()->m_contentType = newType->indexed(); } void MapType::replaceKeyType(AbstractType::Ptr newType) { d_func_dynamic()->m_keyType = newType->indexed(); } IndexedType ListType::contentType() const { return d_func()->m_contentType; } IndexedType MapType::keyType() const { return d_func()->m_keyType; } AbstractType* ListType::clone() const { return new ListType(*this); } AbstractType* MapType::clone() const { return new MapType(*this); } QString ListType::toString() const { QString prefix = KDevelop::StructureType::toString(); auto content = contentType().abstractType(); if ( content ) { return i18n("%1 of %2", prefix, content->toString()); } return prefix; } QString MapType::toString() const { QString prefix = KDevelop::StructureType::toString(); auto content = contentType().abstractType(); auto key = keyType().abstractType(); auto key_str = ( key ? key->toString() : i18n("unknown") ); auto content_str = ( content ? content->toString() : i18n("unknown") ); if ( key || content ) { return i18n("%1 of %2 : %3", prefix, key_str, content_str); } return prefix; } QString ListType::containerToString() const { return KDevelop::StructureType::toString(); } bool ListType::equals(const AbstractType* rhs) const { if ( this == rhs ) { return true; } if ( ! KDevelop::StructureType::equals(rhs) ) { return false; } auto c = dynamic_cast(rhs); if ( ! c ) { return false; } if ( c->contentType() != d_func()->m_contentType ) { return false; } return true; } bool MapType::equals(const AbstractType* rhs) const { if ( ! ListType::equals(rhs) ) { return false; } auto c = dynamic_cast(rhs); - if ( c->keyType() != d_func()->m_keyType ) { - return false; - } - return true; + + return c && c->keyType() == d_func()->m_keyType; } uint ListType::hash() const { return StructureType::hash() + ( contentType().abstractType() ? contentType().abstractType()->hash() : 1 ); } uint MapType::hash() const { return ListType::hash() + ( keyType().abstractType() ? keyType().abstractType()->hash() : 1 ); } } // namespace KDevelop // kate: space-indent on; indent-width 4 diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp index e28a22b0d9..93b6cd053b 100644 --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -1,1506 +1,1511 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * Copyright 2010 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "gitplugin.h" #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include "gitclonejob.h" #include #include #include "stashmanagerdialog.h" #include #include #include #include #include #include #include "gitjob.h" #include "gitmessagehighlighter.h" #include "gitplugincheckinrepositoryjob.h" #include "gitnameemaildialog.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_GIT, "kdevplatform.plugins.git") using namespace KDevelop; QVariant runSynchronously(KDevelop::VcsJob* job) { QVariant ret; if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) { ret = job->fetchResults(); } delete job; return ret; } namespace { QDir dotGitDirectory(const QUrl& dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir(); static const QString gitDir = QStringLiteral(".git"); while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git if (dir.isRoot()) { qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath; } return dir; } /** * Whenever a directory is provided, change it for all the files in it but not inner directories, * that way we make sure we won't get into recursion, */ static QList preventRecursion(const QList& urls) { QList ret; foreach(const QUrl& url, urls) { QDir d(url.toLocalFile()); if(d.exists()) { QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); foreach(const QString& entry, entries) { QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry)); ret += entryUrl; } } else ret += url; } return ret; } QString toRevisionName(const KDevelop::VcsRevision& rev, QString currentRevision=QString()) { switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("^HEAD"); case VcsRevision::Base: return QString(); case VcsRevision::Working: return QString(); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); return currentRevision + "^1"; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: return rev.revisionValue().toString(); case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) { QString ret; if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval ret = toRevisionName(limit, QString()); else { QString dst = toRevisionName(limit); if(dst.isEmpty()) ret = dst; else { QString src = toRevisionName(rev, dst); if(src.isEmpty()) ret = src; else ret = src+".."+dst; } } return ret; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } QDir urlDir(const QList& urls) { return urlDir(urls.first()); } //TODO: could be improved } GitPlugin::GitPlugin( QObject *parent, const QVariantList & ) : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")), m_oldVersion(false), m_usePrefix(true) { if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) { m_hasError = true; m_errorDescription = i18n("git is not installed"); return; } KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBasicVersionControl ) KDEV_USE_EXTENSION_INTERFACE( KDevelop::IDistributedVersionControl ) KDEV_USE_EXTENSION_INTERFACE( KDevelop::IBranchingVersionControl ) m_hasError = false; setObjectName(QStringLiteral("Git")); DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent); *versionJob << "git" << "--version"; connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput); ICore::self()->runController()->registerJob(versionJob); m_watcher = new KDirWatch(this); connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged); connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged); } GitPlugin::~GitPlugin() {} bool emptyOutput(DVcsJob* job) { QScopedPointer _job(job); if(job->exec() && job->status()==VcsJob::JobSucceeded) return job->rawOutput().trimmed().isEmpty(); return false; } bool GitPlugin::hasStashes(const QDir& repository) { return !emptyOutput(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& d) { return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file) { return !emptyOutput(lsFiles(repo, QStringList() << QStringLiteral("-m") << file.path(), OutputJob::Silent)); } void GitPlugin::additionalMenuEntries(QMenu* menu, const QList& urls) { m_urls = urls; QDir dir=urlDir(urls); bool hasSt = hasStashes(dir); menu->addSeparator()->setText(i18n("Git Stashes")); menu->addAction(i18n("Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt); menu->addAction(i18n("Push Stash"), this, SLOT(ctxPushStash())); menu->addAction(i18n("Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt); } void GitPlugin::ctxPushStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxPopStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxStashManager() { QPointer d = new StashManagerDialog(urlDir(m_urls), this, 0); d->exec(); delete d; } DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } QString GitPlugin::name() const { return QStringLiteral("Git"); } QUrl GitPlugin::repositoryRoot(const QUrl& path) { return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath()); } bool GitPlugin::isValidDirectory(const QUrl & dirPath) { QDir dir=dotGitDirectory(dirPath); return dir.cd(QStringLiteral(".git")) && dir.exists(QStringLiteral("HEAD")); } bool GitPlugin::isVersionControlled(const QUrl &path) { QFileInfo fsObject(path.toLocalFile()); if (!fsObject.exists()) { return false; } if (fsObject.isDir()) { return isValidDirectory(path); } QString filename = fsObject.fileName(); QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent); return !otherFiles.empty(); } VcsJob* GitPlugin::init(const QUrl &directory) { DVcsJob* job = new DVcsJob(urlDir(directory), this); job->setType(VcsJob::Import); *job << "git" << "init"; return job; } VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode) { DVcsJob* job = new GitCloneJob(urlDir(dest), this); job->setType(VcsJob::Import); *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest; return job; } VcsJob* GitPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Add); *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } KDevelop::VcsJob* GitPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent); job->setType(VcsJob::Status); if(m_oldVersion) { *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old); } else { *job << "git" << "status" << "--porcelain"; job->setIgnoreError(true); connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput); } *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, VcsDiff::Type /*type*/, IBasicVersionControl::RecursionMode recursion) { //TODO: control different types DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Diff); *job << "git" << "diff" << "--no-color" << "--no-ext-diff"; if (!usePrefix()) { // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches // has become optional. *job << "--no-prefix"; } if(srcRevision.revisionType()==VcsRevision::Special && dstRevision.revisionType()==VcsRevision::Special && srcRevision.specialType()==VcsRevision::Base && dstRevision.specialType()==VcsRevision::Working) *job << "HEAD"; else { QString revstr = revisionInterval(srcRevision, dstRevision); if(!revstr.isEmpty()) *job << revstr; } *job << "--"; if (recursion == IBasicVersionControl::Recursive) { *job << fileOrDirectory; } else { *job << preventRecursion(QList() << fileOrDirectory); } connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput); return job; } VcsJob* GitPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { if(localLocations.isEmpty() ) return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose); QDir repo = urlDir(repositoryRoot(localLocations.first())); QString modified; for (const auto& file: localLocations) { if (hasModifications(repo, file)) { modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "
"); } } if (!modified.isEmpty()) { auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommited changes, " "which will be lost. Continue?") + "

" + modified); if (res != KMessageBox::Yes) { return errorsFound(QString(), OutputJob::Silent); } } DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Revert); *job << "git" << "checkout" << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly... //If no files specified then commit already added files VcsJob* GitPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); const QDir dir = dotGitDirectory(localLocations.front()); if (!ensureValidGitIdentity(dir)) { return errorsFound(i18n("Email or name for Git not specified")); } DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); QList files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); addNotVersionedFiles(dir, files); *job << "git" << "commit" << "-m" << message; *job << "--" << files; return job; } bool GitPlugin::ensureValidGitIdentity(const QDir& dir) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath()); const QString name = readConfigOption(url, QStringLiteral("user.name")); const QString email = readConfigOption(url, QStringLiteral("user.email")); if (!email.isEmpty() && !name.isEmpty()) { return true; // already okay } GitNameEmailDialog dialog; dialog.setName(name); dialog.setEmail(email); if (!dialog.exec()) { return false; } runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal())); runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal())); return true; } void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList& files) { QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent); QList toadd, otherFiles; foreach(const QString& file, otherStr) { QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file)); otherFiles += v; } //We add the files that are not versioned foreach(const QUrl& file, files) { if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile()) toadd += file; } if(!toadd.isEmpty()) { VcsJob* job = add(toadd); job->exec(); } } bool isEmptyDirStructure(const QDir &dir) { foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if (i.isDir()) { if (!isEmptyDirStructure(QDir(i.filePath()))) return false; } else if (i.isFile()) { return false; } } return true; } VcsJob* GitPlugin::remove(const QList& files) { if (files.isEmpty()) return errorsFound(i18n("No files to remove")); QDir dotGitDir = dotGitDirectory(files.front()); QList files_(files); QMutableListIterator i(files_); while (i.hasNext()) { QUrl file = i.next(); QFileInfo fileInfo(file.toLocalFile()); QStringList otherStr = getLsFiles(dotGitDir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << file.toLocalFile(), KDevelop::OutputJob::Silent); if(!otherStr.isEmpty()) { //remove files not under version control QList otherFiles; foreach(const QString &f, otherStr) { otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f); } if (fileInfo.isFile()) { //if it's an unversioned file we are done, don't use git rm on it i.remove(); } auto trashJob = KIO::trash(otherFiles); trashJob->exec(); qCDebug(PLUGIN_GIT) << "other files" << otherFiles; } if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(file.toLocalFile()))) { //remove empty folders, git doesn't do that auto trashJob = KIO::trash(file); trashJob->exec(); qCDebug(PLUGIN_GIT) << "empty folder, removing" << file; //we already deleted it, don't use git rm on it i.remove(); } } } if (files_.isEmpty()) return 0; DVcsJob* job = new GitJob(dotGitDir, this); job->setType(VcsJob::Remove); // git refuses to delete files with local modifications // use --force to overcome this *job << "git" << "rm" << "-r" << "--force"; *job << "--" << files_; return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString rev = revisionInterval(dst, src); if(!rev.isEmpty()) *job << rev; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) *job << revStr; if(limit>0) *job << QStringLiteral("-%1").arg(limit); *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Annotate); *job << "git" << "blame" << "--porcelain" << "-w"; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput); return job; } void GitPlugin::parseGitBlameOutput(DVcsJob *job) { QVariantList results; VcsAnnotationLine* annotation = 0; - QStringList lines = job->output().split('\n'); + const auto output = job->output(); + const auto lines = output.splitRef('\n'); bool skipNext=false; QMap definedRevisions; - for(QStringList::const_iterator it=lines.constBegin(), itEnd=lines.constEnd(); + for(QVector::const_iterator it=lines.constBegin(), itEnd=lines.constEnd(); it!=itEnd; ++it) { if(skipNext) { skipNext=false; results += qVariantFromValue(*annotation); continue; } if(it->isEmpty()) continue; - QString name = it->left(it->indexOf(' ')); - QString value = it->right(it->size()-name.size()-1); + QStringRef name = it->left(it->indexOf(' ')); + QStringRef value = it->right(it->size()-name.size()-1); if(name==QLatin1String("author")) - annotation->setAuthor(value); + annotation->setAuthor(value.toString()); else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail? else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter? else if(name==QLatin1String("author-time")) annotation->setDate(QDateTime::fromTime_t(value.toUInt())); else if(name==QLatin1String("summary")) - annotation->setCommitMessage(value); + annotation->setCommitMessage(value.toString()); else if(name.startsWith(QStringLiteral("committer"))) {} //We will just store the authors else if(name==QLatin1String("previous")) {} //We don't need that either else if(name==QLatin1String("filename")) { skipNext=true; } else if(name==QLatin1String("boundary")) { definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine()); } else { - QStringList values = value.split(' '); + const auto values = value.split(' '); VcsRevision rev; - rev.setRevisionValue(name.left(8), KDevelop::VcsRevision::GlobalNumber); + rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber); - skipNext = definedRevisions.contains(name); + skipNext = definedRevisions.contains(name.toString()); if(!skipNext) - definedRevisions.insert(name, VcsAnnotationLine()); + definedRevisions.insert(name.toString(), VcsAnnotationLine()); - annotation = &definedRevisions[name]; + annotation = &definedRevisions[name.toString()]; annotation->setLineNumber(values[1].toInt() - 1); annotation->setRevision(rev); } } job->setResults(results); } DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "ls-files" << args; return job; } DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "stash" << args; return job; } VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName) { DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "tag" << "-m" << commitMessage << tagName; if(rev.revisionValue().isValid()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch) { QDir d=urlDir(repository); if(hasModifications(d) && KMessageBox::questionYesNo(0, i18n("There are pending changes, do you want to stash them first?"))==KMessageBox::Yes) { QScopedPointer stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose)); stash->exec(); } DVcsJob* job = new DVcsJob(d, this); *job << "git" << "checkout" << branch; return job; } VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "branch" << "--" << branchName; if(!rev.prettyValue().isEmpty()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-D" << branchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-m" << newBranchName << oldBranchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::currentBranch(const QUrl& repository) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); job->setIgnoreError(true); *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } void GitPlugin::parseGitCurrentBranch(DVcsJob* job) { QString out = job->output().trimmed(); job->setResults(out); } VcsJob* GitPlugin::branches(const QUrl &repository) { DVcsJob* job=new DVcsJob(urlDir(repository)); *job << "git" << "branch" << "-a"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput); return job; } void GitPlugin::parseGitBranchOutput(DVcsJob* job) { - QStringList branchListDirty = job->output().split('\n', QString::SkipEmptyParts); + const auto output = job->output(); + const auto branchListDirty = output.splitRef('\n', QString::SkipEmptyParts); QStringList branchList; - foreach(QString branch, branchListDirty) + foreach(const auto & branch, branchListDirty) { // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master"); // "git rev-list" chokes on these entries and we do not need duplicate branches altogether. if (branch.contains(QStringLiteral("->"))) continue; // Skip entries such as '(no branch)' if (branch.contains(QStringLiteral("(no branch)"))) continue; - if (branch.startsWith('*')) - branch = branch.right(branch.size()-2); + QStringRef name = branch; + if (name.startsWith('*')) + name = branch.right(branch.size()-2); - branchList<setResults(branchList); } /* Few words about how this hardcore works: 1. get all commits (with --paretns) 2. select master (root) branch and get all unicial commits for branches (git-rev-list br2 ^master ^br3) 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS, MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count) another setType(INITIAL) is used for "bottom/root/first" commits of branches 4. find and set merges, HEADS. It's an ittaration through all commits. - first we check if parent is from the same branch, if no then we go through all commits searching parent's index and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met) - then we check branchesShas[i][0] to mark heads 4 can be a seporate function. TODO: All this porn require refactoring (rewriting is better)! It's a very dirty implementation. FIXME: 1. HEAD which is head has extra line to connect it with further commit 2. If you menrge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be extra merge rectangle in master. If there are no extra commits in branch2, but there are another branches, then the place for branch2 will be empty (instead of be used for branch3). 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly */ QList GitPlugin::getAllCommits(const QString &repo) { initBranchHash(repo); QStringList args; args << QStringLiteral("--all") << QStringLiteral("--pretty") << QStringLiteral("--parents"); QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); static QRegExp rx_com("commit \\w{40,40}"); QListcommitList; DVcsEvent item; //used to keep where we have empty/cross/branch entry //true if it's an active branch (then cross or branch) and false if not QVector additionalFlags(branchesShas.count()); additionalFlags.fill(false); //parse output for(int i = 0; i < commits.count(); ++i) { if (commits[i].contains(rx_com)) { qCDebug(PLUGIN_GIT) << "commit found in " << commits[i]; item.setCommit(commits[i].section(' ', 1, 1).trimmed()); // qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1); QStringList parents; QString parent = commits[i].section(' ', 2); int section = 2; while (!parent.isEmpty()) { /* qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/ parents.append(parent.trimmed()); section++; parent = commits[i].section(' ', section); } item.setParents(parents); //Avoid Merge string while (!commits[i].contains(QStringLiteral("Author: "))) ++i; item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1); item.setDate(commits[++i].section(QStringLiteral("Date: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date: ", 1); QString log; i++; //next line! while (i < commits.count() && !commits[i].contains(rx_com)) log += commits[i++]; --i; //while took commit line item.setLog(log.trimmed()); // qCDebug(PLUGIN_GIT) << "log is: " << log; //mask is used in CommitViewDelegate to understand what we should draw for each branch QList mask; //set mask (properties for each graph column in row) for(int i = 0; i < branchesShas.count(); ++i) { qCDebug(PLUGIN_GIT)<<"commit: " << item.getCommit(); if (branchesShas[i].contains(item.getCommit())) { mask.append(item.getType()); //we set type in setParents //check if parent from the same branch, if not then we have found a root of the branch //and will use empty column for all futher (from top to bottom) revisions //FIXME: we should set CROSS between parent and child (and do it when find merge point) additionalFlags[i] = false; foreach(const QString &sha, item.getParents()) { if (branchesShas[i].contains(sha)) additionalFlags[i] = true; } if (additionalFlags[i] == false) item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing } else { if (additionalFlags[i] == false) mask.append(DVcsEvent::EMPTY); else mask.append(DVcsEvent::CROSS); } qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i]; } item.setProperties(mask); commitList.append(item); } } //find and set merges, HEADS, require refactoring! for(QList::iterator iter = commitList.begin(); iter != commitList.end(); ++iter) { QStringList parents = iter->getParents(); //we need only only child branches if (parents.count() != 1) break; QString parent = parents[0]; QString commit = iter->getCommit(); bool parent_checked = false; int heads_checked = 0; for(int i = 0; i < branchesShas.count(); ++i) { //check parent if (branchesShas[i].contains(commit)) { if (!branchesShas[i].contains(parent)) { //parent and child are not in same branch //since it is list, than parent has i+1 index //set CROSS and HCROSS for(QList::iterator f_iter = iter; f_iter != commitList.end(); ++f_iter) { if (parent == f_iter->getCommit()) { for(int j = 0; j < i; ++j) { if(branchesShas[j].contains(parent)) f_iter->setPropetry(j, DVcsEvent::MERGE); else f_iter->setPropetry(j, DVcsEvent::HCROSS); } f_iter->setType(DVcsEvent::MERGE); f_iter->setPropetry(i, DVcsEvent::MERGE_RIGHT); qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit; qCDebug(PLUGIN_GIT) << f_iter->getCommit() << " is merge"; parent_checked = true; break; } else f_iter->setPropetry(i, DVcsEvent::CROSS); } } } //mark HEADs if (!branchesShas[i].empty() && commit == branchesShas[i][0]) { iter->setType(DVcsEvent::HEAD); iter->setPropetry(i, DVcsEvent::HEAD); heads_checked++; qCDebug(PLUGIN_GIT) << "HEAD found"; } //some optimization if (heads_checked == branchesShas.count() && parent_checked) break; } } return commitList; } void GitPlugin::initBranchHash(const QString &repo) { const QUrl repoUrl = QUrl::fromLocalFile(repo); QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList(); qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches; //Now root branch is the current branch. In future it should be the longest branch //other commitLists are got with git-rev-lits branch ^br1 ^ br2 QString root = runSynchronously(currentBranch(repoUrl)).toString(); QScopedPointer job(gitRevList(repo, QStringList(root))); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); foreach(const QString &branch, gitBranches) { if (branch == root) continue; QStringList args(branch); foreach(const QString &branch_arg, gitBranches) { if (branch_arg != branch) //man gitRevList for '^' args<<'^' + branch_arg; } QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); } } //Actually we can just copy the output without parsing. So it's a kind of draft for future void GitPlugin::parseLogOutput(const DVcsJob * job, QList& commits) const { // static QRegExp rx_sep( "[-=]+" ); // static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" ); - static QRegExp rx_com( "commit \\w{1,40}" ); + static QRegularExpression rx_com( "commit \\w{1,40}" ); - QStringList lines = job->output().split('\n', QString::SkipEmptyParts); + const auto output = job->output(); + const auto lines = output.splitRef('\n', QString::SkipEmptyParts); DVcsEvent item; QString commitLog; for (int i=0; i commits; QString contents = job->output(); // check if git-log returned anything if (contents.isEmpty()) { job->setResults(commits); // empty list return; } // start parsing the output QTextStream s(&contents); VcsEvent item; QString message; bool pushCommit = false; while (!s.atEnd()) { QString line = s.readLine(); if (commitRegex.exactMatch(line)) { if (pushCommit) { item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); item.setItems(QList()); } else { pushCommit = true; } VcsRevision rev; rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber); item.setRevision(rev); message.clear(); } else if (infoRegex.exactMatch(line)) { QString cap1 = infoRegex.cap(1); if (cap1 == QLatin1String("Author")) { item.setAuthor(infoRegex.cap(2).trimmed()); } else if (cap1 == QLatin1String("Date")) { item.setDate(QDateTime::fromTime_t(infoRegex.cap(2).trimmed().split(' ')[0].toUInt())); } } else if (modificationsRegex.exactMatch(line)) { VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1()); QString filenameA = modificationsRegex.cap(2); VcsItemEvent itemEvent; itemEvent.setActions(a); itemEvent.setRepositoryLocation(filenameA); if(a==VcsItemEvent::Replaced) { QString filenameB = modificationsRegex.cap(3); itemEvent.setRepositoryCopySourceLocation(filenameB); } item.addItem(itemEvent); } else if (line.startsWith(QLatin1String(" "))) { message += line.remove(0, 4); message += '\n'; } } item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); job->setResults(commits); } void GitPlugin::parseGitDiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath()))); diff.setDepth(usePrefix()? 1 : 0); job->setResults(qVariantFromValue(diff)); } static VcsStatusInfo::State lsfilesToState(char id) { switch(id) { case 'H': return VcsStatusInfo::ItemUpToDate; //Cached case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted case 'C': return VcsStatusInfo::ItemModified; //modified/changed case 'K': return VcsStatusInfo::ItemDeleted; //to be killed case '?': return VcsStatusInfo::ItemUnknown; //other } Q_ASSERT(false); return VcsStatusInfo::ItemUnknown; } void GitPlugin::parseGitStatusOutput_old(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QDir dir = job->directory(); QMap allStatus; foreach(const QString& line, outputLines) { VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1()); QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.right(line.size()-2))); allStatus[url] = status; } QVariantList statuses; QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd(); for(; it!=itEnd; ++it) { VcsStatusInfo status; status.setUrl(it.key()); status.setState(it.value()); statuses.append(qVariantFromValue(status)); } job->setResults(statuses); } void GitPlugin::parseGitStatusOutput(DVcsJob* job) { - QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); + const auto output = job->output(); + const auto outputLines = output.splitRef('\n', QString::SkipEmptyParts); QDir workingDir = job->directory(); QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); QVariantList statuses; QList processedFiles; - foreach(const QString& line, outputLines) { + foreach(const QStringRef& line, outputLines) { //every line is 2 chars for the status, 1 space then the file desc - QString curr=line.right(line.size()-3); - QString state = line.left(2); + QStringRef curr=line.right(line.size()-3); + QStringRef state = line.left(2); int arrow = curr.indexOf(QStringLiteral(" -> ")); if(arrow>=0) { VcsStatusInfo status; - status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.left(arrow)))); + status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow)))); status.setState(VcsStatusInfo::ItemDeleted); statuses.append(qVariantFromValue(status)); processedFiles += status.url(); curr = curr.mid(arrow+4); } if(curr.startsWith('\"') && curr.endsWith('\"')) { //if the path is quoted, unquote curr = curr.mid(1, curr.size()-2); } VcsStatusInfo status; - status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr))); + status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString()))); + qDebug() << "lalala" << line << state; status.setState(messageToState(state)); processedFiles.append(status.url()); - qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << messageToState(state); + qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state(); statuses.append(qVariantFromValue(status)); } QStringList paths; QStringList oldcmd=job->dvcsCommand(); QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd(); for(; it!=itEnd; ++it) paths += *it; //here we add the already up to date files QStringList files = getLsFiles(job->directory(), QStringList() << QStringLiteral("-c") << QStringLiteral("--") << paths, OutputJob::Silent); foreach(const QString& file, files) { QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file)); if(!processedFiles.contains(fileUrl)) { VcsStatusInfo status; status.setUrl(fileUrl); status.setState(VcsStatusInfo::ItemUpToDate); statuses.append(qVariantFromValue(status)); } } job->setResults(statuses); } void GitPlugin::parseGitVersionOutput(DVcsJob* job) { - QStringList versionString = job->output().trimmed().split(' ').last().split('.'); + const auto output = job->output().trimmed(); + auto versionString = output.midRef(output.lastIndexOf(' ')).split('.'); static const QList minimumVersion = QList() << 1 << 7; qCDebug(PLUGIN_GIT) << "checking git version" << versionString << "against" << minimumVersion; m_oldVersion = false; if (versionString.size() < minimumVersion.size()) { m_oldVersion = true; qCWarning(PLUGIN_GIT) << "invalid git version string:" << job->output().trimmed(); return; } foreach(int num, minimumVersion) { - QString curr = versionString.takeFirst(); + QStringRef curr = versionString.takeFirst(); int valcurr = curr.toInt(); if (valcurr < num) { m_oldVersion = true; break; } if (valcurr > num) { m_oldVersion = false; break; } } qCDebug(PLUGIN_GIT) << "the current git version is old: " << m_oldVersion; } QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(lsFiles(directory, args, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) return job->output().split('\n', QString::SkipEmptyParts); return QStringList(); } DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(QDir(repository), this, verbosity); *job << "git" << "rev-parse" << args; return job; } DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args) { DVcsJob* job = new DVcsJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent); { *job << "git" << "rev-list" << args; return job; } } -VcsStatusInfo::State GitPlugin::messageToState(const QString& msg) +VcsStatusInfo::State GitPlugin::messageToState(const QStringRef& msg) { Q_ASSERT(msg.size()==1 || msg.size()==2); VcsStatusInfo::State ret = VcsStatusInfo::ItemUnknown; if(msg.contains('U') || msg == QLatin1String("AA") || msg == QLatin1String("DD")) ret = VcsStatusInfo::ItemHasConflicts; - else switch(msg[0].toLatin1()) + else switch(msg.at(0).toLatin1()) { case 'M': ret = VcsStatusInfo::ItemModified; break; case 'A': ret = VcsStatusInfo::ItemAdded; break; case 'R': ret = VcsStatusInfo::ItemModified; break; case 'C': ret = VcsStatusInfo::ItemHasConflicts; break; case ' ': - ret = msg[1] == 'M' ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemDeleted; + ret = msg.at(1) == 'M' ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemDeleted; break; case 'D': ret = VcsStatusInfo::ItemDeleted; break; case '?': ret = VcsStatusInfo::ItemUnknown; break; default: qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg; break; } return ret; } StandardJob::StandardJob(IPlugin* parent, KJob* job, OutputJob::OutputJobVerbosity verbosity) : VcsJob(parent, verbosity) , m_job(job) , m_plugin(parent) , m_status(JobNotStarted) {} void StandardJob::start() { connect(m_job, &KJob::result, this, &StandardJob::result); m_job->start(); m_status=JobRunning; } void StandardJob::result(KJob* job) { m_status=job->error() == 0? JobSucceeded : JobFailed; emitResult(); } VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { //TODO: Probably we should "git add" after return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent); } VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination) { QDir dir = urlDir(source); QFileInfo fileInfo(source.toLocalFile()); if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(source.toLocalFile()))) { //move empty folder, git doesn't do that qCDebug(PLUGIN_GIT) << "empty folder" << source; return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << source.toLocalFile(), KDevelop::OutputJob::Silent); if(otherStr.isEmpty()) { DVcsJob* job = new DVcsJob(dir, this, KDevelop::OutputJob::Verbose); *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile(); return job; } else { return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job) { job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output()))); } VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(urlDir(localLocation), this); //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "config" << "remote.origin.url"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput); return job; } VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "pull"; if(!localOrRepoLocationSrc.localUrl().isEmpty()) *job << localOrRepoLocationSrc.localUrl().url(); return job; } VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "push"; if(!localOrRepoLocationDst.localUrl().isEmpty()) *job << localOrRepoLocationDst.localUrl().url(); return job; } VcsJob* GitPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); } VcsJob* GitPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Head) { return pull(VcsLocation(), localLocations.first()); } else { DVcsJob* job = new DVcsJob(urlDir(localLocations.first()), this); { //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "checkout" << rev.revisionValue().toString() << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } } } void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const { new GitMessageHighlighter(editor); QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG"))); // Some limit on the file size should be set since whole content is going to be read into // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit // message. static const qint64 maxMergeMsgFileSize = 1024*1024; if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly)) return; QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize)); editor->setPlainText(mergeMsg); } class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget { Q_OBJECT public: GitVcsLocationWidget(QWidget* parent = 0, Qt::WindowFlags f = 0) : StandardVcsLocationWidget(parent, f) {} bool isCorrect() const override { return !url().isEmpty(); } }; KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const { return new GitVcsLocationWidget(parent); } bool GitPlugin::hasError() const { return m_hasError; } QString GitPlugin::errorDescription() const { return m_errorDescription; } void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository) { QDir dir = dotGitDirectory(repository); QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD")); m_watcher->addFile(headFile); } void GitPlugin::fileChanged(const QString& file) { Q_ASSERT(file.endsWith(QStringLiteral("HEAD"))); //SMTH/.git/HEAD -> SMTH/ const QUrl fileUrl = Path(file).parent().parent().toUrl(); //We need to delay the emitted signal, otherwise the branch hasn't change yet //and the repository is not functional m_branchesChange.append(fileUrl); QTimer::singleShot(1000, this, SLOT(delayedBranchChanged())); } void GitPlugin::delayedBranchChanged() { emit repositoryBranchChanged(m_branchesChange.takeFirst()); } CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document) { CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path()); job->start(); return job; } DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) { auto job = new DVcsJob(urlDir(repository), this); QStringList args; args << "git" << "config"; if(global) args << "--global"; args << key << value; *job << args; return job; } QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key) { QProcess exec; exec.setWorkingDirectory(urlDir(repository).absolutePath()); exec.start("git", QStringList() << "config" << "--get" << key); exec.waitForFinished(); return exec.readAllStandardOutput().trimmed(); } #include "gitplugin.moc" diff --git a/plugins/git/gitplugin.h b/plugins/git/gitplugin.h index 1530ba82fd..f89aa0073e 100644 --- a/plugins/git/gitplugin.h +++ b/plugins/git/gitplugin.h @@ -1,227 +1,227 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * * * 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 KDEVPLATFORM_PLUGIN_GIT_PLUGIN_H #define KDEVPLATFORM_PLUGIN_GIT_PLUGIN_H #include #include #include #include #include #include #include #include class KDirWatch; class QDir; namespace KDevelop { class VcsJob; class VcsRevision; } class StandardJob : public KDevelop::VcsJob { Q_OBJECT public: StandardJob(KDevelop::IPlugin* parent, KJob* job, OutputJobVerbosity verbosity); QVariant fetchResults() override { return QVariant(); } void start() override; JobStatus status() const override { return m_status; } KDevelop::IPlugin* vcsPlugin() const override { return m_plugin; } public slots: void result(KJob*); private: KJob* m_job; KDevelop::IPlugin* m_plugin; JobStatus m_status; }; /** * This is the main class of KDevelop's Git plugin. * * It implements the DVCS dependent things not implemented in KDevelop::DistributedVersionControlPlugin * @author Evgeniy Ivanov */ class GitPlugin: public KDevelop::DistributedVersionControlPlugin, public KDevelop::IContentAwareVersionControl { Q_OBJECT Q_INTERFACES(KDevelop::IBasicVersionControl KDevelop::IDistributedVersionControl KDevelop::IContentAwareVersionControl) friend class GitInitTest; public: explicit GitPlugin(QObject *parent, const QVariantList & args = QVariantList() ); ~GitPlugin() override; QString name() const override; bool isVersionControlled(const QUrl &path) override; KDevelop::VcsJob* copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) override; KDevelop::VcsJob* move(const QUrl& localLocationSrc, const QUrl& localLocationDst) override; //TODO KDevelop::VcsJob* pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) override; KDevelop::VcsJob* push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) override; KDevelop::VcsJob* repositoryLocation(const QUrl& localLocation) override; KDevelop::VcsJob* resolve(const QList& localLocations, RecursionMode recursion) override; KDevelop::VcsJob* update(const QList& localLocations, const KDevelop::VcsRevision& rev, RecursionMode recursion) override; KDevelop::VcsLocationWidget* vcsLocation(QWidget* parent) const override; void setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const override; //End of KDevelop::VcsJob* add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* createWorkingCopy(const KDevelop::VcsLocation & localOrRepoLocationSrc, const QUrl& localRepositoryRoot, KDevelop::IBasicVersionControl::RecursionMode) override; KDevelop::VcsJob* remove(const QList& files) override; KDevelop::VcsJob* status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion = KDevelop::IBasicVersionControl::Recursive) override; KDevelop::VcsJob* diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::VcsDiff::Type, RecursionMode recursion) override; KDevelop::VcsJob* log( const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long limit) override; KDevelop::VcsJob* log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) override; KDevelop::VcsJob* annotate(const QUrl &localLocation, const KDevelop::VcsRevision &rev) override; KDevelop::VcsJob* revert(const QList& localLocations, RecursionMode recursion) override; // Begin: KDevelop::IDistributedVersionControl KDevelop::VcsJob* init(const QUrl & directory) override; // Branch management KDevelop::VcsJob* tag(const QUrl& repository, const QString& commitMessage, const KDevelop::VcsRevision& rev, const QString& tagName) override; KDevelop::VcsJob* branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) override; KDevelop::VcsJob* branches(const QUrl& repository) override; KDevelop::VcsJob* currentBranch(const QUrl& repository) override; KDevelop::VcsJob* deleteBranch(const QUrl& repository, const QString& branchName) override; KDevelop::VcsJob* switchBranch(const QUrl& repository, const QString& branchName) override; KDevelop::VcsJob* renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) override; //graph helpers QList getAllCommits(const QString &repo) override; //used in log void parseLogOutput(const KDevelop::DVcsJob * job, QList& commits) const override; void additionalMenuEntries(QMenu* menu, const QList& urls) override; KDevelop::DVcsJob* gitStash(const QDir& repository, const QStringList& args, KDevelop::OutputJob::OutputJobVerbosity verbosity); bool hasStashes(const QDir& repository); bool hasModifications(const QDir& repository); bool hasModifications(const QDir& repo, const QUrl& file); bool hasError() const override; QString errorDescription() const override; void registerRepositoryForCurrentBranchChanges(const QUrl& repository) override; KDevelop::CheckInRepositoryJob* isInRepository(KTextEditor::Document* document) override; KDevelop::DVcsJob* setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global = false); QString readConfigOption(const QUrl& repository, const QString& key); // this indicates whether the diff() function will generate a diff (patch) which // includes the working copy directory name or not (in which case git diff is called // with --no-prefix). bool usePrefix() const { return m_usePrefix; } void setUsePrefix(bool p) { m_usePrefix = p; } protected: QUrl repositoryRoot(const QUrl& path); bool isValidDirectory(const QUrl &dirPath) override; KDevelop::DVcsJob* lsFiles(const QDir &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Verbose); KDevelop::DVcsJob* gitRevList(const QString &directory, const QStringList &args); KDevelop::DVcsJob* gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity = KDevelop::OutputJob::Silent); private slots: void parseGitBlameOutput(KDevelop::DVcsJob *job); void parseGitLogOutput(KDevelop::DVcsJob *job); void parseGitDiffOutput(KDevelop::DVcsJob* job); void parseGitRepoLocationOutput(KDevelop::DVcsJob* job); void parseGitStatusOutput(KDevelop::DVcsJob* job); void parseGitStatusOutput_old(KDevelop::DVcsJob* job); void parseGitVersionOutput(KDevelop::DVcsJob* job); void parseGitBranchOutput(KDevelop::DVcsJob* job); void parseGitCurrentBranch(KDevelop::DVcsJob* job); void ctxPushStash(); void ctxPopStash(); void ctxStashManager(); void fileChanged(const QString& file); void delayedBranchChanged(); signals: void repositoryBranchChanged(const QUrl& repository); private: bool ensureValidGitIdentity(const QDir& dir); void addNotVersionedFiles(const QDir& dir, const QList& files); //commit dialog "main" helper QStringList getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity); KDevelop::DVcsJob* errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity); void initBranchHash(const QString &repo); - static KDevelop::VcsStatusInfo::State messageToState(const QString& ch); + static KDevelop::VcsStatusInfo::State messageToState(const QStringRef& ch); QList branchesShas; QList m_urls; /** Tells if it's older than 1.7.0 or not */ bool m_oldVersion; bool m_hasError; QString m_errorDescription; KDirWatch* m_watcher; QList m_branchesChange; bool m_usePrefix; }; QVariant runSynchronously(KDevelop::VcsJob* job); #endif diff --git a/plugins/patchreview/patchreview.cpp b/plugins/patchreview/patchreview.cpp index 3e8405c83c..a6f4c460cd 100644 --- a/plugins/patchreview/patchreview.cpp +++ b/plugins/patchreview/patchreview.cpp @@ -1,537 +1,538 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "patchreview.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught #define CATCHLIBDIFF /* Exclude this file from doublequote_chars check as krazy doesn't understand std::string*/ //krazy:excludeall=doublequote_chars #include #include #include #include #include #include "patchhighlighter.h" #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "debug.h" Q_LOGGING_CATEGORY(PLUGIN_PATCHREVIEW, "kdevplatform.plugins.patchreview") using namespace KDevelop; namespace { // Maximum number of files to open directly within a tab when the review is started const int maximumFilesToOpenDirectly = 15; } Q_DECLARE_METATYPE( const Diff2::DiffModel* ) void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) { try { qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty(); if ( !m_modelList ) throw "no model"; for ( int a = 0; a < m_modelList->modelCount(); ++a ) { const Diff2::DiffModel* model = m_modelList->modelAt( a ); if ( !model || !model->differences() ) continue; QUrl file = urlForFileModel( model ); if ( !fileName.isEmpty() && fileName != file ) continue; IDocument* doc = ICore::self()->documentController()->documentForUrl( file ); if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) { if ( doc->textDocument() ) { const QList ranges = m_highlighters[doc->url()]->ranges(); KTextEditor::View * v = doc->activeTextView(); int bestLine = -1; if ( v ) { KTextEditor::Cursor c = v->cursorPosition(); for ( QList::const_iterator it = ranges.begin(); it != ranges.end(); ++it ) { int line = ( *it )->start().line(); if ( forwards ) { if ( line > c.line() && ( bestLine == -1 || line < bestLine ) ) bestLine = line; } else { if ( line < c.line() && ( bestLine == -1 || line > bestLine ) ) bestLine = line; } } if ( bestLine != -1 ) { v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) ); return; } else if(fileName.isEmpty()) { int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1); ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next))); } } } } } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str; } qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found"; } void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); if ( file != highlightFile ) continue; qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString(); IDocument* doc = document; if( !doc ) doc = ICore::self()->documentController()->documentForUrl( file ); qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc; if ( !doc || !doc->textDocument() ) continue; removeHighlighting( file ); m_highlighters[file] = new PatchHighlighter( model, doc, this ); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::highlightPatch() { try { if ( !modelList() ) throw "no model"; for ( int a = 0; a < modelList()->modelCount(); ++a ) { const Diff2::DiffModel* model = modelList()->modelAt( a ); if ( !model ) continue; QUrl file = urlForFileModel( model ); addHighlighting( file ); } } catch ( const QString & str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } catch ( const char * str ) { qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str; } } void PatchReviewPlugin::removeHighlighting( const QUrl& file ) { if ( file.isEmpty() ) { ///Remove all highlighting qDeleteAll( m_highlighters ); m_highlighters.clear(); } else { HighlightMap::iterator it = m_highlighters.find( file ); if ( it != m_highlighters.end() ) { delete *it; m_highlighters.erase( it ); } } } void PatchReviewPlugin::notifyPatchChanged() { if (m_patch) { qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file(); m_updateKompareTimer->start( 500 ); } else { m_updateKompareTimer->stop(); } } void PatchReviewPlugin::forceUpdate() { if( m_patch ) { m_patch->update(); notifyPatchChanged(); } } void PatchReviewPlugin::updateKompareModel() { if ( !m_patch ) { ///TODO: this method should be cleaned up, it can be called by the timer and /// e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could /// lead to asserts before... return; } qCDebug(PLUGIN_PATCHREVIEW) << "updating model"; removeHighlighting(); m_modelList.reset( nullptr ); delete m_diffSettings; { IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() ); if( patchDoc ) patchDoc->reload(); } QString patchFile; if( m_patch->file().isLocalFile() ) patchFile = m_patch->file().toLocalFile(); else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) { patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation); bool ret = KIO::copy( m_patch->file(), QUrl::fromLocalFile(patchFile) )->exec(); if( !ret ) { qWarning() << "Problem while downloading: " << m_patch->file() << "to" << patchFile; patchFile.clear(); } } if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load try { m_diffSettings = new DiffSettings( 0 ); m_kompareInfo.reset( new Kompare::Info() ); m_kompareInfo->localDestination = patchFile; m_kompareInfo->localSource = m_patch->baseDir().toLocalFile(); m_kompareInfo->depth = m_patch->depth(); m_kompareInfo->applied = m_patch->isAlreadyApplied(); m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) ); m_modelList->slotKompareInfo( m_kompareInfo.data() ); try { m_modelList->openDirAndDiff(); } catch ( const QString & str ) { throw; } catch ( ... ) { throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." ); } emit patchChanged(); for( int i = 0; i < m_modelList->modelCount(); i++ ) { const Diff2::DiffModel* model = m_modelList->modelAt( i ); for( int j = 0; j < model->differences()->count(); j++ ) { model->differences()->at( j )->apply( m_patch->isAlreadyApplied() ); } } highlightPatch(); return; } catch ( const QString & str ) { KMessageBox::error( 0, str, i18n( "Kompare Model Update" ) ); } catch ( const char * str ) { KMessageBox::error( 0, str, i18n( "Kompare Model Update" ) ); } removeHighlighting(); m_modelList.reset( nullptr ); m_kompareInfo.reset( nullptr ); delete m_diffSettings; emit patchChanged(); } K_PLUGIN_FACTORY_WITH_JSON(KDevProblemReporterFactory, "kdevpatchreview.json", registerPlugin();) class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory { public: PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {} QWidget* create( QWidget *parent = 0 ) override { return new PatchReviewToolView( parent, m_plugin ); } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.PatchReview"); } private: PatchReviewPlugin *m_plugin; }; PatchReviewPlugin::~PatchReviewPlugin() { removeHighlighting(); // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829 // and http://qt-project.org/forums/viewthread/38406/#162801 // modified tweak: use setPatch() and deleteLater in that method. setPatch(0); } void PatchReviewPlugin::clearPatch( QObject* _patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "clearing patch" << _patch << "current:" << ( QObject* )m_patch; IPatchSource::Ptr patch( ( IPatchSource* )_patch ); if( patch == m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "is current patch"; setPatch( IPatchSource::Ptr( new LocalPatchSource ) ); } } void PatchReviewPlugin::closeReview() { if( m_patch ) { removeHighlighting(); m_modelList.reset( 0 ); if( !dynamic_cast( m_patch.data() ) ) { // make sure "show" button still openes the file dialog to open a custom patch file setPatch( new LocalPatchSource ); } else emit patchChanged(); Sublime::Area* area = ICore::self()->uiController()->activeArea(); if( area->objectName() == QLatin1String("review") ) { if( ICore::self()->documentController()->saveAllDocuments() ) ICore::self()->uiController()->switchToArea( QStringLiteral("code"), KDevelop::IUiController::ThisWindow ); } } } void PatchReviewPlugin::cancelReview() { if( m_patch ) { m_patch->cancelReview(); closeReview(); } } void PatchReviewPlugin::finishReview( QList selection ) { if( m_patch && m_patch->finishReview( selection ) ) { closeReview(); } } void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) { Q_UNUSED( mode ); emit startingNewReview(); setPatch( patch ); QMetaObject::invokeMethod( this, "updateReview", Qt::QueuedConnection ); } void PatchReviewPlugin::switchToEmptyReviewArea() { foreach(Sublime::Area* area, ICore::self()->uiController()->allAreas()) { if (area->objectName() == QLatin1String("review")) { emit area->clearDocuments(); } } if ( ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("review") ) ICore::self()->uiController()->switchToArea( QStringLiteral("review"), KDevelop::IUiController::ThisWindow ); } QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model ) { QUrl file = m_patch->baseDir(); - file.setPath(file.toLocalFile() + '/' + model->destinationPath() + '/' + model->destinationFile()); + file.setPath(QDir::cleanPath(file.toLocalFile() + '/' + model->destinationPath() + '/' + model->destinationFile())); return file; } void PatchReviewPlugin::updateReview() { if( !m_patch ) return; m_updateKompareTimer->stop(); switchToEmptyReviewArea(); IDocument* futureActiveDoc = ICore::self()->documentController()->openDocument( m_patch->file() ); updateKompareModel(); if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) { // might happen if e.g. openDocument dialog was cancelled by user // or under the theoretic possibility of a non-text document getting opened return; } futureActiveDoc->textDocument()->setReadWrite( false ); futureActiveDoc->setPrettyName( i18n( "Overview" ) ); IDocument* buddyDoc = futureActiveDoc; KTextEditor::ModificationInterface* modif = dynamic_cast( futureActiveDoc->textDocument() ); modif->setModifiedOnDiskWarning( false ); if( m_modelList->modelCount() < maximumFilesToOpenDirectly ) { //Open all relates files for( int a = 0; a < m_modelList->modelCount(); ++a ) { QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) ); if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") ) { buddyDoc = ICore::self()->documentController()->openDocument( absoluteUrl, KTextEditor::Range::invalid(), IDocumentController::DoNotActivate, QLatin1String(""), buddyDoc ); seekHunk( true, absoluteUrl ); //Jump to the first changed position }else{ // Maybe the file was deleted qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist"; } } } Q_ASSERT( futureActiveDoc ); ICore::self()->documentController()->activateDocument( futureActiveDoc ); bool b = ICore::self()->uiController()->findToolView( i18n( "Patch Review" ), m_factory ); Q_ASSERT( b ); Q_UNUSED( b ); } void PatchReviewPlugin::setPatch( IPatchSource* patch ) { if ( patch == m_patch ) { return; } if( m_patch ) { disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); if ( qobject_cast( m_patch ) ) { // make sure we don't leak this // TODO: what about other patch sources? m_patch->deleteLater(); } } m_patch = patch; if( m_patch ) { qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir(); connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged ); } QString finishText = i18n( "Finish Review" ); if( m_patch && !m_patch->finishReviewCustomText().isEmpty() ) finishText = m_patch->finishReviewCustomText(); m_finishReview->setText( finishText ); m_finishReview->setEnabled( patch ); notifyPatchChanged(); } PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & ) : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ), m_patch( 0 ), m_factory( new PatchReviewToolViewFactory( this ) ) { KDEV_USE_EXTENSION_INTERFACE( KDevelop::IPatchReview ) qRegisterMetaType( "const Diff2::DiffModel*" ); setXMLFile( QStringLiteral("kdevpatchreview.rc") ); connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed ); connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated ); connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved ); m_updateKompareTimer = new QTimer( this ); m_updateKompareTimer->setSingleShot( true ); connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel ); m_finishReview = new QAction(i18n("Finish Review"), this); m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) ); actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return ); actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview); foreach(Sublime::Area* area, ICore::self()->uiController()->allAreas()) { if (area->objectName() == QLatin1String("review")) area->addAction(m_finishReview); } core()->uiController()->addToolView( i18n( "Patch Review" ), m_factory, IUiController::None ); areaChanged(ICore::self()->uiController()->activeArea()); } void PatchReviewPlugin::documentClosed( IDocument* doc ) { removeHighlighting( doc->url() ); } void PatchReviewPlugin::documentSaved( IDocument* doc ) { // Only update if the url is not the patch-file, because our call to // the reload() KTextEditor function also causes this signal, // which would lead to an endless update loop. if( m_patch && doc->url() != m_patch->file() ) forceUpdate(); } void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) { if (m_patch) { addHighlighting( doc->url(), doc ); } } void PatchReviewPlugin::unload() { core()->uiController()->removeToolView( m_factory ); KDevelop::IPlugin::unload(); } void PatchReviewPlugin::areaChanged(Sublime::Area* area) { bool reviewing = area->objectName() == QLatin1String("review"); m_finishReview->setEnabled(reviewing); if(!reviewing) { closeReview(); } } #include "patchreview.moc" // kate: space-indent on; indent-width 4; tab-width 4; replace-tabs on diff --git a/plugins/patchreview/patchreviewtoolview.cpp b/plugins/patchreview/patchreviewtoolview.cpp index 010b329605..27852a8660 100644 --- a/plugins/patchreview/patchreviewtoolview.cpp +++ b/plugins/patchreview/patchreviewtoolview.cpp @@ -1,575 +1,575 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "patchreview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_PURPOSE #include #include #endif using namespace KDevelop; class PatchFilesModel : public VcsFileChangesModel { Q_OBJECT public: PatchFilesModel( QObject *parent, bool allowSelection ) : VcsFileChangesModel( parent, allowSelection ) { }; enum ItemRoles { HunksNumberRole = LastItemRole+1 }; public slots: void updateState( const KDevelop::VcsStatusInfo &status, unsigned hunksNum ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( item, hunksNum ); item->setData( QVariant( hunksNum ), HunksNumberRole ); } void updateState( const KDevelop::VcsStatusInfo &status ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( invisibleRootItem()->child( row, 0 ), item->data( HunksNumberRole ).toUInt() ); } private: void setFileInfo( QStandardItem *item, unsigned int hunksNum ) { const auto url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); const QString path = ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain); const QString newText = i18ncp( "%1: number of changed hunks, %2: file name", "%2 (1 hunk)", "%2 (%1 hunks)", hunksNum, path); item->setText( newText ); } }; PatchReviewToolView::PatchReviewToolView( QWidget* parent, PatchReviewPlugin* plugin ) : QWidget( parent ), m_resetCheckedUrls( true ), m_plugin( plugin ) { connect( m_plugin->finishReviewAction(), &QAction::triggered, this, &PatchReviewToolView::finishReview ); connect( plugin, &PatchReviewPlugin::patchChanged, this, &PatchReviewToolView::patchChanged ); connect( plugin, &PatchReviewPlugin::startingNewReview, this, &PatchReviewToolView::startingNewReview ); connect( ICore::self()->documentController(), &IDocumentController::documentActivated, this, &PatchReviewToolView::documentActivated ); Sublime::MainWindow* w = dynamic_cast( ICore::self()->uiController()->activeMainWindow() ); connect(w, &Sublime::MainWindow::areaChanged, m_plugin, &PatchReviewPlugin::areaChanged); showEditDialog(); patchChanged(); } void PatchReviewToolView::resizeEvent(QResizeEvent* ev) { bool vertical = (width() < height()); m_editPatch.buttonsLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.contentLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.buttonsSpacer->changeSize(vertical ? 0 : 40, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); QWidget::resizeEvent(ev); if(m_customWidget) { m_editPatch.contentLayout->removeWidget( m_customWidget ); m_editPatch.contentLayout->insertWidget(0, m_customWidget ); } } void PatchReviewToolView::startingNewReview() { m_resetCheckedUrls = true; } void PatchReviewToolView::patchChanged() { fillEditFromPatch(); kompareModelChanged(); #ifdef WITH_PURPOSE IPatchSource::Ptr p = m_plugin->patch(); if (p) { m_exportMenu->model()->setInputData(QJsonObject { { QStringLiteral("urls"), QJsonArray { p->file().toString() } }, { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } }, { QStringLiteral("localBaseDir"), { p->baseDir().toString() } } }); } #endif } PatchReviewToolView::~PatchReviewToolView() { } LocalPatchSource* PatchReviewToolView::GetLocalPatchSource() { IPatchSource::Ptr ips = m_plugin->patch(); if ( !ips ) return 0; return dynamic_cast( ips.data() ); } void PatchReviewToolView::fillEditFromPatch() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) return; m_editPatch.cancelReview->setVisible( ipatch->canCancel() ); m_fileModel->setIsCheckbable( m_plugin->patch()->canSelectFiles() ); if( m_customWidget ) { qCDebug(PLUGIN_PATCHREVIEW) << "removing custom widget"; m_customWidget->hide(); m_editPatch.contentLayout->removeWidget( m_customWidget ); } m_customWidget = ipatch->customWidget(); if( m_customWidget ) { m_editPatch.contentLayout->insertWidget( 0, m_customWidget ); m_customWidget->show(); qCDebug(PLUGIN_PATCHREVIEW) << "got custom widget"; } bool showTests = false; IProject* project = 0; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project && !ICore::self()->testController()->testSuitesForProject(project).isEmpty()) { showTests = true; break; } } m_editPatch.testsButton->setVisible(showTests); m_editPatch.testProgressBar->hide(); } void PatchReviewToolView::slotAppliedChanged( int newState ) { if ( LocalPatchSource* lpatch = GetLocalPatchSource() ) { lpatch->setAlreadyApplied( newState == Qt::Checked ); m_plugin->notifyPatchChanged(); } } void PatchReviewToolView::showEditDialog() { m_editPatch.setupUi( this ); bool allowSelection = m_plugin->patch() && m_plugin->patch()->canSelectFiles(); m_fileModel = new PatchFilesModel( this, allowSelection ); m_fileSortProxyModel = new VcsFileChangesSortProxyModel(this); m_fileSortProxyModel->setSourceModel(m_fileModel); m_fileSortProxyModel->sort(1); m_fileSortProxyModel->setDynamicSortFilter(true); m_editPatch.filesList->setModel( m_fileSortProxyModel ); m_editPatch.filesList->header()->hide(); m_editPatch.filesList->setRootIsDecorated( false ); m_editPatch.filesList->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_editPatch.filesList, &QTreeView::customContextMenuRequested, this, &PatchReviewToolView::customContextMenuRequested); connect(m_fileModel, &PatchFilesModel::itemChanged, this, &PatchReviewToolView::fileItemChanged); m_editPatch.previousFile->setIcon( QIcon::fromTheme( QStringLiteral("arrow-left") ) ); m_editPatch.previousHunk->setIcon( QIcon::fromTheme( QStringLiteral("arrow-up") ) ); m_editPatch.nextHunk->setIcon( QIcon::fromTheme( QStringLiteral("arrow-down") ) ); m_editPatch.nextFile->setIcon( QIcon::fromTheme( QStringLiteral("arrow-right") ) ); m_editPatch.cancelReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-cancel") ) ); m_editPatch.updateButton->setIcon( QIcon::fromTheme( QStringLiteral("view-refresh") ) ); m_editPatch.testsButton->setIcon( QIcon::fromTheme( QStringLiteral("preflight-verifier") ) ); m_editPatch.finishReview->setDefaultAction(m_plugin->finishReviewAction()); #ifdef WITH_PURPOSE m_exportMenu = new Purpose::Menu(this); connect(m_exportMenu, &Purpose::Menu::finished, this, [](const QJsonObject &output, int error, const QString &message) { if (error==0) { KMessageBox::information(0, i18n("You can find the new request at:
%1
", output["url"].toString()), QString(), QString(), KMessageBox::AllowLink); } else { QMessageBox::warning(nullptr, i18n("Error exporting"), i18n("Couldn't export the patch.\n%1", message)); } }); m_exportMenu->model()->setPluginType("Export"); m_editPatch.exportReview->setMenu( m_exportMenu ); #else m_editPatch.exportReview->setEnabled(false); #endif connect( m_editPatch.previousHunk, &QToolButton::clicked, this, &PatchReviewToolView::prevHunk ); connect( m_editPatch.nextHunk, &QToolButton::clicked, this, &PatchReviewToolView::nextHunk ); connect( m_editPatch.previousFile, &QToolButton::clicked, this, &PatchReviewToolView::prevFile ); connect( m_editPatch.nextFile, &QToolButton::clicked, this, &PatchReviewToolView::nextFile ); connect( m_editPatch.filesList, &QTreeView::activated , this, &PatchReviewToolView::fileDoubleClicked ); connect( m_editPatch.cancelReview, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::cancelReview ); //connect( m_editPatch.cancelButton, SIGNAL(pressed()), this, SLOT(slotEditCancel()) ); //connect( this, SIGNAL(finished(int)), this, SLOT(slotEditDialogFinished(int)) ); connect( m_editPatch.updateButton, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::forceUpdate ); connect( m_editPatch.testsButton, &QPushButton::clicked, this, &PatchReviewToolView::runTests ); m_selectAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-all")), i18n("Select All"), this ); connect( m_selectAllAction, &QAction::triggered, this, &PatchReviewToolView::selectAll ); m_deselectAllAction = new QAction( i18n("Deselect All"), this ); connect( m_deselectAllAction, &QAction::triggered, this, &PatchReviewToolView::deselectAll ); } void PatchReviewToolView::customContextMenuRequested(const QPoint& ) { QList urls; QModelIndexList selectionIdxs = m_editPatch.filesList->selectionModel()->selectedIndexes(); foreach(const QModelIndex& idx, selectionIdxs) { urls += idx.data(KDevelop::VcsFileChangesModel::UrlRole).toUrl(); } QPointer menu = new QMenu(m_editPatch.filesList); QList extensions; if(!urls.isEmpty()) { KDevelop::FileContext context(urls); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &context ); } QList vcsActions; foreach( const ContextMenuExtension& ext, extensions ) { vcsActions += ext.actions(ContextMenuExtension::VcsGroup); } menu->addAction(m_selectAllAction); menu->addAction(m_deselectAllAction); menu->addActions(vcsActions); if ( !menu->isEmpty() ) { menu->exec(QCursor::pos()); } delete menu; } void PatchReviewToolView::nextHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( true, current->textDocument()->url() ); } void PatchReviewToolView::prevHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( false, current->textDocument()->url() ); } void PatchReviewToolView::seekFile(bool forwards) { if(!m_plugin->patch()) return; QList checkedUrls = m_fileModel->checkedUrls(); QList allUrls = m_fileModel->urls(); IDocument* current = ICore::self()->documentController()->activeDocument(); if(!current || checkedUrls.empty()) return; qCDebug(PLUGIN_PATCHREVIEW) << "seeking direction" << forwards; int currentIndex = allUrls.indexOf(current->url()); QUrl newUrl; if((forwards && current->url() == checkedUrls.back()) || (!forwards && current->url() == checkedUrls[0])) { newUrl = m_plugin->patch()->file(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping to patch"; } else if(current->url() == m_plugin->patch()->file() || currentIndex == -1) { if(forwards) newUrl = checkedUrls[0]; else newUrl = checkedUrls.back(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping from patch"; } else { QSet checkedUrlsSet( checkedUrls.toSet() ); for(int offset = 1; offset < allUrls.size(); ++offset) { int pos; if(forwards) { pos = (currentIndex + offset) % allUrls.size(); }else{ pos = currentIndex - offset; if(pos < 0) pos += allUrls.size(); } if(checkedUrlsSet.contains(allUrls[pos])) { newUrl = allUrls[pos]; break; } } } if(newUrl.isValid()) { activate( newUrl, forwards ? current : 0 ); }else{ qCDebug(PLUGIN_PATCHREVIEW) << "found no valid target url"; } } void PatchReviewToolView::activate( const QUrl& url, IDocument* buddy ) const { qCDebug(PLUGIN_PATCHREVIEW) << "activating url" << url; // If the document is already open in this area, just re-activate it if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) { foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views()) { if(view->document() == dynamic_cast(doc)) { ICore::self()->documentController()->activateDocument(doc); return; } } } // If the document is not open yet, open it in the correct order IDocument* newDoc = ICore::self()->documentController()->openDocument(url, KTextEditor::Range(), IDocumentController::DefaultMode, QString(), buddy); KTextEditor::View* view = 0; if(newDoc) view= newDoc->activeTextView(); if(view && view->cursorPosition().line() == 0) m_plugin->seekHunk( true, url ); } void PatchReviewToolView::fileItemChanged( QStandardItem* item ) { if (item->column()!=0) return; QUrl url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); if (url.isEmpty()) return; KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if(m_fileModel->isCheckable() && item->checkState() != Qt::Checked) { // Eventually close the document if(doc && doc->state() == IDocument::Clean) { foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views()) { if(view->document() == dynamic_cast(doc)) { ICore::self()->uiController()->activeArea()->closeView(view); return; } } } } else if (!doc) { ICore::self()->documentController()->openDocument(url, KTextEditor::Range::invalid(), IDocumentController::DoNotActivate); } } void PatchReviewToolView::nextFile() { seekFile(true); } void PatchReviewToolView::prevFile() { seekFile(false); } void PatchReviewToolView::deselectAll() { m_fileModel->setAllChecked(false); } void PatchReviewToolView::selectAll() { m_fileModel->setAllChecked(true); } void PatchReviewToolView::finishReview() { QList selectedUrls = m_fileModel->checkedUrls(); qCDebug(PLUGIN_PATCHREVIEW) << "finishing review with" << selectedUrls; m_plugin->finishReview( selectedUrls ); } void PatchReviewToolView::fileDoubleClicked( const QModelIndex& idx ) { const QUrl file = idx.data(VcsFileChangesModel::UrlRole).toUrl(); activate( file ); } void PatchReviewToolView::kompareModelChanged() { QList oldCheckedUrls = m_fileModel->checkedUrls(); m_fileModel->clear(); if ( !m_plugin->modelList() ) return; QMap additionalUrls = m_plugin->patch()->additionalSelectableFiles(); const Diff2::DiffModelList* models = m_plugin->modelList()->models(); if( models ) { Diff2::DiffModelList::const_iterator it = models->constBegin(); for(; it != models->constEnd(); ++it ) { Diff2::DifferenceList * diffs = ( *it )->differences(); int cnt = 0; if ( diffs ) cnt = diffs->count(); - QUrl file = m_plugin->urlForFileModel( *it ); - if( !QFileInfo( file.toLocalFile() ).isReadable() ) + const QUrl file = m_plugin->urlForFileModel( *it ); + if( file.isLocalFile() && !QFileInfo( file.toLocalFile() ).isReadable() ) continue; VcsStatusInfo status; status.setUrl( file ); status.setState( cnt>0 ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemUpToDate ); m_fileModel->updateState( status, cnt ); } } for( QMap::const_iterator it = additionalUrls.constBegin(); it != additionalUrls.constEnd(); it++ ) { VcsStatusInfo status; status.setUrl( it.key() ); status.setState( it.value() ); m_fileModel->updateState( status ); } if(!m_resetCheckedUrls) m_fileModel->setCheckedUrls(oldCheckedUrls); else m_resetCheckedUrls = false; m_editPatch.filesList->resizeColumnToContents( 0 ); // Eventually select the active document documentActivated( ICore::self()->documentController()->activeDocument() ); } void PatchReviewToolView::documentActivated( IDocument* doc ) { if( !doc ) return; if ( !m_plugin->modelList() ) return; const auto matches = m_fileSortProxyModel->match( m_fileSortProxyModel->index(0, 0), VcsFileChangesModel::UrlRole, doc->url(), 1, Qt::MatchExactly); m_editPatch.filesList->setCurrentIndex(matches.value(0)); } void PatchReviewToolView::runTests() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) { return; } IProject* project = 0; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project) { break; } } if (!project) { return; } m_editPatch.testProgressBar->setFormat(i18n("Running tests: %p%")); m_editPatch.testProgressBar->setValue(0); m_editPatch.testProgressBar->show(); ProjectTestJob* job = new ProjectTestJob(project, this); connect(job, &ProjectTestJob::finished, this, &PatchReviewToolView::testJobResult); connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(testJobPercent(KJob*,ulong))); ICore::self()->runController()->registerJob(job); } void PatchReviewToolView::testJobPercent(KJob* job, ulong percent) { Q_UNUSED(job); m_editPatch.testProgressBar->setValue(percent); } void PatchReviewToolView::testJobResult(KJob* job) { ProjectTestJob* testJob = qobject_cast(job); if (!testJob) { return; } ProjectTestResult result = testJob->testResult(); QString format; if (result.passed > 0 && result.failed == 0 && result.error == 0) { format = i18np("Test passed", "All %1 tests passed", result.passed); } else { format = i18n("Test results: %1 passed, %2 failed, %3 errors", result.passed, result.failed, result.error); } m_editPatch.testProgressBar->setFormat(format); // Needed because some test jobs may raise their own output views ICore::self()->uiController()->raiseToolView(this); } #include "patchreviewtoolview.moc" diff --git a/plugins/quickopen/quickopenplugin.cpp b/plugins/quickopen/quickopenplugin.cpp index 6b9d1b1a1a..adf8849c08 100644 --- a/plugins/quickopen/quickopenplugin.cpp +++ b/plugins/quickopen/quickopenplugin.cpp @@ -1,1095 +1,1100 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quickopenplugin.h" #include "quickopenwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "quickopenmodel.h" #include "projectfilequickopen.h" #include "projectitemquickopen.h" #include "declarationlistquickopen.h" #include "documentationquickopenprovider.h" #include "actionsquickopenprovider.h" #include "debug.h" #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(PLUGIN_QUICKOPEN, "kdevplatform.plugins.quickopen") using namespace KDevelop; const bool noHtmlDestriptionInOutline = true; class QuickOpenWidgetCreator { public: virtual ~QuickOpenWidgetCreator() { } virtual QuickOpenWidget* createWidget() = 0; virtual QString objectNameForLine() = 0; virtual void widgetShown() { } }; class StandardQuickOpenWidgetCreator : public QuickOpenWidgetCreator { public: StandardQuickOpenWidgetCreator(const QStringList& items, const QStringList& scopes) : m_items(items) , m_scopes(scopes) { } QString objectNameForLine() override { return QStringLiteral("Quickopen"); } void setItems(const QStringList& scopes, const QStringList& items) { m_scopes = scopes; m_items = items; } QuickOpenWidget* createWidget() override { QStringList useItems = m_items; if(useItems.isEmpty()) useItems = QuickOpenPlugin::self()->lastUsedItems; QStringList useScopes = m_scopes; if(useScopes.isEmpty()) useScopes = QuickOpenPlugin::self()->lastUsedScopes; return new QuickOpenWidget( i18n("Quick Open"), QuickOpenPlugin::self()->m_model, QuickOpenPlugin::self()->lastUsedItems, useScopes, false, true ); } QStringList m_items; QStringList m_scopes; }; class OutlineFilter : public DUChainUtils::DUChainItemFilter { public: enum OutlineMode { Functions, FunctionsAndClasses }; OutlineFilter(QList& _items, OutlineMode _mode = FunctionsAndClasses) : items(_items), mode(_mode) { } bool accept(Declaration* decl) override { if(decl->range().isEmpty()) return false; bool collectable = mode == Functions ? decl->isFunctionDeclaration() : (decl->isFunctionDeclaration() || (decl->internalContext() && decl->internalContext()->type() == DUContext::Class)); if (collectable) { DUChainItem item; item.m_item = IndexedDeclaration(decl); item.m_text = decl->toString(); items << item; return true; } else return false; } bool accept(DUContext* ctx) override { if(ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper ) return true; else return false; } QList& items; OutlineMode mode; }; K_PLUGIN_FACTORY_WITH_JSON(KDevQuickOpenFactory, "kdevquickopen.json", registerPlugin();) Declaration* cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return 0; KDevelop::DUChainReadLocker lock( DUChain::lock() ); return DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor( view->document()->url(), KTextEditor::Cursor(view->cursorPosition()) ) ); } ///The first definition that belongs to a context that surrounds the current cursor Declaration* cursorContextDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if(!view) return 0; KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* ctx = DUChainUtils::standardContextForUrl(view->document()->url()); if(!ctx) return 0; KTextEditor::Cursor cursor(view->cursorPosition()); DUContext* subCtx = ctx->findContext(ctx->transformToLocalRevision(cursor)); while(subCtx && !subCtx->owner()) subCtx = subCtx->parentContext(); Declaration* definition = 0; if(!subCtx || !subCtx->owner()) definition = DUChainUtils::declarationInLine(cursor, ctx); else definition = subCtx->owner(); if(!definition) return 0; return definition; } //Returns only the name, no template-parameters or scope QString cursorItemText() { KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) return QString(); IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) return QString(); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return QString(); } AbstractType::Ptr t = decl->abstractType(); IdentifiedType* idType = dynamic_cast(t.data()); if( idType && idType->declaration(context) ) decl = idType->declaration(context); if(!decl->qualifiedIdentifier().isEmpty()) return decl->qualifiedIdentifier().last().identifier().str(); return QString(); } QuickOpenLineEdit* QuickOpenPlugin::createQuickOpenLineWidget() { return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(QStringList(), QStringList())); } QuickOpenLineEdit* QuickOpenPlugin::quickOpenLine(QString name) { QList< QuickOpenLineEdit* > lines = ICore::self()->uiController()->activeMainWindow()->findChildren(name); foreach(QuickOpenLineEdit* line, lines) { if(line->isVisible()) { return line; } } return 0; } static QuickOpenPlugin* staticQuickOpenPlugin = 0; QuickOpenPlugin* QuickOpenPlugin::self() { return staticQuickOpenPlugin; } void QuickOpenPlugin::createActionsForMainWindow(Sublime::MainWindow* /*window*/, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevquickopen.rc"); QAction* quickOpen = actions.addAction(QStringLiteral("quick_open")); quickOpen->setText( i18n("&Quick Open") ); quickOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen")) ); actions.setDefaultShortcut( quickOpen, Qt::CTRL | Qt::ALT | Qt::Key_Q ); connect(quickOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpen); QAction* quickOpenFile = actions.addAction(QStringLiteral("quick_open_file")); quickOpenFile->setText( i18n("Quick Open &File") ); quickOpenFile->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); actions.setDefaultShortcut( quickOpenFile, Qt::CTRL | Qt::ALT | Qt::Key_O ); connect(quickOpenFile, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFile); QAction* quickOpenClass = actions.addAction(QStringLiteral("quick_open_class")); quickOpenClass->setText( i18n("Quick Open &Class") ); quickOpenClass->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-class")) ); actions.setDefaultShortcut( quickOpenClass, Qt::CTRL | Qt::ALT | Qt::Key_C ); connect(quickOpenClass, &QAction::triggered, this, &QuickOpenPlugin::quickOpenClass); QAction* quickOpenFunction = actions.addAction(QStringLiteral("quick_open_function")); quickOpenFunction->setText( i18n("Quick Open &Function") ); quickOpenFunction->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-function")) ); actions.setDefaultShortcut( quickOpenFunction, Qt::CTRL | Qt::ALT | Qt::Key_M ); connect(quickOpenFunction, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFunction); QAction* quickOpenAlreadyOpen = actions.addAction(QStringLiteral("quick_open_already_open")); quickOpenAlreadyOpen->setText( i18n("Quick Open &Already Open File") ); quickOpenAlreadyOpen->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-file")) ); connect(quickOpenAlreadyOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpenOpenFile); QAction* quickOpenDocumentation = actions.addAction(QStringLiteral("quick_open_documentation")); quickOpenDocumentation->setText( i18n("Quick Open &Documentation") ); quickOpenDocumentation->setIcon( QIcon::fromTheme(QStringLiteral("quickopen-documentation")) ); actions.setDefaultShortcut( quickOpenDocumentation, Qt::CTRL | Qt::ALT | Qt::Key_D ); connect(quickOpenDocumentation, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDocumentation); QAction* quickOpenActions = actions.addAction(QStringLiteral("quick_open_actions")); quickOpenActions->setText( i18n("Quick Open &Actions") ); actions.setDefaultShortcut( quickOpenActions, Qt::CTRL | Qt::ALT | Qt::Key_A); connect(quickOpenActions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenActions); m_quickOpenDeclaration = actions.addAction(QStringLiteral("quick_open_jump_declaration")); m_quickOpenDeclaration->setText( i18n("Jump to Declaration") ); m_quickOpenDeclaration->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-declaration") ) ); actions.setDefaultShortcut( m_quickOpenDeclaration, Qt::CTRL | Qt::Key_Period ); connect(m_quickOpenDeclaration, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDeclaration, Qt::QueuedConnection); m_quickOpenDefinition = actions.addAction(QStringLiteral("quick_open_jump_definition")); m_quickOpenDefinition->setText( i18n("Jump to Definition") ); m_quickOpenDefinition->setIcon( QIcon::fromTheme(QStringLiteral("go-jump-definition") ) ); actions.setDefaultShortcut( m_quickOpenDefinition, Qt::CTRL | Qt::Key_Comma ); connect(m_quickOpenDefinition, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDefinition, Qt::QueuedConnection); QWidgetAction* quickOpenLine = new QWidgetAction(this); quickOpenLine->setText( i18n("Embedded Quick Open") ); // actions.setDefaultShortcut( quickOpenLine, Qt::CTRL | Qt::ALT | Qt::Key_E ); // connect(quickOpenLine, SIGNAL(triggered(bool)), this, SLOT(quickOpenLine(bool))); quickOpenLine->setDefaultWidget(createQuickOpenLineWidget()); actions.addAction(QStringLiteral("quick_open_line"), quickOpenLine); QAction* quickOpenNextFunction = actions.addAction(QStringLiteral("quick_open_next_function")); quickOpenNextFunction->setText( i18n("Next Function") ); actions.setDefaultShortcut( quickOpenNextFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageDown ); connect(quickOpenNextFunction, &QAction::triggered, this, &QuickOpenPlugin::nextFunction); QAction* quickOpenPrevFunction = actions.addAction(QStringLiteral("quick_open_prev_function")); quickOpenPrevFunction->setText( i18n("Previous Function") ); actions.setDefaultShortcut( quickOpenPrevFunction, Qt::CTRL| Qt::ALT | Qt::Key_PageUp ); connect(quickOpenPrevFunction, &QAction::triggered, this, &QuickOpenPlugin::previousFunction); QAction* quickOpenNavigateFunctions = actions.addAction(QStringLiteral("quick_open_outline")); quickOpenNavigateFunctions->setText( i18n("Outline") ); actions.setDefaultShortcut( quickOpenNavigateFunctions, Qt::CTRL| Qt::ALT | Qt::Key_N ); connect(quickOpenNavigateFunctions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenNavigateFunctions); } QuickOpenPlugin::QuickOpenPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevquickopen"), parent) { staticQuickOpenPlugin = this; KDEV_USE_EXTENSION_INTERFACE( KDevelop::IQuickOpen ) m_model = new QuickOpenModel( 0 ); KConfigGroup quickopengrp = KSharedConfig::openConfig()->group("QuickOpen"); lastUsedScopes = quickopengrp.readEntry("SelectedScopes", QStringList() << i18n("Project") << i18n("Includes") << i18n("Includers") << i18n("Currently Open") ); lastUsedItems = quickopengrp.readEntry("SelectedItems", QStringList() ); { m_openFilesData = new OpenFilesDataProvider(); QStringList scopes, items; scopes << i18n("Currently Open"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_openFilesData ); } { m_projectFileData = new ProjectFileDataProvider(); QStringList scopes, items; scopes << i18n("Project"); items << i18n("Files"); m_model->registerProvider( scopes, items, m_projectFileData ); } { m_projectItemData = new ProjectItemDataProvider(this); QStringList scopes, items; scopes << i18n("Project"); items << ProjectItemDataProvider::supportedItemTypes(); m_model->registerProvider( scopes, items, m_projectItemData ); } { m_documentationItemData = new DocumentationQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Documentation"); m_model->registerProvider( scopes, items, m_documentationItemData ); } { m_actionsItemData = new ActionsQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Actions"); m_model->registerProvider( scopes, items, m_actionsItemData ); } } QuickOpenPlugin::~QuickOpenPlugin() { freeModel(); delete m_model; delete m_projectFileData; delete m_projectItemData; delete m_openFilesData; delete m_documentationItemData; delete m_actionsItemData; } void QuickOpenPlugin::unload() { } ContextMenuExtension QuickOpenPlugin::contextMenuExtension(Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker readLock; Declaration* decl(codeContext->declaration().data()); if (decl) { const bool isDef = FunctionDefinition::definition(decl); if (codeContext->use().isValid() || !isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDeclaration); } if(isDef) { menuExt.addAction( KDevelop::ContextMenuExtension::ExtensionGroup, m_quickOpenDefinition); } } return menuExt; } void QuickOpenPlugin::showQuickOpen(const QStringList& items) { if(!freeModel()) return; QStringList initialItems = items; QStringList useScopes = lastUsedScopes; if (!useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); showQuickOpenWidget(initialItems, useScopes, false); } void QuickOpenPlugin::showQuickOpen( ModelTypes modes ) { if(!freeModel()) return; QStringList initialItems; if( modes & Files || modes & OpenFiles ) initialItems << i18n("Files"); if( modes & Functions ) initialItems << i18n("Functions"); if( modes & Classes ) initialItems << i18n("Classes"); QStringList useScopes; if ( modes != OpenFiles ) useScopes = lastUsedScopes; if((modes & OpenFiles) && !useScopes.contains(i18n("Currently Open"))) useScopes << i18n("Currently Open"); bool preselectText = (!(modes & Files) || modes == QuickOpenPlugin::All); showQuickOpenWidget(initialItems, useScopes, preselectText); } void QuickOpenPlugin::showQuickOpenWidget(const QStringList& items, const QStringList& scopes, bool preselectText) { QuickOpenWidgetDialog* dialog = new QuickOpenWidgetDialog( i18n("Quick Open"), m_model, items, scopes ); m_currentWidgetHandler = dialog; if (preselectText) { KDevelop::IDocument *currentDoc = core()->documentController()->activeDocument(); if (currentDoc && currentDoc->isTextDocument()) { QString preselected = currentDoc->textSelection().isEmpty() ? currentDoc->textWord() : currentDoc->textDocument()->text(currentDoc->textSelection()); dialog->widget()->setPreselectedText(preselected); } } connect( dialog->widget(), &QuickOpenWidget::scopesChanged, this, &QuickOpenPlugin::storeScopes ); //Not connecting itemsChanged to storeItems, as showQuickOpen doesn't use lastUsedItems and so shouldn't store item changes //connect( dialog->widget(), SIGNAL(itemsChanged(QStringList)), this, SLOT(storeItems(QStringList)) ); dialog->widget()->ui.itemsButton->setEnabled(false); if(quickOpenLine()) { quickOpenLine()->showWithWidget(dialog->widget()); dialog->deleteLater(); }else{ dialog->run(); } } void QuickOpenPlugin::storeScopes( const QStringList& scopes ) { lastUsedScopes = scopes; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedScopes", scopes ); } void QuickOpenPlugin::storeItems( const QStringList& items ) { lastUsedItems = items; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry( "SelectedItems", items ); } void QuickOpenPlugin::quickOpen() { if(quickOpenLine()) //Same as clicking on Quick Open quickOpenLine()->setFocus(); else showQuickOpen( All ); } void QuickOpenPlugin::quickOpenFile() { showQuickOpen( (ModelTypes)(Files | OpenFiles) ); } void QuickOpenPlugin::quickOpenFunction() { showQuickOpen( Functions ); } void QuickOpenPlugin::quickOpenClass() { showQuickOpen( Classes ); } void QuickOpenPlugin::quickOpenOpenFile() { showQuickOpen( OpenFiles ); } void QuickOpenPlugin::quickOpenDocumentation() { showQuickOpenWidget(QStringList(i18n("Documentation")), QStringList(i18n("Includes")), true); } void QuickOpenPlugin::quickOpenActions() { showQuickOpenWidget(QStringList(i18n("Actions")), QStringList(i18n("Includes")), true); } QSet QuickOpenPlugin::fileSet() const { return m_model->fileSet(); } void QuickOpenPlugin::registerProvider( const QStringList& scope, const QStringList& type, KDevelop::QuickOpenDataProviderBase* provider ) { m_model->registerProvider( scope, type, provider ); } bool QuickOpenPlugin::removeProvider( KDevelop::QuickOpenDataProviderBase* provider ) { m_model->removeProvider( provider ); return true; } void QuickOpenPlugin::quickOpenDeclaration() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } decl->activateSpecialization(); IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } QWidget* QuickOpenPlugin::specialObjectNavigationWidget() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return 0; QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QWidget* w = language->specialLanguageObjectNavigationWidget(url, KTextEditor::Cursor(view->cursorPosition()) ); if(w) return w; } return 0; } QPair QuickOpenPlugin::specialObjectJumpPosition() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if( !view ) return qMakePair(QUrl(), KTextEditor::Cursor()); QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QPair pos = language->specialLanguageObjectJumpCursor(url, KTextEditor::Cursor(view->cursorPosition()) ); if(pos.second.isValid()) { return pos; } } return qMakePair(QUrl(), KTextEditor::Cursor::invalid()); } bool QuickOpenPlugin::jumpToSpecialObject() { QPair pos = specialObjectJumpPosition(); if(pos.second.isValid()) { if(pos.first.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for special language object"; return false; } ICore::self()->documentController()->openDocument(pos.first, pos.second); return true; } return false; } void QuickOpenPlugin::quickOpenDefinition() { if(jumpToSpecialObject()) return; KDevelop::DUChainReadLocker lock( DUChain::lock() ); Declaration* decl = cursorDeclaration(); if(!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if(FunctionDefinition* def = FunctionDefinition::definition(decl)) { def->activateSpecialization(); u = def->url(); c = def->rangeInCurrentRevision().start(); }else{ qCDebug(PLUGIN_QUICKOPEN) << "Found no definition for declaration"; decl->activateSpecialization(); } if(u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } bool QuickOpenPlugin::freeModel() { if(m_currentWidgetHandler) delete m_currentWidgetHandler; m_currentWidgetHandler = 0; return true; } void QuickOpenPlugin::nextFunction() { jumpToNearestFunction(NextFunction); } void QuickOpenPlugin::previousFunction() { jumpToNearestFunction(PreviousFunction); } void QuickOpenPlugin::jumpToNearestFunction(QuickOpenPlugin::FunctionJumpDirection direction) { IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } QList items; OutlineFilter filter(items, OutlineFilter::Functions); DUChainUtils::collectItems( context, filter ); CursorInRevision cursor = context->transformToLocalRevision(KTextEditor::Cursor(doc->cursorPosition())); if (!cursor.isValid()) return; Declaration *nearestDeclBefore = 0; int distanceBefore = INT_MIN; Declaration *nearestDeclAfter = 0; int distanceAfter = INT_MAX; for (int i = 0; i < items.count(); ++i) { Declaration *decl = items[i].m_item.data(); int distance = decl->range().start.line - cursor.line; if (distance < 0 && distance >= distanceBefore) { distanceBefore = distance; nearestDeclBefore = decl; } else if (distance > 0 && distance <= distanceAfter) { distanceAfter = distance; nearestDeclAfter = decl; } } CursorInRevision c = CursorInRevision::invalid(); if (direction == QuickOpenPlugin::NextFunction && nearestDeclAfter) c = nearestDeclAfter->range().start; else if (direction == QuickOpenPlugin::PreviousFunction && nearestDeclBefore) c = nearestDeclBefore->range().start; KTextEditor::Cursor textCursor = KTextEditor::Cursor::invalid(); if (c.isValid()) textCursor = context->transformFromLocalRevision(c); lock.unlock(); if (textCursor.isValid()) core()->documentController()->openDocument(doc->url(), textCursor); else qCDebug(PLUGIN_QUICKOPEN) << "No declaration to jump to"; } struct CreateOutlineDialog { CreateOutlineDialog() : dialog(0), cursorDecl(0), model(0) { } void start() { if(!QuickOpenPlugin::self()->freeModel()) return; IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock( DUChain::lock() ); TopDUContext* context = DUChainUtils::standardContextForUrl( doc->url() ); if( !context ) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } model = new QuickOpenModel(0); OutlineFilter filter(items); DUChainUtils::collectItems( context, filter ); if(noHtmlDestriptionInOutline) { for(int a = 0; a < items.size(); ++a) items[a].m_noHtmlDestription = true; } cursorDecl = cursorContextDeclaration(); model->registerProvider( QStringList(), QStringList(), new DeclarationListDataProvider(QuickOpenPlugin::self(), items, true) ); dialog = new QuickOpenWidgetDialog( i18n("Outline"), model, QStringList(), QStringList(), true ); model->setParent(dialog->widget()); } void finish() { //Select the declaration that contains the cursor if(cursorDecl && dialog) { int num = 0; foreach(const DUChainItem& item, items) { if(item.m_item.data() == cursorDecl) { - dialog->widget()->ui.list->setCurrentIndex( model->index(num,0,QModelIndex()) ); - dialog->widget()->ui.list->scrollTo( model->index(num,0,QModelIndex()), QAbstractItemView::PositionAtCenter ); + QModelIndex index(model->index(num,0,QModelIndex())); + // Need to invoke the scrolling later. If we did it now, then it wouldn't have any effect, + // apparently because the widget internals aren't initialized yet properly (although we've + // already called 'widget->show()'. + auto list = dialog->widget()->ui.list; + QMetaObject::invokeMethod(list, "setCurrentIndex", Qt::QueuedConnection, Q_ARG(QModelIndex, index)); + QMetaObject::invokeMethod(list, "scrollTo", Qt::QueuedConnection, Q_ARG(QModelIndex, index), Q_ARG(QAbstractItemView::ScrollHint, QAbstractItemView::PositionAtCenter)); } ++num; } } } QPointer dialog; Declaration* cursorDecl; QList items; QuickOpenModel* model; }; class OutlineQuickopenWidgetCreator : public QuickOpenWidgetCreator { public: OutlineQuickopenWidgetCreator(QStringList /*scopes*/, QStringList /*items*/) : m_creator(0) { } ~OutlineQuickopenWidgetCreator() override { delete m_creator; } QuickOpenWidget* createWidget() override { delete m_creator; m_creator = new CreateOutlineDialog; m_creator->start(); if(!m_creator->dialog) return 0; m_creator->dialog->deleteLater(); return m_creator->dialog->widget(); } void widgetShown() override { if(m_creator) { m_creator->finish(); delete m_creator; m_creator = 0; } } QString objectNameForLine() override { return QStringLiteral("Outline"); } CreateOutlineDialog* m_creator; }; void QuickOpenPlugin::quickOpenNavigateFunctions() { CreateOutlineDialog create; create.start(); if(!create.dialog) return; m_currentWidgetHandler = create.dialog; QuickOpenLineEdit* line = quickOpenLine(QStringLiteral("Outline")); if(!line) line = quickOpenLine(); if(line) { line->showWithWidget(create.dialog->widget()); create.dialog->deleteLater(); }else create.dialog->run(); create.finish(); } QuickOpenLineEdit::QuickOpenLineEdit(QuickOpenWidgetCreator* creator) : m_widget(0), m_forceUpdate(false), m_widgetCreator(creator) { setMinimumWidth(200); setMaximumWidth(400); deactivate(); setDefaultText(i18n("Quick Open...")); setToolTip(i18n("Search for files, classes, functions and more," " allowing you to quickly navigate in your source code.")); setObjectName(m_widgetCreator->objectNameForLine()); setFocusPolicy(Qt::ClickFocus); } QuickOpenLineEdit::~QuickOpenLineEdit() { delete m_widget; delete m_widgetCreator; } bool QuickOpenLineEdit::insideThis(QObject* object) { while (object) { qCDebug(PLUGIN_QUICKOPEN) << object; if (object == this || object == m_widget) { return true; } object = object->parent(); } return false; } void QuickOpenLineEdit::widgetDestroyed(QObject* obj) { Q_UNUSED(obj); // need to use a queued connection here, because this function is called in ~QWidget! // => QuickOpenWidget instance is half-destructed => connections are not yet cleared // => clear() will trigger signals which will operate on the invalid QuickOpenWidget // So, just wait until properly destructed QMetaObject::invokeMethod(this, "deactivate", Qt::QueuedConnection); } void QuickOpenLineEdit::showWithWidget(QuickOpenWidget* widget) { connect(widget, &QuickOpenWidget::destroyed, this, &QuickOpenLineEdit::widgetDestroyed); qCDebug(PLUGIN_QUICKOPEN) << "storing widget" << widget; deactivate(); if(m_widget) { qCDebug(PLUGIN_QUICKOPEN) << "deleting" << m_widget; delete m_widget; } m_widget = widget; m_forceUpdate = true; setFocus(); } void QuickOpenLineEdit::focusInEvent(QFocusEvent* ev) { QLineEdit::focusInEvent(ev); // delete m_widget; qCDebug(PLUGIN_QUICKOPEN) << "got focus"; qCDebug(PLUGIN_QUICKOPEN) << "old widget" << m_widget << "force update:" << m_forceUpdate; if (m_widget && !m_forceUpdate) return; if (!m_forceUpdate && !QuickOpenPlugin::self()->freeModel()) { deactivate(); return; } m_forceUpdate = false; if(!m_widget) { m_widget = m_widgetCreator->createWidget(); if(!m_widget) { deactivate(); return; } } activate(); m_widget->showStandardButtons(false); m_widget->showSearchField(false); m_widget->setParent(0, Qt::ToolTip); m_widget->setFocusPolicy(Qt::NoFocus); m_widget->setAlternativeSearchField(this); QuickOpenPlugin::self()->m_currentWidgetHandler = m_widget; connect(m_widget.data(), &QuickOpenWidget::ready, this, &QuickOpenLineEdit::deactivate); connect( m_widget.data(), &QuickOpenWidget::scopesChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeScopes ); connect( m_widget.data(), &QuickOpenWidget::itemsChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeItems ); Q_ASSERT(m_widget->ui.searchLine == this); m_widget->prepareShow(); QRect widgetGeometry = QRect(mapToGlobal(QPoint(0, height())), mapToGlobal(QPoint(width(), height() + 400))); widgetGeometry.setWidth(700); ///@todo Waste less space QRect screenGeom = QApplication::desktop()->screenGeometry(this); if (widgetGeometry.right() > screenGeom.right()) { widgetGeometry.moveRight(screenGeom.right()); } if (widgetGeometry.bottom() > screenGeom.bottom()) { widgetGeometry.moveBottom(mapToGlobal(QPoint(0, 0)).y()); } m_widget->setGeometry(widgetGeometry); m_widget->show(); m_widgetCreator->widgetShown(); } void QuickOpenLineEdit::hideEvent(QHideEvent* ev) { QWidget::hideEvent(ev); if(m_widget) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); // deactivate(); } bool QuickOpenLineEdit::eventFilter(QObject* obj, QEvent* e) { if (!m_widget) return false; switch (e->type()) { case QEvent::KeyPress: case QEvent::ShortcutOverride: if (static_cast(e)->key() == Qt::Key_Escape) { deactivate(); e->accept(); return true; } break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: qCDebug(PLUGIN_QUICKOPEN) << "closing because of window activation"; deactivate(); break; // handle bug 260657 - "Outline menu doesn't follow main window on its move" case QEvent::Move: { if (QWidget* widget = qobject_cast(obj)) { // close the outline menu in case a parent widget moved if (widget->isAncestorOf(this)) { qCDebug(PLUGIN_QUICKOPEN) << "closing because of parent widget move"; deactivate(); } break; } } case QEvent::FocusIn: if (dynamic_cast(obj)) { QFocusEvent* focusEvent = dynamic_cast(e); Q_ASSERT(focusEvent); //Eat the focus event, keep the focus qCDebug(PLUGIN_QUICKOPEN) << "focus change" << "inside this: " << insideThis(obj) << "this" << this << "obj" << obj; if(obj == this) return false; qCDebug(PLUGIN_QUICKOPEN) << "reason" << focusEvent->reason(); if (focusEvent->reason() != Qt::MouseFocusReason && focusEvent->reason() != Qt::ActiveWindowFocusReason) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); return false; } if (!insideThis(obj)) deactivate(); } break; default: break; } return false; } void QuickOpenLineEdit::activate() { qCDebug(PLUGIN_QUICKOPEN) << "activating"; setText(QString()); setStyleSheet(QString()); qApp->installEventFilter(this); } void QuickOpenLineEdit::deactivate() { qCDebug(PLUGIN_QUICKOPEN) << "deactivating"; clear(); if(m_widget || hasFocus()) QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); if (m_widget) m_widget->deleteLater(); m_widget = 0; qApp->removeEventFilter(this); } void QuickOpenLineEdit::checkFocus() { qCDebug(PLUGIN_QUICKOPEN) << "checking focus" << m_widget; if(m_widget) { if(isVisible() && !isHidden()) setFocus(); else deactivate(); }else{ if (ICore::self()->documentController()->activeDocument()) ICore::self()->documentController()->activateDocument(ICore::self()->documentController()->activeDocument()); //Make sure the focus is somewehre else, even if there is no active document setEnabled(false); setEnabled(true); } } IQuickOpenLine* QuickOpenPlugin::createQuickOpenLine(const QStringList& scopes, const QStringList& type, IQuickOpen::QuickOpenType kind) { if(kind == Outline) return new QuickOpenLineEdit(new OutlineQuickopenWidgetCreator(scopes, type)); else return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(scopes, type)); } #include "quickopenplugin.moc" diff --git a/plugins/vcschangesview/vcschangesview.cpp b/plugins/vcschangesview/vcschangesview.cpp index 2446839e0d..16cbb344a5 100644 --- a/plugins/vcschangesview/vcschangesview.cpp +++ b/plugins/vcschangesview/vcschangesview.cpp @@ -1,175 +1,176 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * * 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 "vcschangesview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vcschangesviewplugin.h" using namespace KDevelop; VcsChangesView::VcsChangesView(VcsProjectIntegrationPlugin* plugin, QWidget* parent) : QTreeView(parent) { setRootIsDecorated(false); setEditTriggers(QAbstractItemView::NoEditTriggers); setSelectionMode(ContiguousSelection); setContextMenuPolicy(Qt::CustomContextMenu); setTextElideMode(Qt::ElideLeft); setWordWrap(true); setWindowIcon(QIcon::fromTheme(QStringLiteral("exchange-positions"), windowIcon())); connect(this, &VcsChangesView::customContextMenuRequested, this, &VcsChangesView::popupContextMenu); foreach(QAction* action, plugin->actionCollection()->actions()) addAction(action); QAction* action = plugin->actionCollection()->action(QStringLiteral("locate_document")); connect(action, &QAction::triggered, this, &VcsChangesView::selectCurrentDocument); connect(this, &VcsChangesView::doubleClicked, this, &VcsChangesView::openSelected); } static void appendActions(QMenu* menu, const QList& actions) { menu->addSeparator(); menu->addActions(actions); } void VcsChangesView::popupContextMenu( const QPoint &pos ) { QList urls; QList projects; QModelIndexList selectionIdxs = selectedIndexes(); if(selectionIdxs.isEmpty()) return; foreach(const QModelIndex& idx, selectionIdxs) { if(idx.column()==0) { if(idx.parent().isValid()) urls += idx.data(KDevelop::VcsFileChangesModel::VcsStatusInfoRole).value().url(); else { projects += ICore::self()->projectController()->findProjectByName(idx.data(ProjectChangesModel::ProjectNameRole).toString()); + Q_ASSERT(projects.last()); } } } QPointer menu = new QMenu; QAction* refreshAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Refresh")); QList extensions; if(!urls.isEmpty()) { KDevelop::FileContext context(urls); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &context ); } else { QList items; foreach(IProject* p, projects) items += p->projectItem(); KDevelop::ProjectItemContextImpl context(items); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &context ); } QList buildActions; QList vcsActions; QList extActions; QList projectActions; QList fileActions; QList runActions; foreach( const ContextMenuExtension& ext, extensions ) { buildActions += ext.actions(ContextMenuExtension::BuildGroup); fileActions += ext.actions(ContextMenuExtension::FileGroup); projectActions += ext.actions(ContextMenuExtension::ProjectGroup); vcsActions += ext.actions(ContextMenuExtension::VcsGroup); extActions += ext.actions(ContextMenuExtension::ExtensionGroup); runActions += ext.actions(ContextMenuExtension::RunGroup); } appendActions(menu, buildActions); appendActions(menu, runActions ); appendActions(menu, fileActions); appendActions(menu, vcsActions); appendActions(menu, extActions); appendActions(menu, projectActions); if ( !menu->isEmpty() ) { QAction* res = menu->exec( mapToGlobal( pos ) ); if(res == refreshAction) { if(!urls.isEmpty()) emit reload(urls); else emit reload(projects); } } delete menu; } void VcsChangesView::selectCurrentDocument() { IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url = doc->url(); IProject* p = ICore::self()->projectController()->findProjectForUrl(url); QModelIndex idx = (p ? model()->match(model()->index(0, 0), ProjectChangesModel::UrlRole, url, 1, Qt::MatchExactly).value(0) : QModelIndex()); if(idx.isValid()) { expand(idx.parent()); setCurrentIndex(idx); } else collapseAll(); } void VcsChangesView::setModel(QAbstractItemModel* model) { connect(model, &QAbstractItemModel::rowsInserted, this, &VcsChangesView::expand); QTreeView::setModel(model); } void VcsChangesView::openSelected(const QModelIndex& index) { if(!index.parent().isValid()) //then it's a project return; QModelIndex idx = index.sibling(index.row(), 1); VcsStatusInfo info = idx.data(ProjectChangesModel::VcsStatusInfoRole).value(); QUrl url = info.url(); ICore::self()->documentController()->openDocument(url); } diff --git a/project/projectmodel.cpp b/project/projectmodel.cpp index 8ab754d13a..0d9b6f66f8 100644 --- a/project/projectmodel.cpp +++ b/project/projectmodel.cpp @@ -1,1167 +1,1167 @@ /* This file is part of KDevelop Copyright 2005 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2007 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 "projectmodel.h" #include #include #include #include #include #include #include #include #include #include "interfaces/iprojectfilemanager.h" #include #include "path.h" namespace KDevelop { QStringList removeProjectBasePath( const QStringList& fullpath, KDevelop::ProjectBaseItem* item ) { QStringList result = fullpath; if( item ) { KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); QStringList basePath = model->pathFromIndex( model->indexFromItem( item ) ); if( basePath.count() >= fullpath.count() ) { return QStringList(); } for( int i = 0; i < basePath.count(); i++ ) { result.takeFirst(); } } return result; } QStringList joinProjectBasePath( const QStringList& partialpath, KDevelop::ProjectBaseItem* item ) { QStringList basePath; if( item ) { KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); basePath = model->pathFromIndex( model->indexFromItem( item ) ); } return basePath + partialpath; } inline uint indexForPath( const Path& path ) { return IndexedString::indexForString(path.pathOrUrl()); } class ProjectModelPrivate { public: ProjectModelPrivate( ProjectModel* model ): model( model ) { } ProjectBaseItem* rootItem; ProjectModel* model; ProjectBaseItem* itemFromIndex( const QModelIndex& idx ) { if( !idx.isValid() ) { return rootItem; } if( idx.model() != model ) { return 0; } return model->itemFromIndex( idx ); } // a hash of IndexedString::indexForString(path) <-> ProjectBaseItem for fast lookup QMultiHash pathLookupTable; }; class ProjectBaseItemPrivate { public: ProjectBaseItemPrivate() : project(0), parent(0), row(-1), model(0), m_pathIndex(0) {} IProject* project; ProjectBaseItem* parent; int row; QList children; QString text; ProjectBaseItem::ProjectItemType type; Qt::ItemFlags flags; ProjectModel* model; Path m_path; uint m_pathIndex; QString iconName; ProjectBaseItem::RenameStatus renameBaseItem(ProjectBaseItem* item, const QString& newName) { if (item->parent()) { foreach(ProjectBaseItem* sibling, item->parent()->children()) { if (sibling->text() == newName) { return ProjectBaseItem::ExistingItemSameName; } } } item->setText( newName ); return ProjectBaseItem::RenameOk; } ProjectBaseItem::RenameStatus renameFileOrFolder(ProjectBaseItem* item, const QString& newName) { Q_ASSERT(item->file() || item->folder()); if (newName.contains('/')) { return ProjectBaseItem::InvalidNewName; } if (item->text() == newName) { return ProjectBaseItem::RenameOk; } Path newPath = item->path(); newPath.setLastPathSegment(newName); auto job = KIO::stat(newPath.toUrl(), KIO::StatJob::SourceSide, 0); if (job->exec()) { // file/folder exists already return ProjectBaseItem::ExistingItemSameName; } if( !item->project() || !item->project()->projectFileManager() ) { return renameBaseItem(item, newName); } else if( item->folder() && item->project()->projectFileManager()->renameFolder(item->folder(), newPath) ) { return ProjectBaseItem::RenameOk; } else if ( item->file() && item->project()->projectFileManager()->renameFile(item->file(), newPath) ) { return ProjectBaseItem::RenameOk; } else { return ProjectBaseItem::ProjectManagerRenameFailed; } } }; ProjectBaseItem::ProjectBaseItem( IProject* project, const QString &name, ProjectBaseItem *parent ) : d_ptr(new ProjectBaseItemPrivate) { Q_ASSERT(!name.isEmpty() || !parent); Q_D(ProjectBaseItem); d->project = project; d->text = name; d->flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if( parent ) { parent->appendRow( this ); } } ProjectBaseItem::~ProjectBaseItem() { Q_D(ProjectBaseItem); if (model() && d->m_pathIndex) { model()->d->pathLookupTable.remove(d->m_pathIndex, this); } if( parent() ) { parent()->takeRow( d->row ); } else if( model() ) { model()->takeRow( d->row ); } removeRows(0, d->children.size()); delete d; } ProjectBaseItem* ProjectBaseItem::child( int row ) const { Q_D(const ProjectBaseItem); if( row < 0 || row >= d->children.length() ) { return 0; } return d->children.at( row ); } QList< ProjectBaseItem* > ProjectBaseItem::children() const { Q_D(const ProjectBaseItem); return d->children; } ProjectBaseItem* ProjectBaseItem::takeRow(int row) { Q_D(ProjectBaseItem); Q_ASSERT(row >= 0 && row < d->children.size()); if( model() ) { model()->beginRemoveRows(index(), row, row); } ProjectBaseItem* olditem = d->children.takeAt( row ); olditem->d_func()->parent = 0; olditem->d_func()->row = -1; olditem->setModel( 0 ); for(int i=row; id_func()->row--; Q_ASSERT(child(i)->d_func()->row==i); } if( model() ) { model()->endRemoveRows(); } return olditem; } void ProjectBaseItem::removeRow( int row ) { delete takeRow( row ); } void ProjectBaseItem::removeRows(int row, int count) { if (!count) { return; } Q_D(ProjectBaseItem); Q_ASSERT(row >= 0 && row + count <= d->children.size()); if( model() ) { model()->beginRemoveRows(index(), row, row + count - 1); } //NOTE: we unset parent, row and model manually to speed up the deletion if (row == 0 && count == d->children.size()) { // optimize if we want to delete all foreach(ProjectBaseItem* item, d->children) { item->d_func()->parent = 0; item->d_func()->row = -1; item->setModel( 0 ); delete item; } d->children.clear(); } else { for (int i = row; i < count; ++i) { ProjectBaseItem* item = d->children.at(i); item->d_func()->parent = 0; item->d_func()->row = -1; item->setModel( 0 ); delete d->children.takeAt( row ); } for(int i = row; i < d->children.size(); ++i) { d->children.at(i)->d_func()->row--; Q_ASSERT(child(i)->d_func()->row==i); } } if( model() ) { model()->endRemoveRows(); } } QModelIndex ProjectBaseItem::index() const { if( model() ) { return model()->indexFromItem( this ); } return QModelIndex(); } int ProjectBaseItem::rowCount() const { Q_D(const ProjectBaseItem); return d->children.count(); } int ProjectBaseItem::type() const { return ProjectBaseItem::BaseItem; } ProjectModel* ProjectBaseItem::model() const { Q_D(const ProjectBaseItem); return d->model; } ProjectBaseItem* ProjectBaseItem::parent() const { Q_D(const ProjectBaseItem); if( model() && model()->d->rootItem == d->parent ) { return 0; } return d->parent; } int ProjectBaseItem::row() const { Q_D(const ProjectBaseItem); return d->row; } QString ProjectBaseItem::text() const { Q_D(const ProjectBaseItem); if( project() && !parent() ) { return project()->name(); } else { return d->text; } } void ProjectBaseItem::setModel( ProjectModel* model ) { Q_D(ProjectBaseItem); if (model == d->model) { return; } if (d->model && d->m_pathIndex) { d->model->d->pathLookupTable.remove(d->m_pathIndex, this); } d->model = model; if (model && d->m_pathIndex) { model->d->pathLookupTable.insert(d->m_pathIndex, this); } foreach( ProjectBaseItem* item, d->children ) { item->setModel( model ); } } void ProjectBaseItem::setRow( int row ) { Q_D(ProjectBaseItem); d->row = row; } void ProjectBaseItem::setText( const QString& text ) { Q_ASSERT(!text.isEmpty() || !parent()); Q_D(ProjectBaseItem); d->text = text; if( d->model ) { QModelIndex idx = index(); emit d->model->dataChanged(idx, idx); } } ProjectBaseItem::RenameStatus ProjectBaseItem::rename(const QString& newName) { Q_D(ProjectBaseItem); return d->renameBaseItem(this, newName); } KDevelop::ProjectBaseItem::ProjectItemType baseType( int type ) { if( type == KDevelop::ProjectBaseItem::Folder || type == KDevelop::ProjectBaseItem::BuildFolder ) return KDevelop::ProjectBaseItem::Folder; if( type == KDevelop::ProjectBaseItem::Target || type == KDevelop::ProjectBaseItem::ExecutableTarget || type == KDevelop::ProjectBaseItem::LibraryTarget) return KDevelop::ProjectBaseItem::Target; return static_cast( type ); } bool ProjectBaseItem::lessThan( const KDevelop::ProjectBaseItem* item ) const { if(item->type() >= KDevelop::ProjectBaseItem::CustomProjectItemType ) { // For custom types we want to make sure that if they override lessThan, then we // prefer their lessThan implementation return !item->lessThan( this ); } KDevelop::ProjectBaseItem::ProjectItemType leftType=baseType(type()), rightType=baseType(item->type()); if(leftType==rightType) { if(leftType==KDevelop::ProjectBaseItem::File) { return file()->fileName().compare(item->file()->fileName(), Qt::CaseInsensitive) < 0; } return this->text()text(); } else { return leftTypepath() < item2->path(); } IProject* ProjectBaseItem::project() const { Q_D(const ProjectBaseItem); return d->project; } void ProjectBaseItem::appendRow( ProjectBaseItem* item ) { Q_D(ProjectBaseItem); if( !item ) { return; } if( item->parent() ) { // Proper way is to first removeRow() on the original parent, then appendRow on this one qWarning() << "Ignoring double insertion of item" << item; return; } // this is too slow... O(n) and thankfully not a problem anyways // Q_ASSERT(!d->children.contains(item)); int startrow,endrow; if( model() ) { startrow = endrow = d->children.count(); model()->beginInsertRows(index(), startrow, endrow); } d->children.append( item ); item->setRow( d->children.count() - 1 ); item->d_func()->parent = this; item->setModel( model() ); if( model() ) { model()->endInsertRows(); } } Path ProjectBaseItem::path() const { Q_D(const ProjectBaseItem); return d->m_path; } QString ProjectBaseItem::baseName() const { return text(); } void ProjectBaseItem::setPath( const Path& path) { Q_D(ProjectBaseItem); if (model() && d->m_pathIndex) { model()->d->pathLookupTable.remove(d->m_pathIndex, this); } d->m_path = path; d->m_pathIndex = indexForPath(path); setText( path.lastPathSegment() ); if (model() && d->m_pathIndex) { model()->d->pathLookupTable.insert(d->m_pathIndex, this); } } Qt::ItemFlags ProjectBaseItem::flags() { Q_D(ProjectBaseItem); return d->flags; } Qt::DropActions ProjectModel::supportedDropActions() const { return (Qt::DropActions)(Qt::MoveAction); } void ProjectBaseItem::setFlags(Qt::ItemFlags flags) { Q_D(ProjectBaseItem); d->flags = flags; if(d->model) d->model->dataChanged(index(), index()); } QString ProjectBaseItem::iconName() const { - return QLatin1String(""); + return QString(); } ProjectFolderItem *ProjectBaseItem::folder() const { return 0; } ProjectTargetItem *ProjectBaseItem::target() const { return 0; } ProjectExecutableTargetItem *ProjectBaseItem::executable() const { return 0; } ProjectFileItem *ProjectBaseItem::file() const { return 0; } QList ProjectBaseItem::folderList() const { QList lst; for ( int i = 0; i < rowCount(); ++i ) { ProjectBaseItem* item = child( i ); if ( item->type() == Folder || item->type() == BuildFolder ) { ProjectFolderItem *kdevitem = dynamic_cast( item ); if ( kdevitem ) lst.append( kdevitem ); } } return lst; } QList ProjectBaseItem::targetList() const { QList lst; for ( int i = 0; i < rowCount(); ++i ) { ProjectBaseItem* item = child( i ); if ( item->type() == Target || item->type() == LibraryTarget || item->type() == ExecutableTarget ) { ProjectTargetItem *kdevitem = dynamic_cast( item ); if ( kdevitem ) lst.append( kdevitem ); } } return lst; } QList ProjectBaseItem::fileList() const { QList lst; for ( int i = 0; i < rowCount(); ++i ) { ProjectBaseItem* item = child( i ); Q_ASSERT(item); if ( item && item->type() == File ) { ProjectFileItem *kdevitem = dynamic_cast( item ); if ( kdevitem ) lst.append( kdevitem ); } } return lst; } void ProjectModel::clear() { d->rootItem->removeRows(0, d->rootItem->rowCount()); } ProjectFolderItem::ProjectFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent) : ProjectBaseItem( project, path.lastPathSegment(), parent ) { setPath( path ); setFlags(flags() | Qt::ItemIsDropEnabled); if (project && project->path() != path) setFlags(flags() | Qt::ItemIsDragEnabled); } ProjectFolderItem::ProjectFolderItem( const QString & name, ProjectBaseItem * parent ) : ProjectBaseItem( parent->project(), name, parent ) { setPath( Path(parent->path(), name) ); setFlags(flags() | Qt::ItemIsDropEnabled); if (project() && project()->path() != path()) setFlags(flags() | Qt::ItemIsDragEnabled); } ProjectFolderItem::~ProjectFolderItem() { } void ProjectFolderItem::setPath( const Path& path ) { ProjectBaseItem::setPath(path); propagateRename(path); } ProjectFolderItem *ProjectFolderItem::folder() const { return const_cast(this); } int ProjectFolderItem::type() const { return ProjectBaseItem::Folder; } QString ProjectFolderItem::folderName() const { return baseName(); } void ProjectFolderItem::propagateRename( const Path& newBase ) const { Path path = newBase; path.addPath(QStringLiteral("dummy")); foreach( KDevelop::ProjectBaseItem* child, children() ) { path.setLastPathSegment( child->text() ); child->setPath( path ); const ProjectFolderItem* folder = child->folder(); if ( folder ) { folder->propagateRename( path ); } } } ProjectBaseItem::RenameStatus ProjectFolderItem::rename(const QString& newName) { return d_ptr->renameFileOrFolder(this, newName); } bool ProjectFolderItem::hasFileOrFolder(const QString& name) const { foreach ( ProjectBaseItem* item, children() ) { if ( (item->type() == Folder || item->type() == File || item->type() == BuildFolder) && name == item->baseName() ) { return true; } } return false; } bool ProjectBaseItem::isProjectRoot() const { return parent()==0; } ProjectBuildFolderItem::ProjectBuildFolderItem(IProject* project, const Path& path, ProjectBaseItem *parent) : ProjectFolderItem( project, path, parent ) { } ProjectBuildFolderItem::ProjectBuildFolderItem( const QString& name, ProjectBaseItem* parent ) : ProjectFolderItem( name, parent ) { } QString ProjectFolderItem::iconName() const { return QStringLiteral("folder"); } int ProjectBuildFolderItem::type() const { return ProjectBaseItem::BuildFolder; } QString ProjectBuildFolderItem::iconName() const { return QStringLiteral("folder-development"); } ProjectFileItem::ProjectFileItem( IProject* project, const Path& path, ProjectBaseItem* parent ) : ProjectBaseItem( project, path.lastPathSegment(), parent ) { setFlags(flags() | Qt::ItemIsDragEnabled); setPath( path ); } ProjectFileItem::ProjectFileItem( const QString& name, ProjectBaseItem* parent ) : ProjectBaseItem( parent->project(), name, parent ) { setFlags(flags() | Qt::ItemIsDragEnabled); setPath( Path(parent->path(), name) ); } ProjectFileItem::~ProjectFileItem() { if( project() && d_ptr->m_pathIndex ) { project()->removeFromFileSet( this ); } } IndexedString ProjectFileItem::indexedPath() const { return IndexedString::fromIndex( d_ptr->m_pathIndex ); } ProjectBaseItem::RenameStatus ProjectFileItem::rename(const QString& newName) { return d_ptr->renameFileOrFolder(this, newName); } QString ProjectFileItem::fileName() const { return baseName(); } // Maximum length of a string to still consider it as a file extension which we cache // This has to be a slow value, so that we don't fill our file extension cache with crap static const int maximumCacheExtensionLength = 3; bool isNumeric(const QStringRef& str) { int len = str.length(); if(len == 0) return false; for(int a = 0; a < len; ++a) if(!str.at(a).isNumber()) return false; return true; } class IconNameCache { public: QString iconNameForPath(const Path& path, const QString& fileName) { // find icon name based on file extension, if possible QString extension; int extensionStart = fileName.lastIndexOf(QLatin1Char('.')); if( extensionStart != -1 && fileName.length() - extensionStart - 1 <= maximumCacheExtensionLength ) { QStringRef extRef = fileName.midRef(extensionStart + 1); if( isNumeric(extRef) ) { // don't cache numeric extensions extRef.clear(); } if( !extRef.isEmpty() ) { extension = extRef.toString(); QMutexLocker lock(&mutex); QHash< QString, QString >::const_iterator it = fileExtensionToIcon.constFind( extension ); if( it != fileExtensionToIcon.constEnd() ) { return *it; } } } QMimeType mime = QMimeDatabase().mimeTypeForFile(path.lastPathSegment(), QMimeDatabase::MatchExtension); // no I/O QMutexLocker lock(&mutex); QHash< QString, QString >::const_iterator it = mimeToIcon.constFind(mime.name()); QString iconName; if ( it == mimeToIcon.constEnd() ) { iconName = mime.iconName(); mimeToIcon.insert(mime.name(), iconName); } else { iconName = *it; } if ( !extension.isEmpty() ) { fileExtensionToIcon.insert(extension, iconName); } return iconName; } QMutex mutex; QHash mimeToIcon; QHash fileExtensionToIcon; }; Q_GLOBAL_STATIC(IconNameCache, s_cache); QString ProjectFileItem::iconName() const { // think of d_ptr->iconName as mutable, possible since d_ptr is not const if (d_ptr->iconName.isEmpty()) { // lazy load implementation of icon lookup d_ptr->iconName = s_cache->iconNameForPath( d_ptr->m_path, d_ptr->text ); // we should always get *some* icon name back Q_ASSERT(!d_ptr->iconName.isEmpty()); } return d_ptr->iconName; } void ProjectFileItem::setPath( const Path& path ) { if (path == d_ptr->m_path) { return; } if( project() && d_ptr->m_pathIndex ) { // remove from fileset if we are in there project()->removeFromFileSet( this ); } ProjectBaseItem::setPath( path ); if( project() && d_ptr->m_pathIndex ) { // add to fileset with new path project()->addToFileSet( this ); } // invalidate icon name for future lazy-loaded updated d_ptr->iconName.clear(); } int ProjectFileItem::type() const { return ProjectBaseItem::File; } ProjectFileItem *ProjectFileItem::file() const { return const_cast( this ); } ProjectTargetItem::ProjectTargetItem( IProject* project, const QString &name, ProjectBaseItem *parent ) : ProjectBaseItem( project, name, parent ) { setFlags(flags() | Qt::ItemIsDropEnabled); } QString ProjectTargetItem::iconName() const { return QStringLiteral("system-run"); } void ProjectTargetItem::setPath( const Path& path ) { // don't call base class, it calls setText with the new path's filename // which we do not want for target items d_ptr->m_path = path; } int ProjectTargetItem::type() const { return ProjectBaseItem::Target; } ProjectTargetItem *ProjectTargetItem::target() const { return const_cast( this ); } ProjectExecutableTargetItem::ProjectExecutableTargetItem( IProject* project, const QString &name, ProjectBaseItem *parent ) : ProjectTargetItem(project, name, parent) { } ProjectExecutableTargetItem *ProjectExecutableTargetItem::executable() const { return const_cast( this ); } int ProjectExecutableTargetItem::type() const { return ProjectBaseItem::ExecutableTarget; } ProjectLibraryTargetItem::ProjectLibraryTargetItem( IProject* project, const QString &name, ProjectBaseItem *parent ) : ProjectTargetItem(project, name, parent) {} int ProjectLibraryTargetItem::type() const { return ProjectBaseItem::LibraryTarget; } QModelIndex ProjectModel::pathToIndex(const QStringList& tofetch_) const { if(tofetch_.isEmpty()) return QModelIndex(); QStringList tofetch(tofetch_); if(tofetch.last().isEmpty()) tofetch.takeLast(); QModelIndex current=index(0,0, QModelIndex()); QModelIndex ret; for(int a = 0; a < tofetch.size(); ++a) { const QString& currentName = tofetch[a]; bool matched = false; QModelIndexList l = match(current, Qt::DisplayRole, currentName, -1, Qt::MatchExactly); foreach(const QModelIndex& idx, l) { //If this is not the last item, only match folders, as there may be targets and folders with the same name if(a == tofetch.size()-1 || itemFromIndex(idx)->folder()) { ret = idx; current = index(0,0, ret); matched = true; break; } } if(!matched) { ret = QModelIndex(); break; } } Q_ASSERT(!ret.isValid() || data(ret).toString()==tofetch.last()); return ret; } QStringList ProjectModel::pathFromIndex(const QModelIndex& index) const { if (!index.isValid()) return QStringList(); QModelIndex idx = index; QStringList list; do { QString t = data(idx, Qt::DisplayRole).toString(); list.prepend(t); QModelIndex parent = idx.parent(); idx = parent.sibling(parent.row(), index.column()); } while (idx.isValid()); return list; } int ProjectModel::columnCount( const QModelIndex& ) const { return 1; } int ProjectModel::rowCount( const QModelIndex& parent ) const { ProjectBaseItem* item = d->itemFromIndex( parent ); return item ? item->rowCount() : 0; } QModelIndex ProjectModel::parent( const QModelIndex& child ) const { if( child.isValid() ) { ProjectBaseItem* item = static_cast( child.internalPointer() ); return indexFromItem( item ); } return QModelIndex(); } QModelIndex ProjectModel::indexFromItem( const ProjectBaseItem* item ) const { if( item && item->d_func()->parent ) { return createIndex( item->row(), 0, item->d_func()->parent ); } return QModelIndex(); } ProjectBaseItem* ProjectModel::itemFromIndex( const QModelIndex& index ) const { if( index.row() >= 0 && index.column() == 0 && index.model() == this ) { ProjectBaseItem* parent = static_cast( index.internalPointer() ); if( parent ) { return parent->child( index.row() ); } } return 0; } QVariant ProjectModel::data( const QModelIndex& index, int role ) const { static const QSet allowedRoles = { Qt::DisplayRole, Qt::ToolTipRole, Qt::DecorationRole, ProjectItemRole, ProjectRole, UrlRole }; if( allowedRoles.contains(role) && index.isValid() ) { ProjectBaseItem* item = itemFromIndex( index ); if( item ) { switch(role) { case Qt::DecorationRole: return QIcon::fromTheme(item->iconName()); case Qt::ToolTipRole: return item->path().pathOrUrl(); case Qt::DisplayRole: return item->text(); case ProjectItemRole: return QVariant::fromValue(item); case UrlRole: return item->path().toUrl(); case ProjectRole: return QVariant::fromValue(item->project()); } } } return QVariant(); } ProjectModel::ProjectModel( QObject *parent ) : QAbstractItemModel( parent ), d( new ProjectModelPrivate( this ) ) { d->rootItem = new ProjectBaseItem( 0, QString(), 0 ); d->rootItem->setModel( this ); } ProjectModel::~ProjectModel() { d->rootItem->setModel(nullptr); delete d->rootItem; delete d; } ProjectVisitor::ProjectVisitor() { } QModelIndex ProjectModel::index( int row, int column, const QModelIndex& parent ) const { ProjectBaseItem* parentItem = d->itemFromIndex( parent ); if( parentItem && row >= 0 && row < parentItem->rowCount() && column == 0 ) { return createIndex( row, column, parentItem ); } return QModelIndex(); } void ProjectModel::appendRow( ProjectBaseItem* item ) { d->rootItem->appendRow( item ); } void ProjectModel::removeRow( int row ) { d->rootItem->removeRow( row ); } ProjectBaseItem* ProjectModel::takeRow( int row ) { return d->rootItem->takeRow( row ); } ProjectBaseItem* ProjectModel::itemAt(int row) const { return d->rootItem->child(row); } QList< ProjectBaseItem* > ProjectModel::topItems() const { return d->rootItem->children(); } Qt::ItemFlags ProjectModel::flags(const QModelIndex& index) const { ProjectBaseItem* item = itemFromIndex( index ); if(item) return item->flags(); else return 0; } bool ProjectModel::insertColumns(int, int, const QModelIndex&) { // Not supported return false; } bool ProjectModel::insertRows(int, int, const QModelIndex&) { // Not supported return false; } bool ProjectModel::setData(const QModelIndex&, const QVariant&, int) { // Not supported return false; } QList ProjectModel::itemsForPath(const IndexedString& path) const { return d->pathLookupTable.values(path.index()); } ProjectBaseItem* ProjectModel::itemForPath(const IndexedString& path) const { return d->pathLookupTable.value(path.index()); } void ProjectVisitor::visit( ProjectModel* model ) { foreach( ProjectBaseItem* item, model->topItems() ) { visit( item->project() ); } } void ProjectVisitor::visit ( IProject* prj ) { visit( prj->projectItem() ); } void ProjectVisitor::visit ( ProjectBuildFolderItem* folder ) { visit(static_cast(folder)); } void ProjectVisitor::visit ( ProjectExecutableTargetItem* exec ) { foreach( ProjectFileItem* item, exec->fileList() ) { visit( item ); } } void ProjectVisitor::visit ( ProjectFolderItem* folder ) { foreach( ProjectFileItem* item, folder->fileList() ) { visit( item ); } foreach( ProjectTargetItem* item, folder->targetList() ) { if( item->type() == ProjectBaseItem::LibraryTarget ) { visit( dynamic_cast( item ) ); } else if( item->type() == ProjectBaseItem::ExecutableTarget ) { visit( dynamic_cast( item ) ); } } foreach( ProjectFolderItem* item, folder->folderList() ) { if( item->type() == ProjectBaseItem::BuildFolder ) { visit( dynamic_cast( item ) ); } else if( item->type() == ProjectBaseItem::Folder ) { visit( dynamic_cast( item ) ); } } } void ProjectVisitor::visit ( ProjectFileItem* ) { } void ProjectVisitor::visit ( ProjectLibraryTargetItem* lib ) { foreach( ProjectFileItem* item, lib->fileList() ) { visit( item ); } } ProjectVisitor::~ProjectVisitor() { } } diff --git a/shell/mainwindow.cpp b/shell/mainwindow.cpp index 55cd262024..d10aaf48a2 100644 --- a/shell/mainwindow.cpp +++ b/shell/mainwindow.cpp @@ -1,461 +1,467 @@ /* This file is part of the KDevelop project Copyright 2002 Falk Brettschneider Copyright 2003 John Firebaugh Copyright 2006 Adam Treat Copyright 2006, 2007 Alexander Dymo 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 "mainwindow.h" #include "mainwindow_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shellextension.h" #include "partcontroller.h" #include "plugincontroller.h" #include "projectcontroller.h" #include "uicontroller.h" #include "documentcontroller.h" #include "debugcontroller.h" #include "workingsetcontroller.h" #include "sessioncontroller.h" #include "sourceformattercontroller.h" #include "areadisplay.h" #include "project.h" #include "debug.h" #include "uiconfig.h" #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QColor defaultColor(const QPalette& palette) { const KColorScheme scheme(palette.currentColorGroup()); return scheme.foreground(KColorScheme::NormalText).color(); } -QColor colorForDocument(const QUrl& url, const QPalette& palette) +QColor colorForDocument(const QUrl& url, const QPalette& palette, const QColor& defaultColor) { auto project = Core::self()->projectController()->findProjectForUrl(url); if (!project) - return QColor(); + return defaultColor; return WidgetColorizer::colorForId(qHash(project->path()), palette); } } void MainWindow::applyMainWindowSettings(const KConfigGroup& config) { if(!d->changingActiveView()) KXmlGuiWindow::applyMainWindowSettings(config); } void MainWindow::createGUI(KParts::Part* part) { Sublime::MainWindow::createGUI(part); if (part) { // Don't let the Part control the main window caption -- we take care of that disconnect(part, SIGNAL(setWindowCaption(QString)), this, SLOT(setCaption(QString))); } } void MainWindow::initializeCorners() { const KConfigGroup cg = KSharedConfig::openConfig()->group( "UiSettings" ); const int bottomleft = cg.readEntry( "BottomLeftCornerOwner", 0 ); const int bottomright = cg.readEntry( "BottomRightCornerOwner", 0 ); qCDebug(SHELL) << "Bottom Left:" << bottomleft; qCDebug(SHELL) << "Bottom Right:" << bottomright; // 0 means vertical dock (left, right), 1 means horizontal dock( top, bottom ) if( bottomleft == 0 ) setCorner( Qt::BottomLeftCorner, Qt::LeftDockWidgetArea ); else if( bottomleft == 1 ) setCorner( Qt::BottomLeftCorner, Qt::BottomDockWidgetArea ); if( bottomright == 0 ) setCorner( Qt::BottomRightCorner, Qt::RightDockWidgetArea ); else if( bottomright == 1 ) setCorner( Qt::BottomRightCorner, Qt::BottomDockWidgetArea ); } MainWindow::MainWindow( Sublime::Controller *parent, Qt::WindowFlags flags ) : Sublime::MainWindow( parent, flags ) { QDBusConnection::sessionBus().registerObject( QStringLiteral("/kdevelop/MainWindow"), this, QDBusConnection::ExportScriptableSlots ); setAcceptDrops( true ); initializeCorners(); setObjectName( QStringLiteral("MainWindow") ); d = new MainWindowPrivate(this); setStandardToolBarMenuEnabled( true ); d->setupActions(); if( !ShellExtension::getInstance()->xmlFile().isEmpty() ) { setXMLFile( ShellExtension::getInstance() ->xmlFile() ); } menuBar()->setCornerWidget(new AreaDisplay(this), Qt::TopRightCorner); } MainWindow::~ MainWindow() { if (memberList().count() == 1) { // We're closing down... Core::self()->shutdown(); } delete d; } KTextEditorIntegration::MainWindow *MainWindow::kateWrapper() const { return d->kateWrapper(); } void MainWindow::ensureVisible() { if (isMinimized()) { if (isMaximized()) { showMaximized(); } else { showNormal(); } } KWindowSystem::forceActiveWindow(winId()); } QAction* MainWindow::createCustomElement(QWidget* parent, int index, const QDomElement& element) { QAction* before = 0L; if (index > 0 && index < parent->actions().count()) before = parent->actions().at(index); //KDevelop needs to ensure that separators defined as //are always shown in the menubar. For those, we create special disabled actions //instead of calling QMenuBar::addSeparator() because menubar separators are ignored if (element.tagName().toLower() == QLatin1String("separator") && element.attribute(QStringLiteral("style")) == QLatin1String("visible")) { if ( QMenuBar* bar = qobject_cast( parent ) ) { QAction *separatorAction = new QAction(QStringLiteral("|"), this); bar->insertAction( before, separatorAction ); separatorAction->setDisabled(true); return separatorAction; } } return KXMLGUIBuilder::createCustomElement(parent, index, element); } void MainWindow::dragEnterEvent( QDragEnterEvent* ev ) { if( ev->mimeData()->hasFormat( QStringLiteral("text/uri-list") ) && ev->mimeData()->hasUrls() ) { ev->acceptProposedAction(); } } void MainWindow::dropEvent( QDropEvent* ev ) { Sublime::View* dropToView = viewForPosition(mapToGlobal(ev->pos())); if(dropToView) activateView(dropToView); foreach( const QUrl& u, ev->mimeData()->urls() ) { Core::self()->documentController()->openDocument( u ); } ev->acceptProposedAction(); } void MainWindow::loadSettings() { qCDebug(SHELL) << "Loading Settings"; initializeCorners(); updateAllTabColors(); Sublime::MainWindow::loadSettings(); } void MainWindow::configureShortcuts() { ///Workaround for a problem with the actions: Always start the shortcut-configuration in the first mainwindow, then propagate the updated ///settings into the other windows // We need to bring up the shortcut dialog ourself instead of // Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->configureShortcuts(); // so we can connect to the saved() signal to propagate changes in the editor shortcuts KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); foreach (KXMLGUIClient *client, Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients()) { if(client && !client->xmlFile().isEmpty()) dlg.addCollection( client->actionCollection() ); } connect(&dlg, &KShortcutsDialog::saved, this, &MainWindow::shortcutsChanged); dlg.configure(true); QMap shortcuts; foreach(KXMLGUIClient* client, Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients()) { foreach(QAction* action, client->actionCollection()->actions()) { if(!action->objectName().isEmpty()) { shortcuts[action->objectName()] = action->shortcut(); } } } for(int a = 1; a < Core::self()->uiControllerInternal()->mainWindows().size(); ++a) { foreach(KXMLGUIClient* client, Core::self()->uiControllerInternal()->mainWindows()[a]->guiFactory()->clients()) { foreach(QAction* action, client->actionCollection()->actions()) { qCDebug(SHELL) << "transferring setting shortcut for" << action->objectName(); if(shortcuts.contains(action->objectName())) { action->setShortcut(shortcuts[action->objectName()]); } } } } } void MainWindow::shortcutsChanged() { KTextEditor::View *activeClient = Core::self()->documentController()->activeTextDocumentView(); if(!activeClient) return; foreach(IDocument * doc, Core::self()->documentController()->openDocuments()) { KTextEditor::Document *textDocument = doc->textDocument(); if (textDocument) { foreach(KTextEditor::View *client, textDocument->views()) { if (client != activeClient) { client->reloadXML(); } } } } } void MainWindow::initialize() { KStandardAction::keyBindings(this, SLOT(configureShortcuts()), actionCollection()); setupGUI( KXmlGuiWindow::ToolBar | KXmlGuiWindow::Create | KXmlGuiWindow::Save ); Core::self()->partController()->addManagedTopLevelWidget(this); qCDebug(SHELL) << "Adding plugin-added connection"; connect( Core::self()->pluginController(), &IPluginController::pluginLoaded, d, &MainWindowPrivate::addPlugin); connect( Core::self()->pluginController(), &IPluginController::pluginUnloaded, d, &MainWindowPrivate::removePlugin); connect( Core::self()->partController(), &IPartController::activePartChanged, d, &MainWindowPrivate::activePartChanged); connect( this, &MainWindow::activeViewChanged, d, &MainWindowPrivate::changeActiveView); foreach(IPlugin* plugin, Core::self()->pluginController()->loadedPlugins()) d->addPlugin(plugin); guiFactory()->addClient(Core::self()->sessionController()); guiFactory()->addClient(Core::self()->sourceFormatterControllerInternal()); // Needed to re-plug the actions from the sessioncontroller as xmlguiclients don't // seem to remember which actions where plugged in. Core::self()->sessionController()->plugActions(); d->setupGui(); //Queued so we process it with some delay, to make sure the rest of the UI has already adapted connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, &MainWindow::updateActiveDocumentConnection, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentClosed, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateCaption, Qt::QueuedConnection); connect(Core::self()->sessionController()->activeSession(), &ISession::sessionUpdated, this, &MainWindow::updateCaption); connect(Core::self()->documentController(), &IDocumentController::documentOpened, this, &MainWindow::updateTabColor, Qt::QueuedConnection); connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateTabColor, Qt::QueuedConnection); connect(this, &Sublime::MainWindow::viewAdded, this, &MainWindow::updateAllTabColors); connect(Core::self()->projectController(), &ProjectController::projectOpened, this, &MainWindow::updateAllTabColors, Qt::QueuedConnection); updateCaption(); } void MainWindow::cleanup() { } void MainWindow::setVisible( bool visible ) { KXmlGuiWindow::setVisible( visible ); emit finishedLoading(); } bool MainWindow::queryClose() { if (!Core::self()->documentControllerInternal()->saveAllDocumentsForWindow(this, IDocument::Default)) return false; return Sublime::MainWindow::queryClose(); } void MainWindow::updateActiveDocumentConnection(IDocument* document) { disconnect(d->activeDocumentReadWriteConnection); - d->activeDocumentReadWriteConnection = connect(document->textDocument(), &KTextEditor::Document::readWriteChanged, this, &MainWindow::updateCaption); + if (auto textDocument = document->textDocument()) { + d->activeDocumentReadWriteConnection = connect(textDocument, &KTextEditor::Document::readWriteChanged, + this, &MainWindow::updateCaption); + } } void MainWindow::updateCaption() { const auto activeSession = Core::self()->sessionController()->activeSession(); QString title = activeSession ? activeSession->description() : QString(); if(area()->activeView()) { if(!title.isEmpty()) title += QLatin1String(" - [ "); Sublime::Document* doc = area()->activeView()->document(); Sublime::UrlDocument* urlDoc = dynamic_cast(doc); if(urlDoc) title += Core::self()->projectController()->prettyFileName(urlDoc->url(), KDevelop::IProjectController::FormatPlain); else title += doc->title(); auto activeDocument = Core::self()->documentController()->activeDocument(); if (activeDocument && activeDocument->textDocument() && !activeDocument->textDocument()->isReadWrite()) title += i18n(" (read only)"); title += QLatin1String(" ]"); } setCaption(title); } void MainWindow::updateAllTabColors() { auto documentController = Core::self()->documentController(); if (!documentController) return; const auto defaultColor = ::defaultColor(palette()); - if (UiConfig::colorizeByProject()){ + if (UiConfig::colorizeByProject()) { + QHash viewColors; foreach (auto container, containers()) { - foreach (auto view, container->views()) { + auto views = container->views(); + viewColors.reserve(views.size()); + viewColors.clear(); + foreach (auto view, views) { const auto urlDoc = qobject_cast(view->document()); if (urlDoc) { - const auto color = colorForDocument(urlDoc->url(), palette()); - container->setTabColor(view, color.isValid() ? color : defaultColor); + viewColors[view] = colorForDocument(urlDoc->url(), palette(), defaultColor); } } + container->setTabColors(viewColors); } } else { foreach (auto container, containers()) { container->resetTabColors(defaultColor); } } } void MainWindow::updateTabColor(IDocument* doc) { if (!UiConfig::self()->colorizeByProject()) return; - const auto defaultColor = ::defaultColor(palette()); - const auto color = colorForDocument(doc->url(), palette()); + const auto color = colorForDocument(doc->url(), palette(), defaultColor(palette())); foreach (auto container, containers()) { foreach (auto view, container->views()) { const auto urlDoc = qobject_cast(view->document()); if (urlDoc && urlDoc->url() == doc->url()) { - container->setTabColor(view, color.isValid() ? color : defaultColor); + container->setTabColor(view, color); } } } } void MainWindow::registerStatus(QObject* status) { d->registerStatus(status); } void MainWindow::initializeStatusBar() { d->setupStatusBar(); } void MainWindow::showErrorMessage(const QString& message, int timeout) { d->showErrorMessage(message, timeout); } void MainWindow::tabContextMenuRequested(Sublime::View* view, QMenu* menu) { Sublime::MainWindow::tabContextMenuRequested(view, menu); d->tabContextMenuRequested(view, menu); } void MainWindow::tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int tab) { d->tabToolTipRequested(view, container, tab); } void MainWindow::dockBarContextMenuRequested(Qt::DockWidgetArea area, const QPoint& position) { d->dockBarContextMenuRequested(area, position); } void MainWindow::newTabRequested() { Sublime::MainWindow::newTabRequested(); d->fileNew(); } diff --git a/shell/sessioncontroller.cpp b/shell/sessioncontroller.cpp index 04c0253f0a..c072b7a51a 100644 --- a/shell/sessioncontroller.cpp +++ b/shell/sessioncontroller.cpp @@ -1,689 +1,689 @@ /* This file is part of KDevelop Copyright 2008 Andreas Pakulat Copyright 2010 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 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 "sessioncontroller.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 "session.h" #include "core.h" #include "uicontroller.h" #include "sessiondialog.h" #include "shellextension.h" #include "sessionlock.h" #include "sessionchooserdialog.h" #include "debug.h" #include #include #include #include #include namespace KDevelop { namespace { int argc = 0; char** argv = 0; }; void SessionController::setArguments(int _argc, char** _argv) { argc = _argc; argv = _argv; } static QStringList standardArguments() { QStringList ret; for(int a = 0; a < argc; ++a) { QString arg = QString::fromLocal8Bit(argv[a]); if(arg.startsWith(QLatin1String("-graphicssystem")) || arg.startsWith(QLatin1String("-style"))) { ret << '-' + arg; if(a+1 < argc) ret << QString::fromLocal8Bit(argv[a+1]); } } return ret; } class SessionControllerPrivate : public QObject { Q_OBJECT public: SessionControllerPrivate( SessionController* s ) : q(s) , activeSession(0) , grp(0) { } ~SessionControllerPrivate() override { } Session* findSessionForName( const QString& name ) const { foreach( Session* s, sessionActions.keys() ) { if( s->name() == name ) return s; } return 0; } Session* findSessionForId(QString idString) { QUuid id(idString); foreach( Session* s, sessionActions.keys() ) { if( s->id() == id) return s; } return 0; } void newSession() { qsrand(QDateTime::currentDateTimeUtc().toTime_t()); Session* session = new Session( QUuid::createUuid().toString() ); KProcess::startDetached(ShellExtension::getInstance()->binaryPath(), QStringList() << QStringLiteral("-s") << session->id().toString() << standardArguments()); delete session; #if 0 //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); #endif } void configureSessions() { SessionDialog dlg(ICore::self()->uiController()-> activeMainWindow()); dlg.exec(); } void deleteCurrentSession() { int choice = KMessageBox::warningContinueCancel(Core::self()->uiController()->activeMainWindow(), i18n("The current session and all contained settings will be deleted. The projects will stay unaffected. Do you really want to continue?")); if(choice == KMessageBox::Continue) { q->deleteSessionFromDisk(sessionLock); q->emitQuitSession(); } } void renameSession() { QDialog dialog; dialog.setWindowTitle(i18n("Rename Session")); auto mainLayout = new QVBoxLayout(&dialog); QGroupBox box; QHBoxLayout layout(&box); box.setTitle(i18n("New Session Name")); QLineEdit edit; layout.addWidget(&edit); mainLayout->addWidget(&box); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); mainLayout->addWidget(buttonBox); edit.setText(q->activeSession()->name()); edit.setFocus(); if(dialog.exec() == QDialog::Accepted) { static_cast(q->activeSession())->setName(edit.text()); } } bool loadSessionExternally( Session* s ) { Q_ASSERT( s ); KProcess::startDetached(ShellExtension::getInstance()->binaryPath(), QStringList() << QStringLiteral("-s") << s->id().toString() << standardArguments()); return true; } TryLockSessionResult activateSession( Session* s ) { Q_ASSERT( s ); activeSession = s; TryLockSessionResult result = SessionController::tryLockSession( s->id().toString()); if( !result.lock ) { activeSession = 0; return result; } Q_ASSERT(s->id().toString() == result.lock->id()); sessionLock = result.lock; KConfigGroup grp = KSharedConfig::openConfig()->group( SessionController::cfgSessionGroup() ); grp.writeEntry( SessionController::cfgActiveSessionEntry(), s->id().toString() ); grp.sync(); if (Core::self()->setupFlags() & Core::NoUi) return result; QHash::iterator it = sessionActions.find(s); Q_ASSERT( it != sessionActions.end() ); (*it)->setCheckable(true); (*it)->setChecked(true); for(it = sessionActions.begin(); it != sessionActions.end(); ++it) { if(it.key() != s) (*it)->setCheckable(false); } return result; } void loadSessionFromAction( QAction* a ) { foreach( Session* s, sessionActions.keys() ) { if( s->id() == QUuid( a->data().toString() ) && s != activeSession ) { loadSessionExternally( s ); break; } } } void addSession( Session* s ) { if (Core::self()->setupFlags() & Core::NoUi) { sessionActions[s] = 0; return; } QAction* a = new QAction( grp ); a->setText( s->description() ); a->setCheckable( false ); a->setData( s->id().toString() ); sessionActions[s] = a; q->actionCollection()->addAction( "session_"+s->id().toString(), a ); q->unplugActionList( QStringLiteral("available_sessions") ); q->plugActionList( QStringLiteral("available_sessions"), grp->actions() ); connect( s, &Session::sessionUpdated, this, &SessionControllerPrivate::sessionUpdated ); sessionUpdated( s ); } SessionController* q; QHash sessionActions; ISession* activeSession; QActionGroup* grp; ISessionLock::Ptr sessionLock; static QString sessionBaseDirectory() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +'/'+ qApp->applicationName() + "/sessions/"; } QString ownSessionDirectory() const { Q_ASSERT(activeSession); return q->sessionDirectory( activeSession->id().toString() ); } private slots: void sessionUpdated( KDevelop::ISession* s ) { sessionActions[static_cast( s )]->setText( KStringHandler::rsqueeze(s->description()) ); } }; SessionController::SessionController( QObject *parent ) : QObject( parent ), d(new SessionControllerPrivate(this)) { setObjectName(QStringLiteral("SessionController")); setComponentName(QStringLiteral("kdevsession"), QStringLiteral("KDevSession")); setXMLFile(QStringLiteral("kdevsessionui.rc")); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/SessionController"), this, QDBusConnection::ExportScriptableSlots ); if (Core::self()->setupFlags() & Core::NoUi) return; QAction* action = actionCollection()->addAction( QStringLiteral("new_session"), this, SLOT(newSession()) ); action->setText( i18nc("@action:inmenu", "Start New Session") ); action->setToolTip( i18nc("@info:tooltip", "Start a new KDevelop instance with an empty session") ); action->setIcon(QIcon::fromTheme(QStringLiteral("window-new"))); action = actionCollection()->addAction( QStringLiteral("rename_session"), this, SLOT(renameSession()) ); action->setText( i18n("Rename Current Session...") ); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); action = actionCollection()->addAction( QStringLiteral("delete_session"), this, SLOT(deleteCurrentSession()) ); action->setText( i18n("Delete Current Session...") ); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); action = actionCollection()->addAction( QStringLiteral("quit"), this, SIGNAL(quitSession()) ); action->setText( i18n("Quit") ); action->setMenuRole( QAction::NoRole ); // OSX: prevent QT from hiding this due to conflict with 'Quit KDevelop...' actionCollection()->setDefaultShortcut( action, Qt::CTRL | Qt::Key_Q ); action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); #if 0 action = actionCollection()->addAction( "configure_sessions", this, SLOT(configureSessions()) ); action->setText( i18n("Configure Sessions...") ); action->setToolTip( i18n("Create/Delete/Activate Sessions") ); action->setWhatsThis( i18n( "Shows a dialog to Create/Delete Sessions and set a new active session." ) ); #endif d->grp = new QActionGroup( this ); connect( d->grp, &QActionGroup::triggered, this, [&] (QAction* a) { d->loadSessionFromAction(a); } ); } SessionController::~SessionController() { delete d; } void SessionController::startNewSession() { d->newSession(); } void SessionController::cleanup() { if (d->activeSession) { Q_ASSERT(d->activeSession->id().toString() == d->sessionLock->id()); if (d->activeSession->isTemporary()) { deleteSessionFromDisk(d->sessionLock); } d->activeSession = 0; } d->sessionLock.clear(); qDeleteAll(d->sessionActions); d->sessionActions.clear(); } void SessionController::initialize( const QString& session ) { QDir sessiondir( SessionControllerPrivate::sessionBaseDirectory() ); foreach( const QString& s, sessiondir.entryList( QDir::AllDirs | QDir::NoDotAndDotDot ) ) { QUuid id( s ); if( id.isNull() ) continue; // Only create sessions for directories that represent proper uuid's Session* ses = new Session( id.toString(), this ); //Delete sessions that have no name and are empty if( ses->containedProjects().isEmpty() && ses->name().isEmpty() && (session.isEmpty() || (ses->id().toString() != session && ses->name() != session)) ) { TryLockSessionResult result = tryLockSession(s); if (result.lock) { deleteSessionFromDisk(result.lock); } delete ses; } else { d->addSession( ses ); } } loadDefaultSession( session ); } ISession* SessionController::activeSession() const { return d->activeSession; } ISessionLock::Ptr SessionController::activeSessionLock() const { return d->sessionLock; } void SessionController::loadSession( const QString& nameOrId ) { d->loadSessionExternally( session( nameOrId ) ); } QList SessionController::sessionNames() const { QStringList l; foreach( const Session* s, d->sessionActions.keys() ) { l << s->name(); } return l; } QList< const KDevelop::Session* > SessionController::sessions() const { QList< const KDevelop::Session* > ret; foreach( const Session* s, d->sessionActions.keys() ) { ret << s; } return ret; } Session* SessionController::createSession( const QString& name ) { Session* s; if(name.startsWith('{')) { s = new Session( QUuid(name).toString() ); }else{ qsrand(QDateTime::currentDateTimeUtc().toTime_t()); s = new Session( QUuid::createUuid().toString() ); s->setName( name ); } d->addSession( s ); return s; } void SessionController::deleteSession( const ISessionLock::Ptr& lock ) { Session* s = session(lock->id()); QHash::iterator it = d->sessionActions.find(s); Q_ASSERT( it != d->sessionActions.end() ); unplugActionList( QStringLiteral("available_sessions") ); actionCollection()->removeAction(*it); if (d->grp) { // happens in unit tests d->grp->removeAction(*it); plugActionList( QStringLiteral("available_sessions"), d->grp->actions() ); } if (s == d->activeSession) { d->activeSession = nullptr; } deleteSessionFromDisk(lock); emit sessionDeleted( s->id().toString() ); d->sessionActions.remove(s); delete s; } void SessionController::deleteSessionFromDisk( const ISessionLock::Ptr& lock ) { qCDebug(SHELL) << "Deleting session:" << lock->id(); - QDir(sessionDirectory(lock->id())).removeRecursively(); + static_cast(lock.data())->removeFromDisk(); ItemRepositoryRegistry::deleteRepositoryFromDisk( lock ); } void SessionController::loadDefaultSession( const QString& session ) { QString load = session; if (load.isEmpty()) { KConfigGroup grp = KSharedConfig::openConfig()->group( cfgSessionGroup() ); load = grp.readEntry( cfgActiveSessionEntry(), "default" ); } // Iteratively try to load the session, asking user what to do in case of failure // If showForceOpenDialog() returns empty string, stop trying Session* s = 0; do { s = this->session( load ); if( !s ) { s = createSession( load ); } TryLockSessionResult result = d->activateSession( s ); if( result.lock ) { Q_ASSERT(d->activeSession == s); Q_ASSERT(d->sessionLock = result.lock); break; } load = handleLockedSession( s->name(), s->id().toString(), result.runInfo ); } while( !load.isEmpty() ); } Session* SessionController::session( const QString& nameOrId ) const { Session* ret = d->findSessionForName( nameOrId ); if(ret) return ret; return d->findSessionForId( nameOrId ); } QString SessionController::cloneSession( const QString& nameOrid ) { Session* origSession = session( nameOrid ); qsrand(QDateTime::currentDateTimeUtc().toTime_t()); QUuid id = QUuid::createUuid(); auto copyJob = KIO::copy(QUrl::fromLocalFile(sessionDirectory(origSession->id().toString())), QUrl::fromLocalFile(sessionDirectory( id.toString()))); KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow()); copyJob->exec(); Session* newSession = new Session( id.toString() ); newSession->setName( i18n( "Copy of %1", origSession->name() ) ); d->addSession(newSession); return newSession->name(); } void SessionController::plugActions() { unplugActionList( QStringLiteral("available_sessions") ); plugActionList( QStringLiteral("available_sessions"), d->grp->actions() ); } QString SessionController::cfgSessionGroup() { return QStringLiteral("Sessions"); } QString SessionController::cfgActiveSessionEntry() { return QStringLiteral("Active Session ID"); } QList SessionController::availableSessionInfo() { return availableSessionInfos().toList(); } SessionInfos SessionController::availableSessionInfos() { SessionInfos sessionInfos; foreach( const QString& sessionId, QDir( SessionControllerPrivate::sessionBaseDirectory() ).entryList( QDir::AllDirs ) ) { if( !QUuid( sessionId ).isNull() ) { sessionInfos << Session::parse( sessionId ); } } return sessionInfos; } QString SessionController::sessionDirectory(const QString& sessionId) { return SessionControllerPrivate::sessionBaseDirectory() + sessionId; } TryLockSessionResult SessionController::tryLockSession(const QString& id) { return SessionLock::tryLockSession(id, true); } bool SessionController::isSessionRunning(const QString& id) { return sessionRunInfo(id).isRunning; } SessionRunInfo SessionController::sessionRunInfo(const QString& id) { return SessionLock::tryLockSession(id, false).runInfo; } QString SessionController::showSessionChooserDialog(QString headerText, bool onlyRunning) { ///FIXME: move this code into sessiondialog.cpp QListView* view = new QListView; QLineEdit* filter = new QLineEdit; filter->setClearButtonEnabled( true ); filter->setPlaceholderText(i18n("Search")); QStandardItemModel* model = new QStandardItemModel(view); QSortFilterProxyModel *proxy = new QSortFilterProxyModel(model); proxy->setSourceModel(model); proxy->setFilterKeyColumn( 1 ); proxy->setFilterCaseSensitivity( Qt::CaseInsensitive ); connect(filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString); SessionChooserDialog dialog(view, proxy, filter); view->setEditTriggers(QAbstractItemView::NoEditTriggers); QVBoxLayout layout(dialog.mainWidget()); if(!headerText.isEmpty()) { QLabel* heading = new QLabel(headerText); QFont font = heading->font(); font.setBold(true); heading->setFont(font); layout.addWidget(heading); } model->setColumnCount(3); model->setHeaderData(0, Qt::Horizontal,i18n("Identity")); model->setHeaderData(1, Qt::Horizontal, i18n("Contents")); model->setHeaderData(2, Qt::Horizontal,i18n("State")); view->setModel(proxy); view->setModelColumn(1); QHBoxLayout* filterLayout = new QHBoxLayout(); filterLayout->addWidget(new QLabel(i18n("Filter:"))); filterLayout->addWidget(filter); layout.addLayout(filterLayout); layout.addWidget(view); filter->setFocus(); int row = 0; QString defaultSession = KSharedConfig::openConfig()->group( cfgSessionGroup() ).readEntry( cfgActiveSessionEntry(), "default" ); foreach(const KDevelop::SessionInfo& si, KDevelop::SessionController::availableSessionInfos()) { if ( si.name.isEmpty() && si.projects.isEmpty() ) { continue; } bool running = KDevelop::SessionController::isSessionRunning(si.uuid.toString()); if(onlyRunning && !running) continue; model->setItem(row, 0, new QStandardItem(si.uuid.toString())); model->setItem(row, 1, new QStandardItem(si.description)); model->setItem(row, 2, new QStandardItem); ++row; } model->sort(1); if(!onlyRunning) { model->setItem(row, 0, new QStandardItem); model->setItem(row, 1, new QStandardItem(QIcon::fromTheme(QStringLiteral("window-new")), i18n("Create New Session"))); } dialog.updateState(); dialog.mainWidget()->layout()->setContentsMargins(0,0,0,0); const QModelIndex defaultSessionIndex = model->match(model->index(0, 0), Qt::DisplayRole, defaultSession, 1, Qt::MatchExactly).value(0); view->selectionModel()->setCurrentIndex(proxy->mapFromSource(defaultSessionIndex), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); view->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); ///@todo We need a way to get a proper size-hint from the view, but unfortunately, that only seems possible after the view was shown. dialog.resize(QSize(900, 600)); if(dialog.exec() != QDialog::Accepted) { return QString(); } QModelIndex selected = view->selectionModel()->currentIndex(); if (!selected.isValid()) return QString(); const QString selectedSessionId = selected.sibling(selected.row(), 0).data().toString(); if (selectedSessionId.isEmpty()) { // "Create New Session" item selected, return a fresh UUID qsrand(QDateTime::currentDateTimeUtc().toTime_t()); return QUuid::createUuid().toString(); } return selectedSessionId; } QString SessionController::handleLockedSession( const QString& sessionName, const QString& sessionId, const SessionRunInfo& runInfo ) { return SessionLock::handleLockedSession(sessionName, sessionId, runInfo); } QString SessionController::sessionDir() { if( !activeSession() ) return QString(); return d->ownSessionDirectory(); } QString SessionController::sessionName() { if(!activeSession()) return QString(); return activeSession()->description(); } } #include "sessioncontroller.moc" #include "moc_sessioncontroller.cpp" diff --git a/shell/sessionlock.cpp b/shell/sessionlock.cpp index 11016f5b7f..9a69a5b285 100644 --- a/shell/sessionlock.cpp +++ b/shell/sessionlock.cpp @@ -1,216 +1,225 @@ /* * 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 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 "sessionlock.h" #include "sessioncontroller.h" #include #include #include #include #include +#include using namespace KDevelop; namespace { QString lockFileForSession( const QString& id ) { - return SessionController::sessionDirectory( id ) + "/lock"; + return SessionController::sessionDirectory( id ) + QLatin1String("/lock"); } QString dBusServiceNameForSession( const QString& id ) { // We remove starting "{" and ending "}" from the string UUID representation // as D-Bus apparently doesn't allow them in service names - return QStringLiteral( "org.kdevelop.kdevplatform-lock-" ) + QString( id ).mid( 1, id.size() - 2 ); + return QStringLiteral( "org.kdevelop.kdevplatform-lock-" ) + id.midRef( 1, id.size() - 2 ); } /// Force-removes the lock-file. void forceRemoveLockfile(const QString& lockFilename) { if( QFile::exists( lockFilename ) ) { QFile::remove( lockFilename ); } } } TryLockSessionResult SessionLock::tryLockSession(const QString& sessionId, bool doLocking) { ///FIXME: if this is hit, someone tried to lock a non-existing session /// this should be fixed by using a proper data type distinct from /// QString for id's, i.e. QUuid or similar. Q_ASSERT(QFile::exists(SessionController::sessionDirectory( sessionId ))); /* * We've got two locking mechanisms here: D-Bus unique service name (based on the session id) * and a plain lockfile (QLockFile). * The latter is required to get the appname/pid of the locking instance * in case if it's stale/hanging/crashed (to make user know which PID he needs to kill). * D-Bus mechanism is the primary one. * * Since there is a kind of "logic tree", the code is a bit hard. */ const QString service = dBusServiceNameForSession( sessionId ); QDBusConnection connection = QDBusConnection::sessionBus(); QDBusConnectionInterface* connectionInterface = connection.interface(); const QString lockFilename = lockFileForSession( sessionId ); QSharedPointer lockFile(new QLockFile( lockFilename )); bool canLockDBus = connectionInterface && !connectionInterface->isServiceRegistered( service ); bool lockedDBus = false; // Lock D-Bus if we can and we need to if( doLocking && canLockDBus ) { lockedDBus = connection.registerService( service ); } // Attempt to lock file, despite the possibility to do so and presence of the request (doLocking) // This is required as QLockFile::getLockInfo() works only after QLockFile::lock() is called bool lockResult = lockFile->tryLock(); SessionRunInfo runInfo; if (lockResult) { // Unlock immediately if we shouldn't have locked it if( !lockedDBus ) { lockFile->unlock(); } } else { // If locking failed, retrieve the lock's metadata lockFile->getLockInfo(&runInfo.holderPid, &runInfo.holderHostname, &runInfo.holderApp ); if( lockedDBus ) { // Since the lock-file is secondary, try to force-lock it if D-Bus locking succeeded forceRemoveLockfile(lockFilename); lockResult = lockFile->tryLock(); Q_ASSERT(lockResult); } } // Set the result by D-Bus status if (doLocking && lockedDBus) { return TryLockSessionResult(QSharedPointer(new SessionLock(sessionId, lockFile))); } else { runInfo.isRunning = !canLockDBus; return TryLockSessionResult(runInfo); } } QString SessionLock::id() { return m_sessionId; } SessionLock::SessionLock(const QString& sessionId, const QSharedPointer& lockFile) : m_sessionId(sessionId) , m_lockFile(lockFile) { Q_ASSERT(lockFile->isLocked()); } SessionLock::~SessionLock() { m_lockFile->unlock(); bool unregistered = QDBusConnection::sessionBus().unregisterService( dBusServiceNameForSession(m_sessionId) ); Q_ASSERT(unregistered); Q_UNUSED(unregistered); } +void SessionLock::removeFromDisk() +{ + Q_ASSERT(m_lockFile->isLocked()); + // unlock first to prevent warnings: "Could not remove our own lock file ..." + m_lockFile->unlock(); + QDir(SessionController::sessionDirectory(m_sessionId)).removeRecursively(); +} + QString SessionLock::handleLockedSession(const QString& sessionName, const QString& sessionId, const SessionRunInfo& runInfo) { if( !runInfo.isRunning ) { return sessionId; } // try to make the locked session active { // The timeout for "ensureVisible" call // Leave it sufficiently low to avoid waiting for hung instances. static const int timeout_ms = 1000; QDBusMessage message = QDBusMessage::createMethodCall( dBusServiceNameForSession(sessionId), QStringLiteral("/kdevelop/MainWindow"), QStringLiteral("org.kdevelop.MainWindow"), QStringLiteral("ensureVisible") ); QDBusMessage reply = QDBusConnection::sessionBus().call( message, QDBus::Block, timeout_ms ); if( reply.type() == QDBusMessage::ReplyMessage ) { QTextStream out(stdout); out << i18nc( "@info:shell", "made running %1 instance (PID: %2) visible", runInfo.holderApp, runInfo.holderPid ) << endl; return QString(); } else { qWarning() << i18nc("@info:shell", "running %1 instance (PID: %2) is apparently hung", runInfo.holderApp, runInfo.holderPid); } } // otherwise ask the user whether we should retry QString problemDescription = i18nc("@info", "The given application did not respond to a DBUS call, " "it may have crashed or is hanging."); QString problemHeader; if( runInfo.holderPid != -1 ) { problemHeader = i18nc("@info", "Failed to lock the session %1, " "already locked by %2 on %3 (PID %4).", sessionName, runInfo.holderApp, runInfo.holderHostname, runInfo.holderPid); } else { problemHeader = i18nc("@info", "Failed to lock the session %1 (lock-file unavailable).", sessionName); } QString problemResolution = i18nc("@info", "

Please, close the offending application instance " "or choose another session to launch.

"); QString errmsg = "

" + problemHeader + "
" + problemDescription + "

" + problemResolution; KGuiItem retry = KStandardGuiItem::cont(); retry.setText(i18nc("@action:button", "Retry startup")); KGuiItem choose = KStandardGuiItem::configure(); choose.setText(i18nc("@action:button", "Choose another session")); KGuiItem cancel = KStandardGuiItem::quit(); int ret = KMessageBox::warningYesNoCancel(0, errmsg, i18nc("@title:window", "Failed to Lock Session %1", sessionName), retry, choose, cancel); switch( ret ) { case KMessageBox::Yes: return sessionId; break; case KMessageBox::No: { QString errmsg = i18nc("@info", "The session %1 is already active in another running instance.", sessionName); return SessionController::showSessionChooserDialog(errmsg); break; } case KMessageBox::Cancel: default: break; } return QString(); } diff --git a/shell/sessionlock.h b/shell/sessionlock.h index 706065ba97..35fc89d015 100644 --- a/shell/sessionlock.h +++ b/shell/sessionlock.h @@ -1,63 +1,68 @@ /* * 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 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 . */ #ifndef SESSIONLOCK_H #define SESSIONLOCK_H #include #include namespace KDevelop { struct SessionRunInfo; struct TryLockSessionResult; class SessionLock : public ISessionLock { public: /** * \copydoc SessionController::tryLockSession * * @param doLocking whether to actually try to lock or whether to only get * information about whether the session could be locked */ static TryLockSessionResult tryLockSession(const QString& sessionId, bool doLocking); /** * \copydoc SessionController::handleLockedSession */ static QString handleLockedSession( const QString& sessionName, const QString& sessionId, const SessionRunInfo& runInfo ); virtual QString id() override; virtual ~SessionLock(); + /** + * Release the lock file and remove the session directory from disk. + */ + void removeFromDisk(); + private: SessionLock(const QString& sessionId, const QSharedPointer& lockFile); QString m_sessionId; QSharedPointer m_lockFile; }; } #endif // SESSIONLOCK_H diff --git a/sublime/container.cpp b/sublime/container.cpp index 15bcbc7a56..fbabdb6583 100644 --- a/sublime/container.cpp +++ b/sublime/container.cpp @@ -1,641 +1,651 @@ /*************************************************************************** * Copyright 2006-2009 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 "container.h" #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "view.h" #include "document.h" #include namespace Sublime { // struct ContainerPrivate class ContainerTabBar : public QTabBar { Q_OBJECT public: ContainerTabBar(Container* container) : QTabBar(container), m_container(container) { installEventFilter(this); } bool event(QEvent* ev) override { if(ev->type() == QEvent::ToolTip) { ev->accept(); int tab = tabAt(mapFromGlobal(QCursor::pos())); if(tab != -1) { m_container->showTooltipForTab(tab); } return true; } return QTabBar::event(ev); } void mousePressEvent(QMouseEvent* event) override { if (event->button() == Qt::MidButton) { // just close on midbutton, drag can still be done with left mouse button int tab = tabAt(mapFromGlobal(QCursor::pos())); if (tab != -1) { emit tabCloseRequested(tab); } return; } QTabBar::mousePressEvent(event); } bool eventFilter(QObject* obj, QEvent* event) override { if (obj != this) { return QObject::eventFilter(obj, event); } // TODO Qt6: Move to mouseDoubleClickEvent when fixme in qttabbar.cpp is resolved // see "fixme Qt 6: move to mouseDoubleClickEvent(), here for BC reasons." in qtabbar.cpp if (event->type() == QEvent::MouseButtonDblClick) { // block tabBarDoubleClicked signals with RMB, see https://bugs.kde.org/show_bug.cgi?id=356016 auto mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::MidButton) { return true; } } return QObject::eventFilter(obj, event); } Q_SIGNALS: void newTabRequested(); private: Container* m_container; }; bool sortViews(const View* const lhs, const View* const rhs) { return lhs->document()->title().compare(rhs->document()->title(), Qt::CaseInsensitive) < 0; } struct ContainerPrivate { QBoxLayout* layout; - QMap viewForWidget; + QHash viewForWidget; ContainerTabBar *tabBar; QStackedWidget *stack; KSqueezedTextLabel *fileNameCorner; QLabel *fileStatus; KSqueezedTextLabel *statusCorner; QPointer leftCornerWidget; QToolButton* documentListButton; QMenu* documentListMenu; - QMap documentListActionForView; + QHash documentListActionForView; /** * Updates the context menu which is shown when * the document list button in the tab bar is clicked. * * It shall build a popup menu which contains all currently * enabled views using the title their document provides. */ void updateDocumentListPopupMenu() { qDeleteAll(documentListActionForView); documentListActionForView.clear(); documentListMenu->clear(); // create a lexicographically sorted list QVector views; views.reserve(viewForWidget.size()); foreach(View* view, viewForWidget){ views << view; } std::sort(views.begin(), views.end(), sortViews); foreach(View* view, views) { QAction* action = documentListMenu->addAction(view->document()->title()); action->setData(QVariant::fromValue(view)); documentListActionForView[view] = action; action->setIcon(view->document()->icon()); ///FIXME: push this code somehow into shell, such that we can access the project model for /// icons and also get a neat, short path like the document switcher. } } }; class UnderlinedLabel: public KSqueezedTextLabel { Q_OBJECT public: UnderlinedLabel(QTabBar *tabBar, QWidget* parent = 0) :KSqueezedTextLabel(parent), m_tabBar(tabBar) { } protected: void paintEvent(QPaintEvent *ev) override { if (m_tabBar->isVisible() && m_tabBar->count() > 0) { QStylePainter p(this); QStyleOptionTabBarBase optTabBase; optTabBase.init(m_tabBar); optTabBase.shape = m_tabBar->shape(); optTabBase.tabBarRect = m_tabBar->rect(); optTabBase.tabBarRect.moveRight(0); QStyleOptionTab tabOverlap; tabOverlap.shape = m_tabBar->shape(); int overlap = style()->pixelMetric(QStyle::PM_TabBarBaseOverlap, &tabOverlap, m_tabBar); if( overlap > 0 ) { QRect rect; rect.setRect(0, height()-overlap, width(), overlap); optTabBase.rect = rect; } if( m_tabBar->drawBase() ) { p.drawPrimitive(QStyle::PE_FrameTabBarBase, optTabBase); } } KSqueezedTextLabel::paintEvent(ev); } QTabBar *m_tabBar; }; class StatusLabel: public UnderlinedLabel { Q_OBJECT public: StatusLabel(QTabBar *tabBar, QWidget* parent = 0): UnderlinedLabel(tabBar, parent) { setAlignment(Qt::AlignRight | Qt::AlignVCenter); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); } QSize minimumSizeHint() const override { QRect rect = style()->itemTextRect(fontMetrics(), QRect(), Qt::AlignRight, true, i18n("Line: 00000 Col: 000")); rect.setHeight(m_tabBar->height()); return rect.size(); } }; // class Container Container::Container(QWidget *parent) :QWidget(parent), d(new ContainerPrivate()) { KAcceleratorManager::setNoAccel(this); QBoxLayout *l = new QBoxLayout(QBoxLayout::TopToBottom, this); l->setMargin(0); l->setSpacing(0); d->layout = new QBoxLayout(QBoxLayout::LeftToRight); d->layout->setMargin(0); d->layout->setSpacing(0); d->documentListMenu = new QMenu(this); d->documentListButton = new QToolButton(this); d->documentListButton->setIcon(QIcon::fromTheme(QStringLiteral("format-list-unordered"))); d->documentListButton->setMenu(d->documentListMenu); d->documentListButton->setPopupMode(QToolButton::InstantPopup); d->documentListButton->setAutoRaise(true); d->documentListButton->setToolTip(i18n("Show sorted list of opened documents")); d->documentListButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); d->layout->addWidget(d->documentListButton); d->tabBar = new ContainerTabBar(this); d->tabBar->setContextMenuPolicy(Qt::CustomContextMenu); d->layout->addWidget(d->tabBar); d->fileStatus = new QLabel( this ); d->fileStatus->setFixedSize( QSize( 16, 16 ) ); d->layout->addWidget(d->fileStatus); d->fileNameCorner = new UnderlinedLabel(d->tabBar, this); d->layout->addWidget(d->fileNameCorner); d->statusCorner = new StatusLabel(d->tabBar, this); d->layout->addWidget(d->statusCorner); l->addLayout(d->layout); d->stack = new QStackedWidget(this); l->addWidget(d->stack); connect(d->tabBar, &ContainerTabBar::currentChanged, this, &Container::widgetActivated); connect(d->tabBar, &ContainerTabBar::tabCloseRequested, this, static_cast(&Container::requestClose)); connect(d->tabBar, &ContainerTabBar::newTabRequested, this, &Container::newTabRequested); connect(d->tabBar, &ContainerTabBar::tabMoved, this, &Container::tabMoved); connect(d->tabBar, &ContainerTabBar::customContextMenuRequested, this, &Container::contextMenu); connect(d->tabBar, &ContainerTabBar::tabBarDoubleClicked, this, &Container::doubleClickTriggered); connect(d->documentListMenu, &QMenu::triggered, this, &Container::documentListActionTriggered); setTabBarHidden(!configTabBarVisible()); d->tabBar->setTabsClosable(true); d->tabBar->setMovable(true); d->tabBar->setExpanding(false); d->tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); } bool Container::configTabBarVisible() { KConfigGroup group = KSharedConfig::openConfig()->group("UiSettings"); return group.readEntry("TabBarVisibility", 1); } void Container::setLeftCornerWidget(QWidget* widget) { if(d->leftCornerWidget.data() == widget) { if(d->leftCornerWidget) d->leftCornerWidget.data()->setParent(0); }else{ delete d->leftCornerWidget.data(); d->leftCornerWidget.clear(); } d->leftCornerWidget = widget; if(!widget) return; widget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); d->layout->insertWidget(0, widget); widget->show(); } Container::~Container() { delete d; } QList Container::views() const { return d->viewForWidget.values(); } void Container::requestClose(int idx) { emit requestClose(widget(idx)); } void Container::widgetActivated(int idx) { if (idx < 0) return; if (QWidget* w = d->stack->widget(idx)) { Sublime::View* view = d->viewForWidget.value(w); if(view) emit activateView(view); } } void Container::addWidget(View *view, int position) { QWidget *w = view->widget(this); int idx = 0; if (position != -1) { idx = d->stack->insertWidget(position, w); } else idx = d->stack->addWidget(w); d->tabBar->insertTab(idx, view->document()->statusIcon(), view->document()->title()); Q_ASSERT(view); d->viewForWidget[w] = view; // Update document list context menu. This has to be called before // setCurrentWidget, because we call the status icon and title update slots // already, which in turn need the document list menu to be setup. d->updateDocumentListPopupMenu(); setCurrentWidget(d->stack->currentWidget()); // This fixes a strange layouting bug, that could be reproduced like this: Open a few files in KDevelop, activate the rightmost tab. // Then temporarily switch to another area, and then switch back. After that, the tab-bar was gone. // The problem could only be fixed by closing/opening another view. d->tabBar->setMinimumHeight(d->tabBar->sizeHint().height()); connect(view, &View::statusChanged, this, &Container::statusChanged); connect(view->document(), &Document::statusIconChanged, this, &Container::statusIconChanged); connect(view->document(), &Document::titleChanged, this, &Container::documentTitleChanged); } void Container::statusChanged(Sublime::View* view) { d->statusCorner->setText(view->viewStatus()); } void Container::statusIconChanged(Document* doc) { - QMapIterator it = d->viewForWidget; + QHashIterator it = d->viewForWidget; while (it.hasNext()) { if (it.next().value()->document() == doc) { d->fileStatus->setPixmap( doc->statusIcon().pixmap( QSize( 16,16 ) ) ); int tabIndex = d->stack->indexOf(it.key()); if (tabIndex != -1) { d->tabBar->setTabIcon(tabIndex, doc->statusIcon()); } // Update the document title's menu associated action // using the View* index map Q_ASSERT(d->documentListActionForView.contains(it.value())); d->documentListActionForView[it.value()]->setIcon(doc->icon()); break; } } } void Container::documentTitleChanged(Sublime::Document* doc) { - QMapIterator it = d->viewForWidget; + QHashIterator it = d->viewForWidget; while (it.hasNext()) { Sublime::View* view = it.next().value(); if (view->document() == doc) { if (currentView() == view) { d->fileNameCorner->setText( doc->title(Document::Extended) ); } int tabIndex = d->stack->indexOf(it.key()); if (tabIndex != -1) { d->tabBar->setTabText(tabIndex, doc->title()); } // Update document list popup title Q_ASSERT(d->documentListActionForView.contains(view)); d->documentListActionForView[view]->setText(doc->title()); break; } } } int Container::count() const { return d->stack->count(); } QWidget* Container::currentWidget() const { return d->stack->currentWidget(); } void Container::setCurrentWidget(QWidget* w) { d->stack->setCurrentWidget(w); //prevent from emitting activateView() signal on tabbar active tab change //this function is called from MainWindow::activateView() //which does the activation without any additional signals d->tabBar->blockSignals(true); d->tabBar->setCurrentIndex(d->stack->indexOf(w)); d->tabBar->blockSignals(false); if (View* view = viewForWidget(w)) { statusChanged(view); if (!d->tabBar->isVisible()) { // repaint icon and document title only in tabbar-less mode // tabbar will do repainting for us statusIconChanged( view->document() ); documentTitleChanged( view->document() ); } } } QWidget* Container::widget(int i) const { return d->stack->widget(i); } int Container::indexOf(QWidget* w) const { return d->stack->indexOf(w); } void Container::removeWidget(QWidget *w) { if (w) { int widgetIdx = d->stack->indexOf(w); d->stack->removeWidget(w); d->tabBar->removeTab(widgetIdx); if (d->tabBar->currentIndex() != -1 && !d->tabBar->isVisible()) { // repaint icon and document title only in tabbar-less mode // tabbar will do repainting for us View* view = currentView(); if( view ) { statusIconChanged( view->document() ); documentTitleChanged( view->document() ); } } View* view = d->viewForWidget.take(w); if (view) { disconnect(view->document(), &Document::titleChanged, this, &Container::documentTitleChanged); disconnect(view->document(), &Document::statusIconChanged, this, &Container::statusIconChanged); disconnect(view, &View::statusChanged, this, &Container::statusChanged); // Update document list context menu Q_ASSERT(d->documentListActionForView.contains(view)); delete d->documentListActionForView.take(view); } } } bool Container::hasWidget(QWidget *w) { return d->stack->indexOf(w) != -1; } View *Container::viewForWidget(QWidget *w) const { return d->viewForWidget.value(w); } void Container::setTabBarHidden(bool hide) { if (hide) { d->tabBar->hide(); d->fileNameCorner->show(); d->fileStatus->show(); } else { d->fileNameCorner->hide(); d->fileStatus->hide(); d->tabBar->show(); } View* v = currentView(); if (v) { documentTitleChanged(v->document()); } } void Container::resetTabColors(const QColor& color) { for (int i = 0; i < count(); i++){ d->tabBar->setTabTextColor(i, color); } } void Container::setTabColor(const View* view, const QColor& color) { for (int i = 0; i < count(); i++){ if (view == viewForWidget(widget(i))) { d->tabBar->setTabTextColor(i, color); } } } +void Container::setTabColors(const QHash& colors) +{ + for (int i = 0; i < count(); i++) { + auto view = viewForWidget(widget(i)); + auto color = colors[view]; + if (color.isValid()) { + d->tabBar->setTabTextColor(i, color); + } + } +} + void Container::tabMoved(int from, int to) { QWidget *w = d->stack->widget(from); d->stack->removeWidget(w); d->stack->insertWidget(to, w); d->viewForWidget[w]->notifyPositionChanged(to); } void Container::contextMenu( const QPoint& pos ) { QWidget* senderWidget = qobject_cast(sender()); Q_ASSERT(senderWidget); int currentTab = d->tabBar->tabAt(pos); QMenu menu; Sublime::View* view = viewForWidget(widget(currentTab)); emit tabContextMenuRequested(view, &menu); menu.addSeparator(); QAction* copyPathAction = nullptr; QAction* closeTabAction = nullptr; QAction* closeOtherTabsAction = nullptr; if (view) { copyPathAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Filename")); menu.addSeparator(); closeTabAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close File")); closeOtherTabsAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close Other Files")); } QAction* closeAllTabsAction = menu.addAction( QIcon::fromTheme(QStringLiteral("document-close")), i18n( "Close All Files" ) ); QAction* triggered = menu.exec(senderWidget->mapToGlobal(pos)); if (triggered) { if ( triggered == closeTabAction ) { requestClose(currentTab); } else if ( triggered == closeOtherTabsAction ) { // activate the remaining tab widgetActivated(currentTab); // first get the widgets to be closed since otherwise the indices will be wrong QList otherTabs; for ( int i = 0; i < count(); ++i ) { if ( i != currentTab ) { otherTabs << widget(i); } } // finally close other tabs foreach( QWidget* tab, otherTabs ) { requestClose(tab); } } else if ( triggered == closeAllTabsAction ) { // activate last tab widgetActivated(count() - 1); // close all for ( int i = 0; i < count(); ++i ) { requestClose(widget(i)); } } else if( triggered == copyPathAction ) { auto view = viewForWidget( widget( currentTab ) ); auto urlDocument = qobject_cast( view->document() ); if( urlDocument ) { QApplication::clipboard()->setText( urlDocument->url().toString() ); } } // else the action was handled by someone else } } void Container::showTooltipForTab(int tab) { emit tabToolTipRequested(viewForWidget(widget(tab)), this, tab); } bool Container::isCurrentTab(int tab) const { return d->tabBar->currentIndex() == tab; } QRect Container::tabRect(int tab) const { return d->tabBar->tabRect(tab).translated(d->tabBar->mapToGlobal(QPoint(0, 0))); } void Container::doubleClickTriggered(int tab) { if (tab == -1) { emit newTabRequested(); } else { emit tabDoubleClicked(viewForWidget(widget(tab))); } } void Container::documentListActionTriggered(QAction* action) { Sublime::View* view = action->data().value< Sublime::View* >(); Q_ASSERT(view); QWidget* widget = d->viewForWidget.key(view); Q_ASSERT(widget); setCurrentWidget(widget); } Sublime::View* Container::currentView() const { return d->viewForWidget.value(widget( d->tabBar->currentIndex() )); } } #include "container.moc" diff --git a/sublime/container.h b/sublime/container.h index 7f15997bef..42b37e9f29 100644 --- a/sublime/container.h +++ b/sublime/container.h @@ -1,118 +1,119 @@ /*************************************************************************** * Copyright 2006-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. * ***************************************************************************/ #ifndef KDEVPLATFORM_SUBLIMECONTAINER_H #define KDEVPLATFORM_SUBLIMECONTAINER_H #include #include "urldocument.h" #include "sublimeexport.h" class QMenu; class QBoxLayout; class QPaintEvent; namespace Sublime { class View; class Document; /** @short Container for the widgets. This container is placed inside mainwindow splitters to show widgets for views in the area. */ class KDEVPLATFORMSUBLIME_EXPORT Container: public QWidget { Q_OBJECT public: explicit Container(QWidget *parent = 0); ~Container() override; /**Adds the widget for given @p view to the container.*/ void addWidget(Sublime::View* view, int position = -1); /**Removes the widget from the container.*/ void removeWidget(QWidget *w); /** @return true if widget is placed inside this container.*/ bool hasWidget(QWidget *w); QList views() const; int count() const; QWidget *currentWidget() const; void setCurrentWidget(QWidget *w); QWidget *widget(int i) const; int indexOf(QWidget *w) const; View *viewForWidget(QWidget *w) const; void setTabBarHidden(bool hide); void setTabColor(const View* view, const QColor& color); + void setTabColors(const QHash& colors); void resetTabColors(const QColor& color); /** Adds a corner widget to the left of this containers tab-bar. To remove it again, just delete it. * The ownership otherwise goes to the container. */ void setLeftCornerWidget(QWidget* widget); void showTooltipForTab(int tab); bool isCurrentTab(int tab) const; /// @return Rect in global position of the tab identified by index @p tab QRect tabRect(int tab) const; static bool configTabBarVisible(); Q_SIGNALS: void activateView(Sublime::View* view); void requestClose(QWidget *w); /** * This signal is emitted whenever the users double clicks on the free * space next to the tab bar. Typically, a new document should be * created. */ void newTabRequested(); void tabContextMenuRequested(Sublime::View* view, QMenu* menu); /** * @p view The view represented by the tab that was hovered * @p Container The tab container that triggered the event * @p idx The index of the tab that was hovered */ void tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int idx); void tabDoubleClicked(Sublime::View* view); private Q_SLOTS: void widgetActivated(int idx); void documentTitleChanged(Sublime::Document* doc); void statusIconChanged(Sublime::Document*); void statusChanged(Sublime::View *view); void requestClose(int idx); void tabMoved(int from, int to); void contextMenu(const QPoint&); void doubleClickTriggered(int tab); void documentListActionTriggered(QAction*); private: Sublime::View* currentView() const; struct ContainerPrivate * const d; }; } #endif diff --git a/vcs/vcsstatusinfo.cpp b/vcs/vcsstatusinfo.cpp index f672bd3769..8a01ef49db 100644 --- a/vcs/vcsstatusinfo.cpp +++ b/vcs/vcsstatusinfo.cpp @@ -1,107 +1,112 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 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 "vcsstatusinfo.h" +#include #include #include #include #include namespace KDevelop { class VcsStatusInfoPrivate { public: int state; QUrl url; }; VcsStatusInfo::VcsStatusInfo() : d( new VcsStatusInfoPrivate) { d->state = VcsStatusInfo::ItemUnknown; } VcsStatusInfo::~VcsStatusInfo() { delete d; } VcsStatusInfo::VcsStatusInfo( const VcsStatusInfo& rhs ) : d(new VcsStatusInfoPrivate) { d->state = rhs.d->state; d->url = rhs.d->url; } VcsStatusInfo& VcsStatusInfo::operator=( const VcsStatusInfo& rhs) { if(this == &rhs) return *this; d->state = rhs.d->state; d->url = rhs.d->url; return *this; } bool VcsStatusInfo::operator==( const KDevelop::VcsStatusInfo& rhs) const { return ( d->state == rhs.d->state && d->url == rhs.d->url ); } bool VcsStatusInfo::operator!=( const KDevelop::VcsStatusInfo& rhs) const { return !(operator==(rhs)); } void VcsStatusInfo::setUrl( const QUrl& url ) { d->url = url; } void VcsStatusInfo::setExtendedState( int newstate ) { d->state = newstate; } void VcsStatusInfo::setState( VcsStatusInfo::State state ) { d->state = state; } int VcsStatusInfo::extendedState() const { return d->state; } QUrl VcsStatusInfo::url() const { return d->url; } VcsStatusInfo::State VcsStatusInfo::state() const { return VcsStatusInfo::State(d->state); } - } +QDebug operator<<(QDebug s, const KDevelop::VcsStatusInfo& statusInfo) +{ + s.nospace() << statusInfo.state() << "@" << statusInfo.url(); + return s.space(); +} diff --git a/vcs/vcsstatusinfo.h b/vcs/vcsstatusinfo.h index 3e03b9ff56..d0e8736cf6 100644 --- a/vcs/vcsstatusinfo.h +++ b/vcs/vcsstatusinfo.h @@ -1,100 +1,102 @@ /* This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * Copyright 2007 Matthew Woehlke * * 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. */ #ifndef KDEVPLATFORM_VCSSTATUSINFO_H #define KDEVPLATFORM_VCSSTATUSINFO_H #include #include #include "vcsexport.h" class QString; class QStringList; namespace KDevelop { /** * * Class that encapsulates status information * for one local url. * * The extendedState functions allow to transport * extended status information * * Note for VCS implementations: * If you want to use this class in queued signal/slot connections * you should call qRegisterMetaType() * in the constructor of the plugin class */ class KDEVPLATFORMVCS_EXPORT VcsStatusInfo { public: /** * Status of a local file */ enum State { ItemUnknown = 0 /**< No VCS information about a file is known (or file is not under VCS control). */, ItemUpToDate = 1 /**< Item was updated or it is already at up to date version. */, ItemAdded = 2 /**< Item was added to the repository but not committed. */, ItemModified = 3 /**< Item was modified locally. */, ItemDeleted = 4 /**< Item is scheduled to be deleted. */, ItemHasConflicts = 8 /**< Local version has conflicts that need to be resolved before commit. */, ItemUserState = 1000 /**< special states for individual vcs implementations should use this as base. */ }; VcsStatusInfo(); virtual ~VcsStatusInfo(); VcsStatusInfo(const VcsStatusInfo&); /** * retrieves the url of this status information item * @return the url */ QUrl url() const; /** * Change the url of this status information item * @param url the url */ void setUrl( const QUrl& ); VcsStatusInfo::State state() const; void setState( VcsStatusInfo::State ); int extendedState() const; void setExtendedState( int ); VcsStatusInfo& operator=( const VcsStatusInfo& rhs); bool operator==( const KDevelop::VcsStatusInfo& rhs) const; bool operator!=( const KDevelop::VcsStatusInfo& rhs) const; private: class VcsStatusInfoPrivate* d; }; } Q_DECLARE_METATYPE( KDevelop::VcsStatusInfo ) +KDEVPLATFORMVCS_EXPORT QDebug operator<<(QDebug s, const KDevelop::VcsStatusInfo& statusInfo); + #endif