diff --git a/language/duchain/declarationid.cpp b/language/duchain/declarationid.cpp index 0cdde0ff1f..ffd4c9f23d 100644 --- a/language/duchain/declarationid.cpp +++ b/language/duchain/declarationid.cpp @@ -1,206 +1,245 @@ /* This file is part of KDevelop 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 "declarationid.h" #include "ducontext.h" #include "topducontext.h" #include "duchain.h" #include "declaration.h" #include "persistentsymboltable.h" #include "instantiationinformation.h" #include "../editor/cursorinrevision.h" #include namespace KDevelop { DeclarationId::DeclarationId(const IndexedQualifiedIdentifier& id, uint additionalId, const IndexedInstantiationInformation& specialization) : m_indirectData{id, additionalId} , m_isDirect(false) , m_specialization(specialization) { } DeclarationId::DeclarationId(const IndexedDeclaration& decl, const IndexedInstantiationInformation& specialization) : m_directData(decl) , m_isDirect(true) , m_specialization(specialization) { } DeclarationId::DeclarationId(const DeclarationId& rhs) : m_isDirect(rhs.m_isDirect) , m_specialization(rhs.m_specialization) { if (!m_isDirect) { // IndexedQualifiedIdentifier doesn't like zero-initialization... new (&m_indirectData.identifier) IndexedQualifiedIdentifier(rhs.m_indirectData.identifier); m_indirectData.additionalIdentity = rhs.m_indirectData.additionalIdentity; } else { m_directData = rhs.m_directData; } } DeclarationId::~DeclarationId() { if (!m_isDirect) { m_indirectData.~Indirect(); } } DeclarationId& DeclarationId::operator=(const DeclarationId& rhs) { if (&rhs == this) return *this; m_isDirect = rhs.m_isDirect; m_specialization = rhs.m_specialization; if (!m_isDirect) { m_indirectData = rhs.m_indirectData; } else { m_directData = rhs.m_directData; } return *this; } bool DeclarationId::isDirect() const { return m_isDirect; } void DeclarationId::setSpecialization(const IndexedInstantiationInformation& spec) { m_specialization = spec; } IndexedInstantiationInformation DeclarationId::specialization() const { return m_specialization; } -void DeclarationId::iterateDeclarations(const TopDUContext* top, std::function func) const +KDevVarLengthArray DeclarationId::getDeclarations(const TopDUContext* top) const { + KDevVarLengthArray ret; + if(m_isDirect == false) { //Find the declaration by its qualified identifier and additionalIdentity - const QualifiedIdentifier id(m_indirectData.identifier); + QualifiedIdentifier id(m_indirectData.identifier); if(top) { //Do filtering PersistentSymbolTable::FilteredDeclarationIterator filter = PersistentSymbolTable::self().getFilteredDeclarations(id, top->recursiveImportIndices()); for(; filter; ++filter) { Declaration* decl = filter->data(); if(decl && m_indirectData.additionalIdentity == decl->additionalIdentity()) { - if (func(decl)) - break; + //Hit + ret.append(decl); } } }else{ //Just accept anything PersistentSymbolTable::Declarations decls = PersistentSymbolTable::self().getDeclarations(id); PersistentSymbolTable::Declarations::Iterator decl = decls.iterator(); for(; decl; ++decl) { const IndexedDeclaration& iDecl(*decl); - ///@todo think this over once we don't pull in all imported top-context any more + ///@todo think this over once we don't pull in all imported top-context any more //Don't trigger loading of top-contexts from here, it will create a lot of problems if((!DUChain::self()->isInMemory(iDecl.topContextIndex()))) continue; if(!top) { Declaration* decl = iDecl.data(); if(decl && m_indirectData.additionalIdentity == decl->additionalIdentity()) { //Hit - if (func(decl)) - break; + ret.append(decl); } } } } }else{ - func(m_directData.declaration()); + Declaration* decl = m_directData.declaration(); + if(decl) + ret.append(decl); } -} - -KDevVarLengthArray DeclarationId::getDeclarations(const TopDUContext* top) const -{ - KDevVarLengthArray ret; - - iterateDeclarations(top, [&ret](Declaration* decl){ - ret.append(decl); - return false; - }); if(!ret.isEmpty() && m_specialization.index()) { KDevVarLengthArray newRet; foreach (Declaration* decl, ret) { Declaration* specialized = decl->specialize(m_specialization, top ? top : decl->topContext()); if(specialized) newRet.append(specialized); } return newRet; } return ret; } Declaration* DeclarationId::getDeclaration(const TopDUContext* top, bool instantiateIfRequired) const { - Declaration* ret = nullptr; + Declaration* ret = 0; - iterateDeclarations(top, [&ret](Declaration* decl){ - ret = decl; - return !ret->isForwardDeclaration(); - }); + if(m_isDirect == false) { + //Find the declaration by its qualified identifier and additionalIdentity + QualifiedIdentifier id(m_indirectData.identifier); - if(ret && m_specialization.isValid()) - { - const TopDUContext* topContextForSpecialization = top; - if(!instantiateIfRequired) - topContextForSpecialization = 0; //If we don't want to instantiate new declarations, set the top-context to zero, so specialize(..) will only look-up - else if(!topContextForSpecialization) - topContextForSpecialization = ret->topContext(); + if(top) { + //Do filtering + PersistentSymbolTable::FilteredDeclarationIterator filter = + PersistentSymbolTable::self().getFilteredDeclarations(id, top->recursiveImportIndices()); + for(; filter; ++filter) { + Declaration* decl = filter->data(); + if(decl && m_indirectData.additionalIdentity == decl->additionalIdentity()) { + //Hit + ret = decl; + if(!ret->isForwardDeclaration()) + break; + } + } + }else{ + //Just accept anything + PersistentSymbolTable::Declarations decls = PersistentSymbolTable::self().getDeclarations(id); + PersistentSymbolTable::Declarations::Iterator decl = decls.iterator(); + for(; decl; ++decl) { + const IndexedDeclaration& iDecl(*decl); + + ///@todo think this over once we don't pull in all imported top-context any more + //Don't trigger loading of top-contexts from here, it will create a lot of problems + if((!DUChain::self()->isInMemory(iDecl.topContextIndex()))) + continue; - return ret->specialize(m_specialization, topContextForSpecialization); + if(!top) { + Declaration* decl = iDecl.data(); + if(decl && m_indirectData.additionalIdentity == decl->additionalIdentity()) { + //Hit + ret = decl; + if(!ret->isForwardDeclaration()) + break; + } + } + } + } + }else{ + //Find the declaration by m_topContext and m_declaration + ret = m_directData.declaration(); } - return ret; + + + if(ret) + { + if(m_specialization.isValid()) + { + const TopDUContext* topContextForSpecialization = top; + if(!instantiateIfRequired) + topContextForSpecialization = 0; //If we don't want to instantiate new declarations, set the top-context to zero, so specialize(..) will only look-up + else if(!topContextForSpecialization) + topContextForSpecialization = ret->topContext(); + + return ret->specialize(m_specialization, topContextForSpecialization); + }else{ + return ret; + } + }else + return 0; } QualifiedIdentifier DeclarationId::qualifiedIdentifier() const { if(!m_isDirect) { QualifiedIdentifier baseIdentifier = m_indirectData.identifier.identifier(); if(!m_specialization.index()) return baseIdentifier; return m_specialization.information().applyToIdentifier(baseIdentifier); } else { Declaration* decl = getDeclaration(0); if(decl) return decl->qualifiedIdentifier(); return QualifiedIdentifier(i18n("(unknown direct declaration)")); } return QualifiedIdentifier(i18n("(missing)")) + m_indirectData.identifier.identifier(); } } diff --git a/language/duchain/declarationid.h b/language/duchain/declarationid.h index 0d23e96731..775c3cf860 100644 --- a/language/duchain/declarationid.h +++ b/language/duchain/declarationid.h @@ -1,213 +1,210 @@ /* This file is part of KDevelop 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_DECLARATION_ID_H #define KDEVPLATFORM_DECLARATION_ID_H #include "indexeddeclaration.h" #include "identifier.h" #include "instantiationinformation.h" #include -#include //krazy:excludeall=dpointer class QString; namespace KDevelop { class Declaration; class TopDUContext; /** * \short Allows clearly identifying a Declaration. * * DeclarationId is needed to uniquely address Declarations that are in another top-context, * because there may be multiple parsed versions of a file. * * There are two forms of DeclarationId, one indirect and one direct. The direct form * holds a reference to the Declaration instance, whereas the indirect form stores the qualified * identifier and an additional index to disambiguate instances of multiple declarations with the same * identifier. * * Both forms also have a specialization index. It can be used in a language-specific way to pick other * versions of the declaration. When the declaration is found, Declaration::specialize() is called on * the found declaration with this value, and the returned value is the actually found declaration. * * \note This only works when the Declaration is in the symbol table. * */ class KDEVPLATFORMLANGUAGE_EXPORT DeclarationId { public: /** * Constructor for indirect access to a declaration. The resulting DeclarationId will not * have a direct reference to the Declaration, but will look it up as needed. * * \param id Identifier for this declaration id. * \param additionalId Additional index to disambiguate * \param specialization Specialization index (see class documentation). */ explicit DeclarationId(const IndexedQualifiedIdentifier& id = IndexedQualifiedIdentifier(), uint additionalId = 0, const IndexedInstantiationInformation& specialization = IndexedInstantiationInformation()); /** * Constructor for direct access to a declaration. The resulting DeclarationId will * directly reference the Declaration * * \param decl Declaration to reference. * \param specialization Specialization index (see class documentation). */ explicit DeclarationId(const IndexedDeclaration& decl, const IndexedInstantiationInformation& specialization = IndexedInstantiationInformation()); DeclarationId(const DeclarationId& rhs); ~DeclarationId(); DeclarationId& operator=(const DeclarationId& rhs); /** * Equality operator. * * \param rhs declaration identifier to compare. * \returns true if equal, otherwise false. */ bool operator==(const DeclarationId& rhs) const { if(m_isDirect != rhs.m_isDirect) return false; if(!m_isDirect) return m_indirectData.identifier == rhs.m_indirectData.identifier && m_indirectData.additionalIdentity == rhs.m_indirectData.additionalIdentity && m_specialization == rhs.m_specialization; else return m_directData == rhs.m_directData && m_specialization == rhs.m_specialization; } /** * Not equal operator. * * \param rhs declaration identifier to compare. * \returns true if not equal, otherwise false. */ bool operator!=(const DeclarationId& rhs) const { return !operator==(rhs); } /** * Determine whether this declaration identifier references a valid declaration. */ bool isValid() const { return (m_isDirect && m_directData.isValid()) || m_indirectData.identifier.isValid(); } /** * Hash function for this declaration identifier. * * \warning This may return different hashes for the same declaration, * depending on whether the id is direct or indirect, * and thus you cannot compare hashes for declaration equality (use operator==() instead) */ uint hash() const { if(m_isDirect) return KDevHash() << m_directData.hash() << m_specialization.index(); else return KDevHash() << m_indirectData.identifier.getIndex() << m_indirectData.additionalIdentity << m_specialization.index(); } /** * Retrieve the declaration, from the perspective of \a context. * In order to be retrievable, the declaration must be in the symbol table. * * \param context Context in which to search for the Declaration. * \param instantiateIfRequired Whether the declaration should be instantiated if required * \returns the referenced Declaration, or null if none was found. * */ Declaration* getDeclaration(const TopDUContext* context, bool instantiateIfRequired = true) const; /** * Same as getDeclaration(..), but returns all matching declarations if there are multiple. * This also returns found forward-declarations. */ KDevVarLengthArray getDeclarations(const TopDUContext* context) const; /** * Set the specialization index (see class documentation). * * \param specializationIndex the new specialization index. */ void setSpecialization(const IndexedInstantiationInformation& spec); /** * Retrieve the specialization index (see class documentation). * * \returns the specialization index. */ IndexedInstantiationInformation specialization() const; /** * Determine whether this DeclarationId directly references a Declaration by indices, * or if it uses identifiers and other data to reference the Declaration. * * \returns true if direct, false if indirect. */ bool isDirect() const; /** * Return the qualified identifier for this declaration. * * \warning This is relatively expensive, and not 100% correct in all cases(actually a top-context would be needed to resolve this correctly), * so avoid using this, except for debugging purposes. */ QualifiedIdentifier qualifiedIdentifier() const; private: - void iterateDeclarations(const TopDUContext* top, std::function func) const; - /// An indirect reference to the declaration, which uses the symbol-table for lookup. Should be preferred for all /// declarations that are in the symbol-table struct Indirect { IndexedQualifiedIdentifier identifier; /// Hash from signature, or similar. Used to disambiguate multiple declarations of the same name. uint additionalIdentity; Indirect& operator=(const Indirect& rhs) = default; }; union { Indirect m_indirectData; IndexedDeclaration m_directData; }; bool m_isDirect; // Can be used in a language-specific way to pick other versions of the declaration. // When the declaration is found, pickSpecialization is called on the found declaration // with this value, and the returned value is the actually found declaration. IndexedInstantiationInformation m_specialization; }; inline uint qHash(const KDevelop::DeclarationId& id) { return id.hash(); } } Q_DECLARE_TYPEINFO(KDevelop::DeclarationId, Q_MOVABLE_TYPE); #endif diff --git a/language/duchain/navigation/abstractnavigationcontext.cpp b/language/duchain/navigation/abstractnavigationcontext.cpp index 5c5e55e303..87baf17697 100644 --- a/language/duchain/navigation/abstractnavigationcontext.cpp +++ b/language/duchain/navigation/abstractnavigationcontext.cpp @@ -1,488 +1,493 @@ /* Copyright 2007 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 "abstractnavigationcontext.h" #include #include #include "abstractdeclarationnavigationcontext.h" #include "abstractnavigationwidget.h" #include "usesnavigationcontext.h" #include "../../../interfaces/icore.h" #include "../../../interfaces/idocumentcontroller.h" #include "../functiondeclaration.h" #include "../namespacealiasdeclaration.h" #include "../types/functiontype.h" #include "../types/structuretype.h" #include "util/debug.h" #include #include #include namespace KDevelop { void AbstractNavigationContext::setTopContext(KDevelop::TopDUContextPointer context) { m_topContext = context; } KDevelop::TopDUContextPointer AbstractNavigationContext::topContext() const { return m_topContext; } AbstractNavigationContext::AbstractNavigationContext( KDevelop::TopDUContextPointer topContext, AbstractNavigationContext* previousContext) : m_selectedLink(0), m_shorten(false), m_linkCount(-1), m_currentPositionLine(0), m_previousContext(previousContext), m_topContext(topContext) { } void AbstractNavigationContext::addExternalHtml( const QString& text ) { int lastPos = 0; int pos = 0; QString fileMark = QStringLiteral("KDEV_FILE_LINK{"); while( pos < text.length() && (pos = text.indexOf( fileMark, pos)) != -1 ) { modifyHtml() += text.mid(lastPos, pos-lastPos); pos += fileMark.length(); if( pos != text.length() ) { int fileEnd = text.indexOf('}', pos); if( fileEnd != -1 ) { QString file = text.mid( pos, fileEnd - pos ); pos = fileEnd + 1; const QUrl url = QUrl::fromUserInput(file); makeLink( url.fileName(), file, NavigationAction( url, KTextEditor::Cursor() ) ); } } lastPos = pos; } modifyHtml() += text.mid(lastPos, text.length()-lastPos); } void AbstractNavigationContext::makeLink( const QString& name, DeclarationPointer declaration, NavigationAction::Type actionType ) { NavigationAction action( declaration, actionType ); - QString targetId = QString::number((quint64)declaration.data() * actionType); - makeLink(name, targetId, action); + makeLink(name, QString(), action); } -QString AbstractNavigationContext::createLink(const QString& name, QString targetId, const NavigationAction& action) +QString AbstractNavigationContext::createLink(const QString& name, QString, const NavigationAction& action) { if(m_shorten) { //Do not create links in shortened mode, it's only for viewing return typeHighlight(name.toHtmlEscaped()); } - m_links[ targetId ] = action; + // NOTE: Since the by definition in the HTML standard some uri components + // are case-insensitive, we define a new lowercase link-id for each + // link. Otherwise Qt 5 seems to mess up the casing and the link + // cannot be matched when it's executed. + QString hrefId = QString("link_%1").arg(m_links.count()); + + m_links[ hrefId ] = action; m_intLinks[ m_linkCount ] = action; m_linkLines[ m_linkCount ] = m_currentLine; if(m_currentPositionLine == m_currentLine) { m_currentPositionLine = -1; m_selectedLink = m_linkCount; } QString str = name.toHtmlEscaped(); if( m_linkCount == m_selectedLink ) str = "" + str + ""; - QString ret = "" + str + ""; + QString ret = "" + str + ""; if( m_selectedLink == m_linkCount ) m_selectedLinkAction = action; ++m_linkCount; return ret; } void AbstractNavigationContext::makeLink( const QString& name, QString targetId, const NavigationAction& action) { modifyHtml() += createLink(name, targetId, action); } void AbstractNavigationContext::clear() { m_linkCount = 0; m_currentLine = 0; m_currentText.clear(); m_links.clear(); m_intLinks.clear(); m_linkLines.clear(); } NavigationContextPointer AbstractNavigationContext::executeLink (QString link) { if(!m_links.contains(link)) return NavigationContextPointer(this); return execute(m_links[link]); } NavigationContextPointer AbstractNavigationContext::executeKeyAction(QString key) { Q_UNUSED(key); return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::execute(const NavigationAction& action) { if(action.targetContext) return NavigationContextPointer(action.targetContext); if(action.type == NavigationAction::ExecuteKey) return executeKeyAction(action.key); if( !action.decl && (action.type != NavigationAction::JumpToSource || action.document.isEmpty()) ) { qCDebug(LANGUAGE) << "Navigation-action has invalid declaration" << endl; return NavigationContextPointer(this); } qRegisterMetaType("KTextEditor::Cursor"); switch( action.type ) { case NavigationAction::ExecuteKey: break; case NavigationAction::None: qCDebug(LANGUAGE) << "Tried to execute an invalid action in navigation-widget" << endl; break; case NavigationAction::NavigateDeclaration: { AbstractDeclarationNavigationContext* ctx = dynamic_cast(m_previousContext); if( ctx && ctx->declaration() == action.decl ) return NavigationContextPointer( m_previousContext ); return AbstractNavigationContext::registerChild(action.decl); } break; case NavigationAction::NavigateUses: { IContextBrowser* browser = ICore::self()->pluginController()->extensionForPlugin(); if (browser) { browser->showUses(action.decl); return NavigationContextPointer(this); } // fall-through } case NavigationAction::ShowUses: return registerChild(new UsesNavigationContext(action.decl.data(), this)); case NavigationAction::JumpToSource: { QUrl doc = action.document; KTextEditor::Cursor cursor = action.cursor; { DUChainReadLocker lock(DUChain::lock()); if(action.decl) { if(doc.isEmpty()) { doc = action.decl->url().toUrl(); /* if(action.decl->internalContext()) cursor = action.decl->internalContext()->range().start() + KTextEditor::Cursor(0, 1); else*/ cursor = action.decl->rangeInCurrentRevision().start(); } action.decl->activateSpecialization(); } } //This is used to execute the slot delayed in the event-loop, so crashes are avoided QMetaObject::invokeMethod( ICore::self()->documentController(), "openDocument", Qt::QueuedConnection, Q_ARG(QUrl, doc), Q_ARG(KTextEditor::Cursor, cursor) ); break; } case NavigationAction::ShowDocumentation: { auto doc = ICore::self()->documentationController()->documentationForDeclaration(action.decl.data()); ICore::self()->documentationController()->showDocumentation(doc); } break; } return NavigationContextPointer( this ); } void AbstractNavigationContext::setPreviousContext(KDevelop::AbstractNavigationContext* previous) { m_previousContext = previous; } NavigationContextPointer AbstractNavigationContext::registerChild( AbstractNavigationContext* context ) { m_children << NavigationContextPointer(context); return m_children.last(); } NavigationContextPointer AbstractNavigationContext::registerChild(DeclarationPointer declaration) { //We create a navigation-widget here, and steal its context.. evil ;) QScopedPointer navigationWidget(declaration->context()->createNavigationWidget(declaration.data())); if (AbstractNavigationWidget* abstractNavigationWidget = dynamic_cast(navigationWidget.data()) ) { NavigationContextPointer ret = abstractNavigationWidget->context(); ret->setPreviousContext(this); m_children << ret; return ret; } else { return NavigationContextPointer(this); } } const int lineJump = 3; void AbstractNavigationContext::down() { //Make sure link-count is valid if( m_linkCount == -1 ) html(); int fromLine = m_currentPositionLine; if(m_selectedLink >= 0 && m_selectedLink < m_linkCount) { if(fromLine == -1) fromLine = m_linkLines[m_selectedLink]; for(int newSelectedLink = m_selectedLink+1; newSelectedLink < m_linkCount; ++newSelectedLink) { if(m_linkLines[newSelectedLink] > fromLine && m_linkLines[newSelectedLink] - fromLine <= lineJump) { m_selectedLink = newSelectedLink; m_currentPositionLine = -1; return; } } } if(fromLine == -1) fromLine = 0; m_currentPositionLine = fromLine + lineJump; if(m_currentPositionLine > m_currentLine) m_currentPositionLine = m_currentLine; } void AbstractNavigationContext::up() { //Make sure link-count is valid if( m_linkCount == -1 ) html(); int fromLine = m_currentPositionLine; if(m_selectedLink >= 0 && m_selectedLink < m_linkCount) { if(fromLine == -1) fromLine = m_linkLines[m_selectedLink]; for(int newSelectedLink = m_selectedLink-1; newSelectedLink >= 0; --newSelectedLink) { if(m_linkLines[newSelectedLink] < fromLine && fromLine - m_linkLines[newSelectedLink] <= lineJump) { m_selectedLink = newSelectedLink; m_currentPositionLine = -1; return; } } } if(fromLine == -1) fromLine = m_currentLine; m_currentPositionLine = fromLine - lineJump; if(m_currentPositionLine < 0) m_currentPositionLine = 0; } void AbstractNavigationContext::nextLink() { //Make sure link-count is valid if( m_linkCount == -1 ) html(); m_currentPositionLine = -1; if( m_linkCount > 0 ) m_selectedLink = (m_selectedLink+1) % m_linkCount; } void AbstractNavigationContext::previousLink() { //Make sure link-count is valid if( m_linkCount == -1 ) html(); m_currentPositionLine = -1; if( m_linkCount > 0 ) { --m_selectedLink; if( m_selectedLink < 0 ) m_selectedLink += m_linkCount; } Q_ASSERT(m_selectedLink >= 0); } void AbstractNavigationContext::setPrefixSuffix( const QString& prefix, const QString& suffix ) { m_prefix = prefix; m_suffix = suffix; } NavigationContextPointer AbstractNavigationContext::back() { if(m_previousContext) return NavigationContextPointer(m_previousContext); else return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::accept() { if( m_selectedLink >= 0 && m_selectedLink < m_linkCount ) { NavigationAction action = m_intLinks[m_selectedLink]; return execute(action); } return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::accept(IndexedDeclaration decl) { if(decl.data()) { NavigationAction action(DeclarationPointer(decl.data()), NavigationAction::NavigateDeclaration); return execute(action); }else{ return NavigationContextPointer(this); } } NavigationContextPointer AbstractNavigationContext::acceptLink(const QString& link) { if( !m_links.contains(link) ) { qCDebug(LANGUAGE) << "Executed unregistered link " << link << endl; return NavigationContextPointer(this); } return execute(m_links[link]); } NavigationAction AbstractNavigationContext::currentAction() const { return m_selectedLinkAction; } QString AbstractNavigationContext::declarationKind(DeclarationPointer decl) { const AbstractFunctionDeclaration* function = dynamic_cast(decl.data()); QString kind; if( decl->isTypeAlias() ) kind = i18n("Typedef"); else if( decl->kind() == Declaration::Type ) { if( decl->type() ) kind = i18n("Class"); } else if( decl->kind() == Declaration::Instance ) { kind = i18n("Variable"); } else if ( decl->kind() == Declaration::Namespace ) { kind = i18n("Namespace"); } if( NamespaceAliasDeclaration* alias = dynamic_cast(decl.data()) ) { if( alias->identifier().isEmpty() ) kind = i18n("Namespace import"); else kind = i18n("Namespace alias"); } if(function) kind = i18n("Function"); if( decl->isForwardDeclaration() ) kind = i18n("Forward Declaration"); return kind; } QString AbstractNavigationContext::html(bool shorten) { m_shorten = shorten; return QString(); } bool AbstractNavigationContext::alreadyComputed() const { return !m_currentText.isEmpty(); } bool AbstractNavigationContext::isWidgetMaximized() const { return true; } QWidget* AbstractNavigationContext::widget() const { return 0; } ///Splits the string by the given regular expression, but keeps the split-matches at the end of each line static QStringList splitAndKeep(QString str, QRegExp regExp) { QStringList ret; int place = regExp.indexIn(str); while(place != -1) { ret << str.left(place + regExp.matchedLength()); str = str.mid(place + regExp.matchedLength()); place = regExp.indexIn(str); } ret << str; return ret; } void AbstractNavigationContext::addHtml(QString html) { QRegExp newLineRegExp("
|
"); foreach(const QString& line, splitAndKeep(html, newLineRegExp)) { m_currentText += line; if(line.indexOf(newLineRegExp) != -1) { ++m_currentLine; if(m_currentLine == m_currentPositionLine) { m_currentText += QStringLiteral(" <-> "); // ><-> is <-> } } } } QString AbstractNavigationContext::currentHtml() const { return m_currentText; } QString AbstractNavigationContext::fontSizePrefix(bool /*shorten*/) const { return QString(); } QString AbstractNavigationContext::fontSizeSuffix(bool /*shorten*/) const { return QString(); } QString Colorizer::operator() ( const QString& str ) const { QString ret = "" + str + ""; if( m_formatting & Fixed ) ret = ""+ret+""; if ( m_formatting & Bold ) ret = ""+ret+""; if ( m_formatting & Italic ) ret = ""+ret+""; return ret; } const Colorizer AbstractNavigationContext::typeHighlight(QStringLiteral("006000")); const Colorizer AbstractNavigationContext::errorHighlight(QStringLiteral("990000")); const Colorizer AbstractNavigationContext::labelHighlight(QStringLiteral("000000")); const Colorizer AbstractNavigationContext::codeHighlight(QStringLiteral("005000")); const Colorizer AbstractNavigationContext::propertyHighlight(QStringLiteral("009900")); const Colorizer AbstractNavigationContext::navigationHighlight(QStringLiteral("000099")); const Colorizer AbstractNavigationContext::importantHighlight(QStringLiteral("000000"), Colorizer::Bold | Colorizer::Italic); const Colorizer AbstractNavigationContext::commentHighlight(QStringLiteral("303030")); const Colorizer AbstractNavigationContext::nameHighlight(QStringLiteral("000000"), Colorizer::Bold); } diff --git a/language/duchain/navigation/problemnavigationcontext.cpp b/language/duchain/navigation/problemnavigationcontext.cpp index 1cc00d607d..df4b601848 100644 --- a/language/duchain/navigation/problemnavigationcontext.cpp +++ b/language/duchain/navigation/problemnavigationcontext.cpp @@ -1,119 +1,128 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "problemnavigationcontext.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; -ProblemNavigationContext::ProblemNavigationContext(ProblemPointer problem): m_problem(problem) +ProblemNavigationContext::ProblemNavigationContext(const IProblem::Ptr& problem) + : m_problem(problem) + , m_widget(nullptr) { - m_widget = 0; - QExplicitlySharedDataPointer< IAssistant > solution = problem->solutionAssistant(); if(solution && !solution->actions().isEmpty()) { m_widget = new QWidget; QHBoxLayout* layout = new QHBoxLayout(m_widget); RichTextPushButton* button = new RichTextPushButton; // button->setPopupMode(QToolButton::InstantPopup); if(!solution->title().isEmpty()) button->setHtml(i18n("Solve: %1", solution->title())); else button->setHtml(i18n("Solve")); QMenu* menu = new QMenu; menu->setFocusPolicy(Qt::NoFocus); foreach(IAssistantAction::Ptr action, solution->actions()) { menu->addAction(action->toKAction()); } button->setMenu(menu); layout->addWidget(button); layout->setAlignment(button, Qt::AlignLeft); m_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); } } ProblemNavigationContext::~ProblemNavigationContext() { delete m_widget; } QWidget* ProblemNavigationContext::widget() const { return m_widget; } bool ProblemNavigationContext::isWidgetMaximized() const { return false; } QString ProblemNavigationContext::name() const { return i18n("Problem"); } QString ProblemNavigationContext::html(bool shorten) { clear(); m_shorten = shorten; modifyHtml() += QStringLiteral("

"); modifyHtml() += i18n("Problem in %1:
", m_problem->sourceString()); modifyHtml() += m_problem->description().toHtmlEscaped(); modifyHtml() += QStringLiteral("
"); modifyHtml() += "" + m_problem->explanation().toHtmlEscaped() + ""; const QVector diagnostics = m_problem->diagnostics(); if (!diagnostics.isEmpty()) { modifyHtml() += QStringLiteral("
"); DUChainReadLocker lock; for (auto diagnostic : diagnostics) { - const DocumentRange range = diagnostic->finalLocation(); - Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()); - modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString())); modifyHtml() += diagnostic->description(); + const DocumentRange range = diagnostic->finalLocation(); + Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()); if (declaration) { - modifyHtml() += QStringLiteral("
"); - makeLink(declaration->toString(), KDevelop::DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); + modifyHtml() += i18n("
See: "); + makeLink(declaration->toString(), DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); modifyHtml() += i18n(" in "); - makeLink(QStringLiteral("%1 :%2").arg(declaration->url().toUrl().fileName()).arg(declaration->rangeInCurrentRevision().start().line()+1), KDevelop::DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); + makeLink(QStringLiteral("%1 :%2") + .arg(declaration->url().toUrl().fileName()) + .arg(declaration->rangeInCurrentRevision().start().line() + 1), + DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); + } else if (range.start().isValid()) { + modifyHtml() += i18n("
See: "); + const auto url = range.document.toUrl(); + makeLink(QStringLiteral("%1 :%2") + .arg(url.fileName()) + .arg(range.start().line() + 1), + url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start())); } modifyHtml() += QStringLiteral("
"); } } modifyHtml() += QStringLiteral("

"); return currentHtml(); } diff --git a/language/duchain/navigation/problemnavigationcontext.h b/language/duchain/navigation/problemnavigationcontext.h index 70728fe62c..c1bc01cc2e 100644 --- a/language/duchain/navigation/problemnavigationcontext.h +++ b/language/duchain/navigation/problemnavigationcontext.h @@ -1,46 +1,49 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PROBLEMNAVIGATIONCONTEXT_H #define KDEVPLATFORM_PROBLEMNAVIGATIONCONTEXT_H +#include #include -#include #include #include namespace KDevelop { class KDEVPLATFORMLANGUAGE_EXPORT ProblemNavigationContext : public AbstractNavigationContext { Q_OBJECT public: - explicit ProblemNavigationContext(KDevelop::ProblemPointer problem); + explicit ProblemNavigationContext(const IProblem::Ptr& problem); ~ProblemNavigationContext(); + virtual QString name() const override; virtual QString html(bool shorten = false) override; virtual QWidget* widget() const override; virtual bool isWidgetMaximized() const override; + private: + IProblem::Ptr m_problem; + QPointer m_widget; - ProblemPointer m_problem; }; } #endif // KDEVPLATFORM_PROBLEMNAVIGATIONCONTEXT_H diff --git a/plugins/contextbrowser/browsemanager.cpp b/plugins/contextbrowser/browsemanager.cpp index 7b5aa58a01..1c5cc70576 100644 --- a/plugins/contextbrowser/browsemanager.cpp +++ b/plugins/contextbrowser/browsemanager.cpp @@ -1,317 +1,342 @@ /* * This file is part of KDevelop * * Copyright 2008 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "browsemanager.h" #include #include #include +#include +#include #include #include #include #include #include "contextbrowserview.h" +#include +#include #include #include #include #include #include #include +#include #include #include #include #include "contextbrowser.h" #include "debug.h" using namespace KDevelop; using namespace KTextEditor; EditorViewWatcher::EditorViewWatcher(QObject* parent) : QObject(parent) { connect(ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &EditorViewWatcher::documentCreated); foreach(KDevelop::IDocument* document, ICore::self()->documentController()->openDocuments()) documentCreated(document); } void EditorViewWatcher::documentCreated( KDevelop::IDocument* document ) { KTextEditor::Document* textDocument = document->textDocument(); if(textDocument) { connect(textDocument, &Document::viewCreated, this, &EditorViewWatcher::viewCreated); foreach(KTextEditor::View* view, textDocument->views()) { Q_ASSERT(view->parentWidget()); addViewInternal(view); } } } void EditorViewWatcher::addViewInternal(KTextEditor::View* view) { m_views << view; viewAdded(view); connect(view, &View::destroyed, this, &EditorViewWatcher::viewDestroyed); } void EditorViewWatcher::viewAdded(KTextEditor::View*) { } void EditorViewWatcher::viewDestroyed(QObject* view) { m_views.removeAll(static_cast(view)); } void EditorViewWatcher::viewCreated(KTextEditor::Document* /*doc*/, KTextEditor::View* view) { Q_ASSERT(view->parentWidget()); addViewInternal(view); } QList EditorViewWatcher::allViews() { return m_views; } void BrowseManager::eventuallyStartDelayedBrowsing() { + avoidMenuAltFocus(); + if(m_browsingByKey && m_browingStartedInView) emit startDelayedBrowsing(m_browingStartedInView); } BrowseManager::BrowseManager(ContextBrowserPlugin* controller) : QObject(controller) , m_plugin(controller) , m_browsingByKey(0) , m_watcher(this) { m_delayedBrowsingTimer = new QTimer(this); m_delayedBrowsingTimer->setSingleShot(true); connect(m_delayedBrowsingTimer, &QTimer::timeout, this, &BrowseManager::eventuallyStartDelayedBrowsing); foreach(KTextEditor::View* view, m_watcher.allViews()) viewAdded(view); } KTextEditor::View* viewFromWidget(QWidget* widget) { if(!widget) return 0; KTextEditor::View* view = qobject_cast(widget); if(view) return view; else return viewFromWidget(widget->parentWidget()); } bool BrowseManager::eventFilter(QObject * watched, QEvent * event) { QWidget* widget = qobject_cast(watched); Q_ASSERT(widget); - KTextEditor::View* view = viewFromWidget(widget); - if(!view) - return false; QKeyEvent* keyEvent = dynamic_cast(event); const int browseKey = Qt::Key_Control; const int magicModifier = Qt::Key_Alt; + KTextEditor::View* view = viewFromWidget(widget); + //Eventually start key-browsing if(keyEvent && (keyEvent->key() == browseKey || keyEvent->key() == magicModifier) && !m_browsingByKey && keyEvent->type() == QEvent::KeyPress) { + m_delayedBrowsingTimer->start(300); // always start the timer, to get consistent behavior regarding the ALT key and the menu activation m_browsingByKey = keyEvent->key(); + if(!view) { + return false; + } + if(keyEvent->key() == magicModifier) { if(dynamic_cast(view) && dynamic_cast(view)->isCompletionActive()) { - //Do nothing, completion is active. + //Completion is active. + avoidMenuAltFocus(); }else{ - m_delayedBrowsingTimer->start(300); m_browingStartedInView = view; } } } + if(!view) { + return false; + } + QFocusEvent* focusEvent = dynamic_cast(event); //Eventually stop key-browsing if((keyEvent && m_browsingByKey && keyEvent->key() == m_browsingByKey && keyEvent->type() == QEvent::KeyRelease) || (focusEvent && focusEvent->lostFocus())) { m_browsingByKey = 0; emit stopDelayedBrowsing(); } QMouseEvent* mouseEvent = dynamic_cast(event); if(mouseEvent) { if (mouseEvent->type() == QEvent::MouseButtonPress && mouseEvent->button() == Qt::XButton1) { m_plugin->historyPrevious(); return true; } if (mouseEvent->type() == QEvent::MouseButtonPress && mouseEvent->button() == Qt::XButton2) { m_plugin->historyNext(); return true; } } if(!m_browsingByKey) { resetChangedCursor(); return false; } if(mouseEvent) { KTextEditor::View* iface = dynamic_cast(view); if(!iface) { qCDebug(PLUGIN_CONTEXTBROWSER) << "Update kdelibs for the browsing-mode to work"; return false; } QPoint coordinatesInView = widget->mapTo(view, mouseEvent->pos()); KTextEditor::Cursor textCursor = iface->coordinatesToCursor(coordinatesInView); if(textCursor.isValid()) { ///@todo find out why this is needed, fix the code in kate if(textCursor.column() > 0) textCursor.setColumn(textCursor.column()-1); QUrl viewUrl = view->document()->url(); auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); QPair jumpTo; //Step 1: Look for a special language object(Macro, included header, etc.) foreach (const auto language, languages) { jumpTo = language->specialLanguageObjectJumpCursor(viewUrl, KTextEditor::Cursor(textCursor)); if(jumpTo.first.isValid() && jumpTo.second.isValid()) break; //Found a special object to jump to } //Step 2: Look for a declaration/use if(!jumpTo.first.isValid() || !jumpTo.second.isValid()) { Declaration* foundDeclaration = 0; KDevelop::DUChainReadLocker lock( DUChain::lock() ); foundDeclaration = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor(textCursor)) ); if(foundDeclaration && (foundDeclaration->url().toUrl() == view->document()->url()) && foundDeclaration->range().contains( foundDeclaration->transformToLocalRevision(KTextEditor::Cursor(textCursor)))) { ///A declaration was clicked directly. Jumping to it is useless, so jump to the definition or something useful bool foundBetter = false; Declaration* definition = FunctionDefinition::definition(foundDeclaration); if(definition) { foundDeclaration = definition; foundBetter = true; } ForwardDeclaration* forward = dynamic_cast(foundDeclaration); if(forward) { TopDUContext* standardContext = DUChainUtils::standardContextForUrl(view->document()->url()); if(standardContext) { Declaration* resolved = forward->resolve(standardContext); if(resolved) { foundDeclaration = resolved; //This probably won't work foundBetter = true; } } } //This will do a search without visibility-restriction, and that search will prefer non forward declarations if(!foundBetter) { Declaration* betterDecl = foundDeclaration->id().getDeclaration(0); if(betterDecl) { foundDeclaration = betterDecl; foundBetter = true; } } } if( foundDeclaration ) { jumpTo.first = foundDeclaration->url().toUrl(); jumpTo.second = foundDeclaration->rangeInCurrentRevision().start(); } } if(jumpTo.first.isValid() && jumpTo.second.isValid()) { if(mouseEvent->button() == Qt::LeftButton) { if(mouseEvent->type() == QEvent::MouseButtonPress) { m_buttonPressPosition = textCursor; // view->setCursorPosition(textCursor); // return false; }else if(mouseEvent->type() == QEvent::MouseButtonRelease && textCursor == m_buttonPressPosition) { ICore::self()->documentController()->openDocument(jumpTo.first, jumpTo.second); // event->accept(); // return true; } }else if(mouseEvent->type() == QEvent::MouseMove) { //Make the cursor a "hand" setHandCursor(widget); return false; } } } resetChangedCursor(); } return false; } void BrowseManager::resetChangedCursor() { QMap, QCursor> cursors = m_oldCursors; m_oldCursors.clear(); for(QMap, QCursor>::iterator it = cursors.begin(); it != cursors.end(); ++it) if(it.key()) it.key()->setCursor(QCursor(Qt::IBeamCursor)); } void BrowseManager::setHandCursor(QWidget* widget) { if(m_oldCursors.contains(widget)) return; //Nothing to do m_oldCursors[widget] = widget->cursor(); widget->setCursor(QCursor(Qt::PointingHandCursor)); } +void BrowseManager::avoidMenuAltFocus() { + // send an invalid key event to the main menu bar. The menu bar will + // stop listening when observing another key than ALT between the press + // and the release. + QKeyEvent event1(QEvent::KeyPress, 0, Qt::NoModifier); + QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event1); + QKeyEvent event2(QEvent::KeyRelease, 0, Qt::NoModifier); + QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event2); +} + void BrowseManager::applyEventFilter(QWidget* object, bool install) { if(install) object->installEventFilter(this); else object->removeEventFilter(this); foreach(QObject* child, object->children()) if(qobject_cast(child)) applyEventFilter(qobject_cast(child), install); } void BrowseManager::viewAdded(KTextEditor::View* view) { applyEventFilter(view, true); //We need to listen for cursorPositionChanged, to clear the shift-detector. The problem: Kate listens for the arrow-keys using shortcuts, //so those keys are not passed to the event-filter // can't use new signal/slot syntax here, these signals are only defined in KateView // TODO: should we really depend on kate internals here? connect(view, SIGNAL(navigateLeft()), m_plugin, SLOT(navigateLeft())); connect(view, SIGNAL(navigateRight()), m_plugin, SLOT(navigateRight())); connect(view, SIGNAL(navigateUp()), m_plugin, SLOT(navigateUp())); connect(view, SIGNAL(navigateDown()), m_plugin, SLOT(navigateDown())); connect(view, SIGNAL(navigateAccept()), m_plugin, SLOT(navigateAccept())); connect(view, SIGNAL(navigateBack()), m_plugin, SLOT(navigateBack())); } void Watcher::viewAdded(KTextEditor::View* view) { m_manager->viewAdded(view); } Watcher::Watcher(BrowseManager* manager) : EditorViewWatcher(manager), m_manager(manager) { foreach(KTextEditor::View* view, allViews()) m_manager->applyEventFilter(view, true); } diff --git a/plugins/contextbrowser/browsemanager.h b/plugins/contextbrowser/browsemanager.h index 6e04ca5f80..f631189080 100644 --- a/plugins/contextbrowser/browsemanager.h +++ b/plugins/contextbrowser/browsemanager.h @@ -1,109 +1,109 @@ /* * This file is part of KDevelop * * Copyright 2008 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PLUGIN_BROWSEMANAGER_H #define KDEVPLATFORM_PLUGIN_BROWSEMANAGER_H #include #include #include #include #include #include class QWidget; namespace KTextEditor { class View; class Document; } namespace KDevelop { class IDocument; } class EditorViewWatcher : public QObject { Q_OBJECT public: ///@param sameWindow If this is true, only views that are child of the same window as the given widget are registered explicit EditorViewWatcher(QObject* parent = 0); QList allViews(); private: ///Called for every added view. Reimplement this to catch them. virtual void viewAdded(KTextEditor::View*); private slots: void viewDestroyed(QObject* view); void viewCreated(KTextEditor::Document*, KTextEditor::View*); void documentCreated( KDevelop::IDocument* document ); private: void addViewInternal(KTextEditor::View* view); QList m_views; }; class ContextBrowserPlugin; class BrowseManager; class Watcher : public EditorViewWatcher { Q_OBJECT public: explicit Watcher(BrowseManager* manager); void viewAdded(KTextEditor::View*) override; private: BrowseManager* m_manager; }; /** * Integrates the context-browser with the editor views, by listening for navigation events, and implementing html-like source browsing */ class BrowseManager : public QObject { Q_OBJECT public: explicit BrowseManager(ContextBrowserPlugin* controller); void viewAdded(KTextEditor::View* view); //Installs/uninstalls the event-filter void applyEventFilter(QWidget* object, bool install); Q_SIGNALS: //Emitted when browsing was started using the magic-modifier void startDelayedBrowsing(KTextEditor::View* view); void stopDelayedBrowsing(); private slots: void eventuallyStartDelayedBrowsing(); private: void resetChangedCursor(); void setHandCursor(QWidget* widget); - + void avoidMenuAltFocus(); bool eventFilter(QObject * watched, QEvent * event) override ; ContextBrowserPlugin* m_plugin; int m_browsingByKey; //Whether the browsing was started because of a key Watcher m_watcher; //Maps widgets to their previously set cursors QMap, QCursor> m_oldCursors; QTimer* m_delayedBrowsingTimer; QPointer m_browingStartedInView; KTextEditor::Cursor m_buttonPressPosition; }; #endif diff --git a/plugins/git/CMakeLists.txt b/plugins/git/CMakeLists.txt index 357474c703..8b395ef8dd 100644 --- a/plugins/git/CMakeLists.txt +++ b/plugins/git/CMakeLists.txt @@ -1,24 +1,26 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevgit\") add_subdirectory(tests) add_subdirectory(icons) set(kdevgit_PART_SRCS stashmanagerdialog.cpp stashpatchsource.cpp gitmessagehighlighter.cpp gitclonejob.cpp gitplugin.cpp gitpluginmetadata.cpp gitjob.cpp gitplugincheckinrepositoryjob.cpp + gitnameemaildialog.cpp ) ki18n_wrap_ui(kdevgit_PART_SRCS stashmanagerdialog.ui) +ki18n_wrap_ui(kdevgit_PART_SRCS gitnameemaildialog.ui) kdevplatform_add_plugin(kdevgit JSON kdevgit.json SOURCES ${kdevgit_PART_SRCS}) target_link_libraries(kdevgit KDev::Util KDev::Interfaces KDev::Vcs KDev::Project KF5::SonnetUi ) diff --git a/plugins/git/gitnameemaildialog.cpp b/plugins/git/gitnameemaildialog.cpp new file mode 100644 index 0000000000..8b10706ed5 --- /dev/null +++ b/plugins/git/gitnameemaildialog.cpp @@ -0,0 +1,84 @@ + /************************************************************************** + * Copyright 2016 Artur Puzio * + * Copyright 2016 Kevin Funk * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library General Public License as * + * published by the Free Software Foundation; either version 2 of the * + * License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "gitnameemaildialog.h" +#include "ui_gitnameemaildialog.h" + +#include "gitplugin.h" + +#include + +#include +#include + +using namespace KDevelop; + +GitNameEmailDialog::GitNameEmailDialog(QWidget *parent) + : QDialog(parent), + ui(new Ui::GitNameEmailDialog) +{ + ui->setupUi(this); + + ui->buttonBox->button(QDialogButtonBox::Save)->setDisabled(true); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &GitNameEmailDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &GitNameEmailDialog::reject); + + QRegularExpression rx(".+"); + auto validator = new QRegularExpressionValidator(rx, this); + ui->emailEdit->setValidator(validator); + ui->nameEdit->setValidator(validator); + + connect(ui->emailEdit, &QLineEdit::textChanged, this, &GitNameEmailDialog::updateUi); + connect(ui->nameEdit, &QLineEdit::textChanged, this, &GitNameEmailDialog::updateUi); +} + +GitNameEmailDialog::~GitNameEmailDialog() = default; + +void GitNameEmailDialog::updateUi() +{ + ui->buttonBox->button(QDialogButtonBox::Save)->setDisabled( + !ui->nameEdit->hasAcceptableInput() || !ui->emailEdit->hasAcceptableInput()); +} + +void GitNameEmailDialog::setName(const QString& name) +{ + ui->nameEdit->setText(name); +} + +void GitNameEmailDialog::setEmail(const QString& email) +{ + ui->emailEdit->setText(email); +} + +QString GitNameEmailDialog::name() const +{ + return ui->nameEdit->text(); +} + +QString GitNameEmailDialog::email() const +{ + return ui->emailEdit->text(); +} + +bool GitNameEmailDialog::isGlobal() const +{ + return ui->globalCheckBox->isChecked(); +} diff --git a/plugins/git/gitnameemaildialog.h b/plugins/git/gitnameemaildialog.h new file mode 100644 index 0000000000..1e9e332c60 --- /dev/null +++ b/plugins/git/gitnameemaildialog.h @@ -0,0 +1,50 @@ + /************************************************************************** + * Copyright 2016 Artur Puzio * + * Copyright 2016 Kevin Funk * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library General Public License as * + * published by the Free Software Foundation; either version 2 of the * + * License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef KDEVPLATFORM_PLUGIN_GIT_NAMEEMAILDIALOG_H +#define KDEVPLATFORM_PLUGIN_GIT_NAMEEMAILDIALOG_H + +#include + +namespace Ui { class GitNameEmailDialog; } + +class GitNameEmailDialog : public QDialog +{ + Q_OBJECT + +public: + explicit GitNameEmailDialog(QWidget *parent = nullptr); + ~GitNameEmailDialog() override; + + QString name() const; + void setName(const QString& name); + QString email() const; + void setEmail(const QString& email); + + bool isGlobal() const; + +private slots: + void updateUi(); + +private: + QScopedPointer ui; +}; + +#endif //KDEVPLATFORM_PLUGIN_GIT_NAMEEMAILDIALOG_H diff --git a/plugins/git/gitnameemaildialog.ui b/plugins/git/gitnameemaildialog.ui new file mode 100644 index 0000000000..85c3417139 --- /dev/null +++ b/plugins/git/gitnameemaildialog.ui @@ -0,0 +1,108 @@ + + + GitNameEmailDialog + + + Qt::WindowModal + + + + 0 + 0 + 446 + 282 + + + + Configure Name and Email for Git + + + + + + <html><head/><body><p>You have not yet configured the name and email to be associated with your Git commits.<br/>The values you enter here will be written to the Git configuration, either locally for the current project only, or globally for all Git projects.</p></body></html> + + + true + + + + + + + + + &Name: + + + nameEdit + + + + + + + + + + Emai&l: + + + emailEdit + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Write to global config + + + + + + + Qt::Horizontal + + + + 40 + 1 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + + + diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp index 7a58b510b2..9d26f9a66b 100644 --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -1,1465 +1,1506 @@ /*************************************************************************** * 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 "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")); - QDir dir = dotGitDirectory(localLocations.front()); + 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'); bool skipNext=false; QMap definedRevisions; for(QStringList::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); if(name==QLatin1String("author")) annotation->setAuthor(value); 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); 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(' '); VcsRevision rev; rev.setRevisionValue(name.left(8), KDevelop::VcsRevision::GlobalNumber); skipNext = definedRevisions.contains(name); if(!skipNext) definedRevisions.insert(name, VcsAnnotationLine()); annotation = &definedRevisions[name]; 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); QStringList branchList; foreach(QString 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); 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}" ); QStringList lines = job->output().split('\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); QDir workingDir = job->directory(); QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); QVariantList statuses; QList processedFiles; foreach(const QString& 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); int arrow = curr.indexOf(QStringLiteral(" -> ")); if(arrow>=0) { VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.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.setState(messageToState(state)); processedFiles.append(status.url()); qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << messageToState(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('.'); 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(); 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) { 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()) { 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; 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" << "-c" << "color.diff=false" << "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) +DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) { auto job = new DVcsJob(urlDir(repository), this); - *job << "git" << "config" << key << value; + 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 eaf9d19325..1530ba82fd 100644 --- a/plugins/git/gitplugin.h +++ b/plugins/git/gitplugin.h @@ -1,225 +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); + 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); 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/git/tests/CMakeLists.txt b/plugins/git/tests/CMakeLists.txt index 895fdcae1e..9fe8022c96 100644 --- a/plugins/git/tests/CMakeLists.txt +++ b/plugins/git/tests/CMakeLists.txt @@ -1,22 +1,24 @@ # Running the test only makes sense if the git command line client # is present. So check for it before adding the test... find_program(GIT_FOUND NAMES git) if (GIT_FOUND) set(gittest_SRCS test_git.cpp ../gitplugin.cpp ../gitclonejob.cpp ../stashmanagerdialog.cpp ../stashpatchsource.cpp ../gitjob.cpp ../gitmessagehighlighter.cpp ../gitplugincheckinrepositoryjob.cpp + ../gitnameemaildialog.cpp ) ki18n_wrap_ui(gittest_SRCS ../stashmanagerdialog.ui) + ki18n_wrap_ui(gittest_SRCS ../gitnameemaildialog.ui) ecm_add_test(${gittest_SRCS} TEST_NAME test_kdevgit LINK_LIBRARIES Qt5::Test KDev::Vcs KDev::Util KDev::Tests GUI) endif () diff --git a/plugins/git/tests/test_git.cpp b/plugins/git/tests/test_git.cpp index 25bd8a96fd..3cfb4b274c 100644 --- a/plugins/git/tests/test_git.cpp +++ b/plugins/git/tests/test_git.cpp @@ -1,452 +1,491 @@ /*************************************************************************** * This file was partly taken from KDevelop's cvs plugin * * Copyright 2007 Robert Gruber * * * * Adapted for Git * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "test_git.h" #include #include #include #include #include #include #include #include "../gitplugin.h" #define VERIFYJOB(j) \ do { QVERIFY(j); QVERIFY(j->exec()); QVERIFY((j)->status() == KDevelop::VcsJob::JobSucceeded); } while(0) inline QString tempDir() { return QDir::tempPath(); } inline QString gitTest_BaseDir() { return tempDir() + "/kdevGit_testdir/"; } inline QString gitTest_BaseDir2() { return tempDir() + "/kdevGit_testdir2/"; } inline QString gitRepo() { return gitTest_BaseDir() + ".git"; } inline QString gitSrcDir() { return gitTest_BaseDir() + "src/"; } inline QString gitTest_FileName() { return QStringLiteral("testfile"); } inline QString gitTest_FileName2() { return QStringLiteral("foo"); } inline QString gitTest_FileName3() { return QStringLiteral("bar"); } using namespace KDevelop; bool writeFile(const QString &path, const QString& content, QIODevice::OpenModeFlag mode = QIODevice::WriteOnly) { QFile f(path); if (!f.open(mode)) { return false; } QTextStream input(&f); input << content; return true; } void GitInitTest::initTestCase() { AutoTestShell::init({QStringLiteral("kdevgit")}); TestCore::initialize(); m_plugin = new GitPlugin(TestCore::self()); } void GitInitTest::cleanupTestCase() { delete m_plugin; TestCore::shutdown(); } void GitInitTest::init() { // Now create the basic directory structure QDir tmpdir(tempDir()); tmpdir.mkdir(gitTest_BaseDir()); tmpdir.mkdir(gitSrcDir()); tmpdir.mkdir(gitTest_BaseDir2()); } void GitInitTest::cleanup() { removeTempDirs(); } void GitInitTest::repoInit() { qDebug() << "Trying to init repo"; // make job that creates the local repository VcsJob* j = m_plugin->init(QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //check if the CVSROOT directory in the new local repository exists now QVERIFY(QFileInfo::exists(gitRepo())); //check if isValidDirectory works QVERIFY(m_plugin->isValidDirectory(QUrl::fromLocalFile(gitTest_BaseDir()))); //and for non-git dir, I hope nobody has /tmp under git QVERIFY(!m_plugin->isValidDirectory(QUrl::fromLocalFile(QStringLiteral("/tmp")))); //we have nothing, so output should be empty DVcsJob * j2 = m_plugin->gitRevParse(gitRepo(), QStringList(QStringLiteral("--branches"))); QVERIFY(j2); QVERIFY(j2->exec()); QString out = j2->output(); QVERIFY(j2->output().isEmpty()); // Make sure to set the Git identity so unit tests don't depend on that auto j3 = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.email"), QStringLiteral("me@example.com")); VERIFYJOB(j3); auto j4 = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.name"), QStringLiteral("My Name")); VERIFYJOB(j4); } void GitInitTest::addFiles() { qDebug() << "Adding files to the repo"; //we start it after repoInit, so we still have empty git repo QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("HELLO WORLD"))); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName2(), QStringLiteral("No, bar()!"))); //test git-status exitCode (see DVcsJob::setExitCode). VcsJob* j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); // /tmp/kdevGit_testdir/ and testfile j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); VERIFYJOB(j); QVERIFY(writeFile(gitSrcDir() + gitTest_FileName3(), QStringLiteral("No, foo()! It's bar()!"))); //test git-status exitCode again j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //repository path without trailing slash and a file in a parent directory // /tmp/repo and /tmp/repo/src/bar j = m_plugin->add(QList() << QUrl::fromLocalFile(gitSrcDir() + gitTest_FileName3())); VERIFYJOB(j); //let's use absolute path, because it's used in ContextMenus j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName2())); VERIFYJOB(j); //Now let's create several files and try "git add file1 file2 file3" QStringList files = QStringList() << QStringLiteral("file1") << QStringLiteral("file2") << QStringLiteral("la la"); QList multipleFiles; foreach(const QString& file, files) { QVERIFY(writeFile(gitTest_BaseDir() + file, file)); multipleFiles << QUrl::fromLocalFile(gitTest_BaseDir() + file); } j = m_plugin->add(multipleFiles); VERIFYJOB(j); } void GitInitTest::commitFiles() { qDebug() << "Committing..."; //we start it after addFiles, so we just have to commit VcsJob* j = m_plugin->commit(QStringLiteral("Test commit"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //test git-status exitCode one more time. j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //since we committed the file to the "pure" repository, .git/refs/heads/master should exist //TODO: maybe other method should be used QString headRefName(gitRepo() + "/refs/heads/master"); QVERIFY(QFileInfo::exists(headRefName)); //Test the results of the "git add" DVcsJob* jobLs = new DVcsJob(gitTest_BaseDir(), m_plugin); *jobLs << "git" << "ls-tree" << "--name-only" << "-r" << "HEAD"; if (jobLs->exec() && jobLs->status() == KDevelop::VcsJob::JobSucceeded) { QStringList files = jobLs->output().split('\n'); QVERIFY(files.contains(gitTest_FileName())); QVERIFY(files.contains(gitTest_FileName2())); QVERIFY(files.contains("src/" + gitTest_FileName3())); } QString firstCommit; QFile headRef(headRefName); if (headRef.open(QIODevice::ReadOnly)) { QTextStream output(&headRef); output >> firstCommit; } headRef.close(); QVERIFY(!firstCommit.isEmpty()); qDebug() << "Committing one more time"; //let's try to change the file and test "git commit -a" QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("Just another HELLO WORLD\n"))); //add changes j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("KDevelop's Test commit2"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); QString secondCommit; if (headRef.open(QIODevice::ReadOnly)) { QTextStream output(&headRef); output >> secondCommit; } headRef.close(); QVERIFY(!secondCommit.isEmpty()); QVERIFY(firstCommit != secondCommit); } void GitInitTest::testInit() { repoInit(); } +static QString runCommand(const QString& cmd, const QStringList& args) +{ + QProcess proc; + proc.setWorkingDirectory(gitTest_BaseDir()); + proc.start(cmd, args); + proc.waitForFinished(); + return proc.readAllStandardOutput().trimmed(); +} + +void GitInitTest::testReadAndSetConfigOption() +{ + repoInit(); + + { + qDebug() << "read non-existing config option"; + QString nameFromPlugin = m_plugin->readConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), + QStringLiteral("notexisting.asdads")); + QVERIFY(nameFromPlugin.isEmpty()); + } + + { + qDebug() << "write user.name = \"John Tester\""; + auto job = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), + QStringLiteral("user.name"), QStringLiteral("John Tester")); + VERIFYJOB(job); + const auto name = runCommand("git", {"config", "--get", QStringLiteral("user.name")}); + QCOMPARE(name, QStringLiteral("John Tester")); + } + + { + qDebug() << "read user.name"; + const QString nameFromPlugin = m_plugin->readConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), + QStringLiteral("user.name")); + QCOMPARE(nameFromPlugin, QStringLiteral("John Tester")); + const auto name = runCommand("git", {"config", "--get", QStringLiteral("user.name")}); + QCOMPARE(name, QStringLiteral("John Tester")); + } +} + void GitInitTest::testAdd() { repoInit(); addFiles(); } void GitInitTest::testCommit() { repoInit(); addFiles(); commitFiles(); } void GitInitTest::testBranch(const QString& newBranch) { //Already tested, so I assume that it works const QUrl baseUrl = QUrl::fromLocalFile(gitTest_BaseDir()); QString oldBranch = runSynchronously(m_plugin->currentBranch(baseUrl)).toString(); VcsRevision rev; rev.setRevisionValue(oldBranch, KDevelop::VcsRevision::GlobalNumber); VcsJob* j = m_plugin->branch(baseUrl, rev, newBranch); VERIFYJOB(j); QVERIFY(runSynchronously(m_plugin->branches(baseUrl)).toStringList().contains(newBranch)); // switch branch j = m_plugin->switchBranch(baseUrl, newBranch); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), newBranch); // get into detached head state j = m_plugin->switchBranch(baseUrl, QStringLiteral("HEAD~1")); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), QString()); // switch back j = m_plugin->switchBranch(baseUrl, newBranch); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), newBranch); j = m_plugin->deleteBranch(baseUrl, oldBranch); VERIFYJOB(j); QVERIFY(!runSynchronously(m_plugin->branches(baseUrl)).toStringList().contains(oldBranch)); } void GitInitTest::testBranching() { repoInit(); addFiles(); commitFiles(); const QUrl baseUrl = QUrl::fromLocalFile(gitTest_BaseDir()); VcsJob* j = m_plugin->branches(baseUrl); VERIFYJOB(j); QString curBranch = runSynchronously(m_plugin->currentBranch(baseUrl)).toString(); QCOMPARE(curBranch, QStringLiteral("master")); testBranch(QStringLiteral("new")); testBranch(QStringLiteral("averylongbranchnamejusttotestlongnames")); testBranch(QStringLiteral("KDE/4.10")); } void GitInitTest::revHistory() { repoInit(); addFiles(); commitFiles(); QList commits = m_plugin->getAllCommits(gitTest_BaseDir()); QVERIFY(!commits.isEmpty()); QStringList logMessages; for (int i = 0; i < commits.count(); ++i) logMessages << commits[i].getLog(); QCOMPARE(commits.count(), 2); QCOMPARE(logMessages[0], QStringLiteral("KDevelop's Test commit2")); //0 is later than 1! QCOMPARE(logMessages[1], QStringLiteral("Test commit")); QVERIFY(commits[1].getParents().isEmpty()); //0 is later than 1! QVERIFY(!commits[0].getParents().isEmpty()); //initial commit is on the top QVERIFY(commits[1].getCommit().contains(QRegExp("^\\w{,40}$"))); QVERIFY(commits[0].getCommit().contains(QRegExp("^\\w{,40}$"))); QVERIFY(commits[0].getParents()[0].contains(QRegExp("^\\w{,40}$"))); } void GitInitTest::testAnnotation() { repoInit(); addFiles(); commitFiles(); // called after commitFiles QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->commit(QStringLiteral("KDevelop's Test commit3"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); j = m_plugin->annotate(QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName()), VcsRevision::createSpecialRevision(VcsRevision::Head)); VERIFYJOB(j); QList results = j->fetchResults().toList(); QCOMPARE(results.size(), 2); QVERIFY(results.at(0).canConvert()); VcsAnnotationLine annotation = results.at(0).value(); QCOMPARE(annotation.lineNumber(), 0); QCOMPARE(annotation.commitMessage(), QStringLiteral("KDevelop's Test commit2")); QVERIFY(results.at(1).canConvert()); annotation = results.at(1).value(); QCOMPARE(annotation.lineNumber(), 1); QCOMPARE(annotation.commitMessage(), QStringLiteral("KDevelop's Test commit3")); } void GitInitTest::testRemoveEmptyFolder() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("emptydir")); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"emptydir/")); if (j) VERIFYJOB(j); QVERIFY(!d.exists(QStringLiteral("emptydir"))); } void GitInitTest::testRemoveEmptyFolderInFolder() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("dir")); QDir d2(gitTest_BaseDir()+"dir"); d2.mkdir(QStringLiteral("emptydir")); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"dir/")); if (j) VERIFYJOB(j); QVERIFY(!d.exists(QStringLiteral("dir"))); } void GitInitTest::testRemoveUnindexedFile() { repoInit(); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); if (j) VERIFYJOB(j); QVERIFY(!QFile::exists(gitTest_BaseDir() + gitTest_FileName())); } void GitInitTest::testRemoveFolderContainingUnversionedFiles() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("dir")); QVERIFY(writeFile(gitTest_BaseDir() + "dir/foo", QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"dir")); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("initial commit"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); QVERIFY(writeFile(gitTest_BaseDir() + "dir/bar", QStringLiteral("An appended line"), QIODevice::Append)); j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + "dir")); if (j) VERIFYJOB(j); QVERIFY(!QFile::exists(gitTest_BaseDir() + "dir")); } void GitInitTest::removeTempDirs() { for (const auto& dirPath : {gitTest_BaseDir(), gitTest_BaseDir2()}) { QDir dir(dirPath); if (dir.exists() && !dir.removeRecursively()) { qDebug() << "QDir::removeRecursively(" << dirPath << ") returned false"; } } } void GitInitTest::testDiff() { repoInit(); addFiles(); commitFiles(); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("something else"))); VcsRevision srcrev = VcsRevision::createSpecialRevision(VcsRevision::Base); VcsRevision dstrev = VcsRevision::createSpecialRevision(VcsRevision::Working); VcsJob* j = m_plugin->diff(QUrl::fromLocalFile(gitTest_BaseDir()), srcrev, dstrev, VcsDiff::DiffUnified, IBasicVersionControl::Recursive); VERIFYJOB(j); KDevelop::VcsDiff d = j->fetchResults().value(); QVERIFY(d.baseDiff().isLocalFile()); QString path = d.baseDiff().toLocalFile(); QVERIFY(QDir().exists(path+"/.git")); } QTEST_MAIN(GitInitTest) // #include "gittest.moc" diff --git a/plugins/git/tests/test_git.h b/plugins/git/tests/test_git.h index d258af24f4..bf0a87764e 100644 --- a/plugins/git/tests/test_git.h +++ b/plugins/git/tests/test_git.h @@ -1,71 +1,72 @@ /*************************************************************************** * This file was partly taken from KDevelop's cvs plugin * * Copyright 2007 Robert Gruber * * * * Adapted for Git * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) 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_INIT_H #define KDEVPLATFORM_PLUGIN_GIT_INIT_H #include #include class GitPlugin; namespace KDevelop { class TestCore; } class GitInitTest: public QObject { Q_OBJECT private: void repoInit(); void addFiles(); void commitFiles(); private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testInit(); + void testReadAndSetConfigOption(); void testAdd(); void testCommit(); void testBranching(); void testBranch(const QString &branchName); void revHistory(); void testAnnotation(); void testRemoveEmptyFolder(); void testRemoveEmptyFolderInFolder(); void testRemoveUnindexedFile(); void testRemoveFolderContainingUnversionedFiles(); void testDiff(); private: GitPlugin* m_plugin; void removeTempDirs(); }; #endif diff --git a/plugins/problemreporter/CMakeLists.txt b/plugins/problemreporter/CMakeLists.txt index 10afdb7b23..3c52a993ef 100644 --- a/plugins/problemreporter/CMakeLists.txt +++ b/plugins/problemreporter/CMakeLists.txt @@ -1,18 +1,18 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevproblemreporter\") ########### next target ############### set(kdevproblemreporter_PART_SRCS problemreporterplugin.cpp problemtreeview.cpp problemhighlighter.cpp problemsview.cpp - problemnavigationcontext.cpp + #problemnavigationcontext.cpp problemreportermodel.cpp ) qt5_add_resources(kdevproblemreporter_PART_SRCS kdevproblemreporter.qrc) kdevplatform_add_plugin(kdevproblemreporter JSON kdevproblemreporter.json SOURCES ${kdevproblemreporter_PART_SRCS}) target_link_libraries(kdevproblemreporter KF5::TextEditor KF5::Parts KDev::Language KDev::Interfaces KDev::Util KDev::Project KDev::Shell) add_subdirectory(tests) diff --git a/plugins/problemreporter/problemhighlighter.cpp b/plugins/problemreporter/problemhighlighter.cpp index 02848111e3..c91a967850 100644 --- a/plugins/problemreporter/problemhighlighter.cpp +++ b/plugins/problemreporter/problemhighlighter.cpp @@ -1,260 +1,260 @@ /* * KDevelop Problem Reporter * * Copyright 2008 Hamish Rodda * Copyright 2008-2009 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 "problemhighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include -#include "problemnavigationcontext.h" using namespace KTextEditor; using namespace KDevelop; namespace { QColor colorForSeverity(IProblem::Severity severity) { KColorScheme scheme(QPalette::Active); switch (severity) { case IProblem::Error: return scheme.foreground(KColorScheme::NegativeText).color(); case IProblem::Warning: return scheme.foreground(KColorScheme::NeutralText).color(); case IProblem::Hint: default: return scheme.foreground(KColorScheme::PositiveText).color(); } } } ProblemHighlighter::ProblemHighlighter(KTextEditor::Document* document) : m_document(document) , m_textHintProvider(this) { Q_ASSERT(m_document); foreach (KTextEditor::View* view, m_document->views()) viewCreated(document, view); connect(m_document.data(), &Document::viewCreated, this, &ProblemHighlighter::viewCreated); connect(ICore::self()->languageController()->completionSettings(), &ICompletionSettings::settingsChanged, this, &ProblemHighlighter::settingsChanged); connect(m_document.data(), &Document::aboutToReload, this, &ProblemHighlighter::clearProblems); if (qobject_cast(m_document)) { // can't use new signal/slot syntax here, MovingInterface is not a QObject connect(m_document, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearProblems())); } connect(m_document, SIGNAL(aboutToRemoveText(KTextEditor::Range)), this, SLOT(aboutToRemoveText(KTextEditor::Range))); } void ProblemHighlighter::settingsChanged() { // Re-highlight setProblems(m_problems); } void ProblemHighlighter::viewCreated(Document*, View* view) { KTextEditor::TextHintInterface* iface = dynamic_cast(view); if (!iface) return; iface->registerTextHintProvider(&m_textHintProvider); } ProblemTextHintProvider::ProblemTextHintProvider(ProblemHighlighter* highlighter) : m_highlighter(highlighter) { } QString ProblemTextHintProvider::textHint(View* view, const Cursor& pos) { KTextEditor::MovingInterface* moving = dynamic_cast(view->document()); if (moving) { ///@todo Sort the ranges when writing them, and do binary search instead of linear foreach (MovingRange* range, m_highlighter->m_topHLRanges) { if (m_highlighter->m_problemsForRanges.contains(range) && range->contains(pos)) { // There is a problem which's range contains the cursor IProblem::Ptr problem = m_highlighter->m_problemsForRanges[range]; if (problem->source() == IProblem::ToDo) { continue; } if (m_currentHintRange == range->toRange()) { continue; } m_currentHintRange = range->toRange(); KDevelop::AbstractNavigationWidget* widget = new KDevelop::AbstractNavigationWidget; widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problem))); KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, QCursor::pos() + QPoint(20, 40), widget); tooltip->resize(widget->sizeHint() + QSize(10, 10)); tooltip->setHandleRect(getItemBoundingRect(view, m_currentHintRange)); tooltip->connect(tooltip, &ActiveToolTip::destroyed, [&] () { m_currentHintRange = {}; }); ActiveToolTip::showToolTip(tooltip, 99, QStringLiteral("problem-tooltip")); return QString(); } } } return QString(); } ProblemHighlighter::~ProblemHighlighter() { if (m_topHLRanges.isEmpty() || !m_document) return; qDeleteAll(m_topHLRanges); } void ProblemHighlighter::setProblems(const QVector& problems) { if (!m_document) return; const bool hadProblems = !m_problems.isEmpty(); m_problems = problems; qDeleteAll(m_topHLRanges); m_topHLRanges.clear(); m_problemsForRanges.clear(); IndexedString url(m_document->url()); /// TODO: create a better MarkInterface that makes it possible to add the marks to the scrollbar /// but having no background. /// also make it nicer together with other plugins, this would currently fail with /// this method... const uint errorMarkType = KTextEditor::MarkInterface::Error; const uint warningMarkType = KTextEditor::MarkInterface::Warning; KTextEditor::MarkInterface* markIface = dynamic_cast(m_document.data()); if (markIface && hadProblems) { // clear previously added marks foreach (KTextEditor::Mark* mark, markIface->marks().values()) { if (mark->type == errorMarkType || mark->type == warningMarkType) { markIface->removeMark(mark->line, mark->type); } } } if (problems.isEmpty()) { return; } DUChainReadLocker lock; TopDUContext* top = DUChainUtils::standardContextForUrl(m_document->url()); KTextEditor::MovingInterface* iface = dynamic_cast(m_document.data()); Q_ASSERT(iface); foreach (const IProblem::Ptr& problem, problems) { if (problem->finalLocation().document != url || !problem->finalLocation().isValid()) continue; KTextEditor::Range range; if (top) range = top->transformFromLocalRevision(RangeInRevision::castFromSimpleRange(problem->finalLocation())); else range = problem->finalLocation(); if (range.end().line() >= m_document->lines()) range.end() = KTextEditor::Cursor(m_document->endOfLine(m_document->lines() - 1)); if (range.isEmpty()) { range.setEnd(range.end() + KTextEditor::Cursor(0, 1)); } KTextEditor::MovingRange* problemRange = iface->newMovingRange(range); m_problemsForRanges.insert(problemRange, problem); m_topHLRanges.append(problemRange); if (problem->source() != IProblem::ToDo && (problem->severity() != IProblem::Hint || ICore::self()->languageController()->completionSettings()->highlightSemanticProblems())) { KTextEditor::Attribute::Ptr attribute(new KTextEditor::Attribute()); attribute->setUnderlineStyle(QTextCharFormat::WaveUnderline); attribute->setUnderlineColor(colorForSeverity(problem->severity())); problemRange->setAttribute(attribute); } if (markIface && ICore::self()->languageController()->completionSettings()->highlightProblematicLines()) { uint mark; if (problem->severity() == IProblem::Error) { mark = errorMarkType; } else if (problem->severity() == IProblem::Warning) { mark = warningMarkType; } else { continue; } markIface->addMark(problem->finalLocation().start().line(), mark); } } } void ProblemHighlighter::aboutToRemoveText(const KTextEditor::Range& range) { if (range.onSingleLine()) { // no need to optimize this return; } QList::iterator it = m_topHLRanges.begin(); while (it != m_topHLRanges.end()) { if (range.contains((*it)->toRange())) { m_problemsForRanges.remove(*it); delete (*it); it = m_topHLRanges.erase(it); } else { ++it; } } } void ProblemHighlighter::clearProblems() { setProblems({}); } diff --git a/plugins/problemreporter/problemnavigationcontext.cpp b/plugins/problemreporter/problemnavigationcontext.cpp deleted file mode 100644 index 431c862a25..0000000000 --- a/plugins/problemreporter/problemnavigationcontext.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - Copyright 2009 David Nolden - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License version 2 as published by the Free Software Foundation. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "problemnavigationcontext.h" - -#include -#include - -#include - -#include -#include -#include -#include -#include - -using namespace KDevelop; - -ProblemNavigationContext::ProblemNavigationContext(const IProblem::Ptr& problem) - : m_problem(problem) -{ - m_widget = 0; - - QExplicitlySharedDataPointer solution = problem->solutionAssistant(); - if (solution && !solution->actions().isEmpty()) { - m_widget = new QWidget; - QHBoxLayout* layout = new QHBoxLayout(m_widget); - RichTextPushButton* button = new RichTextPushButton; - // button->setPopupMode(QToolButton::InstantPopup); - if (!solution->title().isEmpty()) - button->setHtml(i18n("Solve: %1", solution->title())); - else - button->setHtml(i18n("Solve")); - - QMenu* menu = new QMenu; - menu->setFocusPolicy(Qt::NoFocus); - foreach (IAssistantAction::Ptr action, solution->actions()) { - menu->addAction(action->toKAction()); - } - button->setMenu(menu); - - layout->addWidget(button); - layout->setAlignment(button, Qt::AlignLeft); - m_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); - } -} - -ProblemNavigationContext::~ProblemNavigationContext() -{ - delete m_widget; -} - -QWidget* ProblemNavigationContext::widget() const -{ - return m_widget; -} - -bool ProblemNavigationContext::isWidgetMaximized() const -{ - return false; -} - -QString ProblemNavigationContext::name() const -{ - return i18n("Problem"); -} - -QString ProblemNavigationContext::html(bool shorten) -{ - clear(); - m_shorten = shorten; - modifyHtml() += QStringLiteral("

"); - - modifyHtml() += i18n("Problem in %1:
", m_problem->sourceString()); - modifyHtml() += m_problem->description().toHtmlEscaped(); - modifyHtml() += QStringLiteral("
"); - modifyHtml() += "" + m_problem->explanation().toHtmlEscaped() + ""; - - const QVector& diagnostics = m_problem->diagnostics(); - if (!diagnostics.isEmpty()) { - modifyHtml() += QStringLiteral("
"); - - DUChainReadLocker lock; - for (auto diagnostic : diagnostics) { - const DocumentRange range = diagnostic->finalLocation(); - Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()); - - modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString())); - modifyHtml() += diagnostic->description(); - - if (declaration) { - modifyHtml() += QStringLiteral("
"); - makeLink(declaration->toString(), KDevelop::DeclarationPointer(declaration), - NavigationAction::NavigateDeclaration); - modifyHtml() += i18n(" in "); - makeLink(QStringLiteral("%1 :%2") - .arg(declaration->url().toUrl().fileName()) - .arg(declaration->rangeInCurrentRevision().start().line() + 1), - KDevelop::DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); - } - modifyHtml() += QStringLiteral("
"); - } - } - - modifyHtml() += QStringLiteral("

"); - return currentHtml(); -} diff --git a/plugins/problemreporter/problemnavigationcontext.h b/plugins/problemreporter/problemnavigationcontext.h deleted file mode 100644 index 22add5dc6e..0000000000 --- a/plugins/problemreporter/problemnavigationcontext.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright 2009 David Nolden - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License version 2 as published by the Free Software Foundation. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#ifndef KDEVPLATFORM_IPROBLEMNAVIGATIONCONTEXT_H -#define KDEVPLATFORM_IPROBLEMNAVIGATIONCONTEXT_H - -#include -#include - -#include - -namespace KDevelop -{ - -// Navigation context for IProblem -class ProblemNavigationContext : public AbstractNavigationContext -{ - Q_OBJECT -public: - explicit ProblemNavigationContext(const IProblem::Ptr& problem); - ~ProblemNavigationContext(); - virtual QString name() const override; - virtual QString html(bool shorten = false) override; - virtual QWidget* widget() const override; - virtual bool isWidgetMaximized() const override; - -private: - QPointer m_widget; - IProblem::Ptr m_problem; -}; -} - -#endif // KDEVPLATFORM_PROBLEMNAVIGATIONCONTEXT_H diff --git a/plugins/quickopen/quickopenplugin.cpp b/plugins/quickopen/quickopenplugin.cpp index 9c99006b3e..83d8288116 100644 --- a/plugins/quickopen/quickopenplugin.cpp +++ b/plugins/quickopen/quickopenplugin.cpp @@ -1,1551 +1,1567 @@ /* * 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 #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 "expandingtree/expandingdelegate.h" #include "ui_quickopen.h" #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 QuickOpenDelegate : public ExpandingDelegate { Q_OBJECT public: QuickOpenDelegate(ExpandingWidgetModel* model, QObject* parent = 0L) : ExpandingDelegate(model, parent) { } QList createHighlighting(const QModelIndex& index, QStyleOptionViewItem& option) const override { QList highlighting = index.data(KTextEditor::CodeCompletionModel::CustomHighlight).toList(); if(!highlighting.isEmpty()) return highlightingFromVariantList(highlighting); return ExpandingDelegate::createHighlighting( index, option ); } }; 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())); } void QuickOpenWidget::showStandardButtons(bool show) { if(show) { o.okButton->show(); o.cancelButton->show(); }else{ o.okButton->hide(); o.cancelButton->hide(); } } QuickOpenWidget::QuickOpenWidget( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField ) : m_model(model), m_expandedTemporary(false) { m_filterTimer.setSingleShot(true); connect(&m_filterTimer, &QTimer::timeout, this, &QuickOpenWidget::applyFilter); Q_UNUSED( title ); o.setupUi( this ); o.list->header()->hide(); o.list->setRootIsDecorated( false ); o.list->setVerticalScrollMode( QAbstractItemView::ScrollPerItem ); connect(o.list->verticalScrollBar(), &QScrollBar::valueChanged, m_model, &QuickOpenModel::placeExpandingWidgets); o.searchLine->setFocus(); o.list->setItemDelegate( new QuickOpenDelegate( m_model, o.list ) ); if(!listOnly) { QStringList allTypes = m_model->allTypes(); QStringList allScopes = m_model->allScopes(); QMenu* itemsMenu = new QMenu; foreach( const QString &type, allTypes ) { QAction* action = new QAction(type, itemsMenu); action->setCheckable(true); action->setChecked(initialItems.isEmpty() || initialItems.contains( type )); connect( action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection ); itemsMenu->addAction(action); } o.itemsButton->setMenu(itemsMenu); QMenu* scopesMenu = new QMenu; foreach( const QString &scope, allScopes ) { QAction* action = new QAction(scope, scopesMenu); action->setCheckable(true); action->setChecked(initialScopes.isEmpty() || initialScopes.contains( scope ) ); connect( action, &QAction::toggled, this, &QuickOpenWidget::updateProviders, Qt::QueuedConnection ); scopesMenu->addAction(action); } o.scopesButton->setMenu(scopesMenu); }else{ o.list->setFocusPolicy(Qt::StrongFocus); o.scopesButton->hide(); o.itemsButton->hide(); o.label->hide(); o.label_2->hide(); } showSearchField(!noSearchField); o.okButton->hide(); o.cancelButton->hide(); o.searchLine->installEventFilter( this ); o.list->installEventFilter( this ); o.list->setFocusPolicy(Qt::NoFocus); o.scopesButton->setFocusPolicy(Qt::NoFocus); o.itemsButton->setFocusPolicy(Qt::NoFocus); connect( o.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged ); connect( o.list, &ExpandingTree::doubleClicked, this, &QuickOpenWidget::doubleClicked ); connect(o.okButton, &QPushButton::clicked, this, &QuickOpenWidget::accept); connect(o.okButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); connect(o.cancelButton, &QPushButton::clicked, this, &QuickOpenWidget::ready); updateProviders(); updateTimerInterval(true); // no need to call this, it's done by updateProviders already // m_model->restart(); } void QuickOpenWidget::updateTimerInterval(bool cheapFilterChange) { const int MAX_ITEMS = 10000; if ( cheapFilterChange && m_model->rowCount(QModelIndex()) < MAX_ITEMS ) { // cheap change and there are currently just a few items, // so apply filter instantly m_filterTimer.setInterval(0); } else if ( m_model->unfilteredRowCount() < MAX_ITEMS ) { // not a cheap change, but there are generally // just a few items in the list: apply filter instantly m_filterTimer.setInterval(0); } else { // otherwise use a timer to prevent sluggishness while typing m_filterTimer.setInterval(300); } } void QuickOpenWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); // The column width only has an effect _after_ the widget has been shown o.list->setColumnWidth( 0, 20 ); } void QuickOpenWidget::setAlternativeSearchField(QLineEdit* alterantiveSearchField) { o.searchLine = alterantiveSearchField; o.searchLine->installEventFilter( this ); connect( o.searchLine, &QLineEdit::textChanged, this, &QuickOpenWidget::textChanged ); } void QuickOpenWidget::showSearchField(bool b) { if(b){ o.searchLine->show(); o.searchLabel->show(); }else{ o.searchLine->hide(); o.searchLabel->hide(); } } void QuickOpenWidget::prepareShow() { o.list->setModel( 0 ); o.list->setVerticalScrollMode(QAbstractItemView::ScrollPerItem); m_model->setTreeView( o.list ); o.list->setModel( m_model ); m_filterTimer.stop(); m_filter = QString(); if (!m_preselectedText.isEmpty()) { o.searchLine->setText(m_preselectedText); o.searchLine->selectAll(); } m_model->restart(false); connect( o.list->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &QuickOpenWidget::callRowSelected ); connect( o.list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QuickOpenWidget::callRowSelected ); } void QuickOpenWidgetDialog::run() { m_widget->prepareShow(); m_dialog->show(); } QuickOpenWidget::~QuickOpenWidget() { m_model->setTreeView( 0 ); } QuickOpenWidgetDialog::QuickOpenWidgetDialog(QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly, bool noSearchField) { m_widget = new QuickOpenWidget(title, model, initialItems, initialScopes, listOnly, noSearchField); // the QMenu might close on esc and we want to close the whole dialog then connect( m_widget, &QuickOpenWidget::aboutToHide, this, &QuickOpenWidgetDialog::deleteLater ); m_dialog = new QDialog( ICore::self()->uiController()->activeMainWindow() ); m_dialog->resize(QSize(800, 400)); m_dialog->setWindowTitle(title); QVBoxLayout* layout = new QVBoxLayout(m_dialog); layout->addWidget(m_widget); m_widget->showStandardButtons(true); connect(m_widget, &QuickOpenWidget::ready, m_dialog, &QDialog::close); connect( m_dialog, &QDialog::accepted, m_widget, &QuickOpenWidget::accept ); } QuickOpenWidgetDialog::~QuickOpenWidgetDialog() { delete m_dialog; } void QuickOpenWidget::setPreselectedText(const QString& text) { m_preselectedText = text; } void QuickOpenWidget::updateProviders() { if(QAction* action = qobject_cast(sender())) { QMenu* menu = qobject_cast(action->parentWidget()); if(menu) { menu->show(); menu->setActiveAction(action); } } QStringList checkedItems; if(o.itemsButton->menu()) { foreach( QObject* obj, o.itemsButton->menu()->children() ) { QAction* box = qobject_cast( obj ); if( box ) { if( box->isChecked() ) checkedItems << box->text().remove('&'); } } o.itemsButton->setText(checkedItems.join(QStringLiteral(", "))); } QStringList checkedScopes; if(o.scopesButton->menu()) { foreach( QObject* obj, o.scopesButton->menu()->children() ) { QAction* box = qobject_cast( obj ); if( box ) { if( box->isChecked() ) checkedScopes << box->text().remove('&'); } } o.scopesButton->setText(checkedScopes.join(QStringLiteral(", "))); } emit itemsChanged( checkedItems ); emit scopesChanged( checkedScopes ); m_model->enableProviders( checkedItems, checkedScopes ); } void QuickOpenWidget::textChanged( const QString& str ) { // "cheap" when something was just appended to the current filter updateTimerInterval(str.startsWith(m_filter)); m_filter = str; m_filterTimer.start(); } void QuickOpenWidget::applyFilter() { m_model->textChanged( m_filter ); QModelIndex currentIndex = m_model->index(0, 0, QModelIndex()); o.list->selectionModel()->setCurrentIndex( currentIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current ); callRowSelected(); } void QuickOpenWidget::callRowSelected() { QModelIndex currentIndex = o.list->selectionModel()->currentIndex(); if( currentIndex.isValid() ) m_model->rowSelected( currentIndex ); else qCDebug(PLUGIN_QUICKOPEN) << "current index is not valid"; } void QuickOpenWidget::accept() { QString filterText = o.searchLine->text(); m_model->execute( o.list->currentIndex(), filterText ); } void QuickOpenWidget::doubleClicked ( const QModelIndex & index ) { // crash guard: https://bugs.kde.org/show_bug.cgi?id=297178 o.list->setCurrentIndex(index); QMetaObject::invokeMethod(this, "accept", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "ready", Qt::QueuedConnection); } +void QuickOpenWidget::avoidMenuAltFocus() { + // send an invalid key event to the main menu bar. The menu bar will + // stop listening when observing another key than ALT between the press + // and the release. + QKeyEvent event1(QEvent::KeyPress, 0, Qt::NoModifier); + QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event1); + QKeyEvent event2(QEvent::KeyRelease, 0, Qt::NoModifier); + QApplication::sendEvent(ICore::self()->uiController()->activeMainWindow()->menuBar(), &event2); +} + bool QuickOpenWidget::eventFilter ( QObject * watched, QEvent * event ) { QKeyEvent *keyEvent = dynamic_cast(event); if( event->type() == QEvent::KeyRelease ) { if(keyEvent->key() == Qt::Key_Alt) { if((m_expandedTemporary && m_altDownTime.msecsTo( QTime::currentTime() ) > 300) || (!m_expandedTemporary && m_altDownTime.msecsTo( QTime::currentTime() ) < 300 && m_hadNoCommandSinceAlt)) { //Unexpand the item QModelIndex row = o.list->selectionModel()->currentIndex(); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if(m_model->isExpanded( row )) m_model->setExpanded( row, false ); } } m_expandedTemporary = false; } } if( event->type() == QEvent::KeyPress ) { m_hadNoCommandSinceAlt = false; if(keyEvent->key() == Qt::Key_Alt) { + avoidMenuAltFocus(); m_hadNoCommandSinceAlt = true; //Expand QModelIndex row = o.list->selectionModel()->currentIndex(); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); m_altDownTime = QTime::currentTime(); if(!m_model->isExpanded( row )) { m_expandedTemporary = true; m_model->setExpanded( row, true ); } } } switch( keyEvent->key() ) { case Qt::Key_Tab: if ( keyEvent->modifiers() == Qt::NoModifier ) { // Tab should work just like Down QCoreApplication::sendEvent(o.list, new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier)); QCoreApplication::sendEvent(o.list, new QKeyEvent(QEvent::KeyRelease, Qt::Key_Down, Qt::NoModifier)); return true; } break; case Qt::Key_Backtab: if ( keyEvent->modifiers() == Qt::ShiftModifier ) { // Shift + Tab should work just like Up QCoreApplication::sendEvent(o.list, new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier)); QCoreApplication::sendEvent(o.list, new QKeyEvent(QEvent::KeyRelease, Qt::Key_Up, Qt::NoModifier)); return true; } break; case Qt::Key_Down: case Qt::Key_Up: { if( keyEvent->modifiers() == Qt::AltModifier ) { QWidget* w = m_model->expandingWidget(o.list->selectionModel()->currentIndex()); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ if( keyEvent->key() == Qt::Key_Down ) interface->down(); else interface->up(); return true; } return false; } } case Qt::Key_PageUp: case Qt::Key_PageDown: if(watched == o.list ) return false; QApplication::sendEvent( o.list, event ); //callRowSelected(); return true; case Qt::Key_Left: { //Expand/unexpand if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget QWidget* w = m_model->expandingWidget(o.list->selectionModel()->currentIndex()); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->previous(); return true; } } else { QModelIndex row = o.list->selectionModel()->currentIndex(); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if( m_model->isExpanded( row ) ) { m_model->setExpanded( row, false ); return true; } } } return false; } case Qt::Key_Right: { //Expand/unexpand if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget QWidget* w = m_model->expandingWidget(o.list->selectionModel()->currentIndex()); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->next(); return true; } } else { QModelIndex row = o.list->selectionModel()->currentIndex(); if( row.isValid() ) { row = row.sibling( row.row(), 0 ); if( !m_model->isExpanded( row ) ) { m_model->setExpanded( row, true ); return true; } } } return false; } case Qt::Key_Return: case Qt::Key_Enter: { if (m_filterTimer.isActive()) { m_filterTimer.stop(); applyFilter(); } if( keyEvent->modifiers() == Qt::AltModifier ) { //Eventually Send action to the widget QWidget* w = m_model->expandingWidget(o.list->selectionModel()->currentIndex()); if( KDevelop::QuickOpenEmbeddedWidgetInterface* interface = dynamic_cast( w ) ){ interface->accept(); return true; } } else { QString filterText = o.searchLine->text(); //Safety: Track whether this object is deleted. When execute() is called, a dialog may be opened, //which kills the quickopen widget. QPointer stillExists(this); if( m_model->execute( o.list->currentIndex(), filterText ) ) { if(!stillExists) return true; if(!(keyEvent->modifiers() & Qt::ShiftModifier)) emit ready(); } else { //Maybe the filter-text was changed: if( filterText != o.searchLine->text() ) { o.searchLine->setText( filterText ); } } } return true; } } } return false; } 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()->o.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()->o.list->setCurrentIndex( model->index(num,0,QModelIndex()) ); dialog->widget()->o.list->scrollTo( model->index(num,0,QModelIndex()), 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->o.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" // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on diff --git a/plugins/quickopen/quickopenplugin.h b/plugins/quickopen/quickopenplugin.h index 12509f944b..914a8b1815 100644 --- a/plugins/quickopen/quickopenplugin.h +++ b/plugins/quickopen/quickopenplugin.h @@ -1,245 +1,247 @@ /* * 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. */ #ifndef KDEVPLATFORM_PLUGIN_QUICKOPENPLUGIN_H #define KDEVPLATFORM_PLUGIN_QUICKOPENPLUGIN_H #include #include #include #include #include #include #include #include #include "ui_quickopen.h" class QAction; namespace KTextEditor { class Cursor; } class QuickOpenModel; class QuickOpenWidget; class QuickOpenLineEdit; class QuickOpenPlugin : public KDevelop::IPlugin, public KDevelop::IQuickOpen { Q_OBJECT Q_INTERFACES( KDevelop::IQuickOpen ) public: explicit QuickOpenPlugin( QObject *parent, const QVariantList & = QVariantList() ); ~QuickOpenPlugin() override; static QuickOpenPlugin* self(); // KDevelop::Plugin methods void unload() override; KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context* context) override; enum ModelTypes { Files = 1, Functions = 2, Classes = 4, OpenFiles = 8, All = Files + Functions + Classes + OpenFiles }; /** * Shows the quickopen dialog with the specified Model-types * @param modes A combination of ModelTypes * */ void showQuickOpen( ModelTypes modes = All ); void showQuickOpen( const QStringList &items ) override; void registerProvider( const QStringList& scope, const QStringList& type, KDevelop::QuickOpenDataProviderBase* provider ) override; bool removeProvider( KDevelop::QuickOpenDataProviderBase* provider ) override; QSet fileSet() const override; //Frees the model by closing active quickopen dialoags, and retuns whether successful. bool freeModel(); void createActionsForMainWindow( Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions ) override; QuickOpenLineEdit* createQuickOpenLineWidget(); KDevelop::IQuickOpenLine* createQuickOpenLine(const QStringList& scopes, const QStringList& type, QuickOpenType kind) override; public slots: void quickOpen(); void quickOpenFile(); void quickOpenFunction(); void quickOpenClass(); void quickOpenDeclaration(); void quickOpenOpenFile(); void quickOpenDefinition(); void quickOpenNavigateFunctions(); void quickOpenDocumentation(); void quickOpenActions(); void previousFunction(); void nextFunction(); private slots: void storeScopes( const QStringList& ); void storeItems( const QStringList& ); private: friend class QuickOpenLineEdit; friend class StandardQuickOpenWidgetCreator; QuickOpenLineEdit* quickOpenLine(QString name = QStringLiteral("Quickopen")); enum FunctionJumpDirection { NextFunction, PreviousFunction }; void jumpToNearestFunction(FunctionJumpDirection direction); QPair specialObjectJumpPosition() const; QWidget* specialObjectNavigationWidget() const; bool jumpToSpecialObject(); void showQuickOpenWidget(const QStringList &items, const QStringList &scopes, bool preselectText); QuickOpenModel* m_model; class ProjectFileDataProvider* m_projectFileData; class ProjectItemDataProvider* m_projectItemData; class OpenFilesDataProvider* m_openFilesData; class DocumentationQuickOpenProvider* m_documentationItemData; class ActionsQuickOpenProvider* m_actionsItemData; QStringList lastUsedScopes; QStringList lastUsedItems; //We can only have one widget at a time, because we manipulate the model. QPointer m_currentWidgetHandler; QAction* m_quickOpenDeclaration; QAction* m_quickOpenDefinition; }; ///Will delete itself once the dialog is closed, so use QPointer when referencing it permanently class QuickOpenWidget : public QMenu { Q_OBJECT public: /** * @param initialItems List of items that should initially be enabled in the quickopen-list. If empty, all are enabled. * @param initialScopes List of scopes that should initially be enabled in the quickopen-list. If empty, all are enabled. * @param listOnly when this is true, the given items will be listed, but all filtering using checkboxes is disabled. * @param noSearchFied when this is true, no search-line is shown. * */ QuickOpenWidget( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly = false, bool noSearchField = false ); ~QuickOpenWidget() override; void setPreselectedText(const QString &text); void prepareShow(); void setAlternativeSearchField(QLineEdit* alterantiveSearchField); //Shows OK + Cancel. By default they are hidden void showStandardButtons(bool show); void showSearchField(bool show); signals: void scopesChanged( const QStringList& scopes ); void itemsChanged( const QStringList& scopes ); void ready(); private slots: void callRowSelected(); void updateTimerInterval( bool cheapFilterChange ); void accept(); void textChanged( const QString& str ); void updateProviders(); void doubleClicked ( const QModelIndex & index ); void applyFilter(); private: void showEvent(QShowEvent *) override; bool eventFilter ( QObject * watched, QEvent * event ) override; + void avoidMenuAltFocus(); + QuickOpenModel* m_model; bool m_expandedTemporary, m_hadNoCommandSinceAlt; QTime m_altDownTime; QString m_preselectedText; QTimer m_filterTimer; QString m_filter; public: Ui::QuickOpen o; friend class QuickOpenWidgetDialog; friend class QuickOpenPlugin; friend class QuickOpenLineEdit; }; class QuickOpenWidgetDialog : public QObject { Q_OBJECT public: QuickOpenWidgetDialog( QString title, QuickOpenModel* model, const QStringList& initialItems, const QStringList& initialScopes, bool listOnly = false, bool noSearchField = false ); ~QuickOpenWidgetDialog() override; ///Shows the dialog void run(); QuickOpenWidget* widget() const { return m_widget; } private: QDialog* m_dialog; //Warning: m_dialog is also the parent QuickOpenWidget* m_widget; }; class QuickOpenWidgetCreator; class QuickOpenLineEdit : public KDevelop::IQuickOpenLine { Q_OBJECT public: explicit QuickOpenLineEdit(QuickOpenWidgetCreator* creator) ; ~QuickOpenLineEdit() override ; bool insideThis(QObject* object); void showWithWidget(QuickOpenWidget* widget); void setDefaultText(const QString& text) override { m_defaultText = text; setPlaceholderText(m_defaultText); } private slots: void activate() ; void deactivate() ; void checkFocus(); void widgetDestroyed(QObject*); private: void focusInEvent(QFocusEvent* ev) override ; bool eventFilter(QObject* obj, QEvent* e) override ; void hideEvent(QHideEvent* ) override; QPointer m_widget; bool m_forceUpdate; QString m_defaultText; QuickOpenWidgetCreator* m_widgetCreator; }; #endif // KDEVPLATFORM_PLUGIN_QUICKOPENPLUGIN_H // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on diff --git a/project/filemanagerlistjob.cpp b/project/filemanagerlistjob.cpp index ae233ed6d4..20c31541e3 100644 --- a/project/filemanagerlistjob.cpp +++ b/project/filemanagerlistjob.cpp @@ -1,176 +1,176 @@ /* This file is part of KDevelop Copyright 2009 Radu Benea 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 "filemanagerlistjob.h" #include #include #include "path.h" #include "debug.h" #include #include using namespace KDevelop; FileManagerListJob::FileManagerListJob(ProjectFolderItem* item) : KIO::Job(), m_item(item), m_aborted(false) { qRegisterMetaType("KIO::UDSEntryList"); qRegisterMetaType(); qRegisterMetaType(); /* the following line is not an error in judgment, apparently starting a * listJob while the previous one hasn't self-destructed takes a lot of time, * so we give the job a chance to selfdestruct first */ connect( this, &FileManagerListJob::nextJob, this, &FileManagerListJob::startNextJob, Qt::QueuedConnection ); addSubDir(item); #ifdef TIME_IMPORT_JOB m_timer.start(); #endif } ProjectFolderItem* FileManagerListJob::item() const { return m_item; } void FileManagerListJob::addSubDir( ProjectFolderItem* item ) { Q_ASSERT(!m_listQueue.contains(item)); Q_ASSERT(!m_item || m_item == item || m_item->path().isDirectParentOf(item->path())); m_listQueue.enqueue(item); } void FileManagerListJob::removeSubDir(ProjectFolderItem* item) { m_listQueue.removeAll(item); } void FileManagerListJob::slotEntries(KIO::Job* job, const KIO::UDSEntryList& entriesIn) { Q_UNUSED(job); entryList.append(entriesIn); } void FileManagerListJob::startNextJob() { if ( m_listQueue.isEmpty() || m_aborted ) { return; } #ifdef TIME_IMPORT_JOB m_subTimer.start(); #endif m_item = m_listQueue.dequeue(); if (m_item->path().isLocalFile()) { // optimized version for local projects using QDir directly QtConcurrent::run([this] (const Path& path) { if (m_aborted) { return; } QDir dir(path.toLocalFile()); const auto entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::Hidden); if (m_aborted) { return; } KIO::UDSEntryList results; std::transform(entries.begin(), entries.end(), std::back_inserter(results), [] (const QFileInfo& info) -> KIO::UDSEntry { KIO::UDSEntry entry; entry.insert(KIO::UDSEntry::UDS_NAME, info.fileName()); if (info.isDir()) { entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, QT_STAT_DIR); } if (info.isSymLink()) { entry.insert(KIO::UDSEntry::UDS_LINK_DEST, info.symLinkTarget()); } return entry; }); QMetaObject::invokeMethod(this, "handleResults", Q_ARG(KIO::UDSEntryList, results)); }, m_item->path()); } else { KIO::ListJob* job = KIO::listDir( m_item->path().toUrl(), KIO::HideProgressInfo ); job->addMetaData(QStringLiteral("details"), QStringLiteral("0")); job->setParentJob( this ); connect( job, &KIO::ListJob::entries, this, &FileManagerListJob::slotEntries ); connect( job, &KIO::ListJob::result, this, &FileManagerListJob::slotResult ); } } void FileManagerListJob::slotResult(KJob* job) { if (m_aborted) { return; } if( job && job->error() ) { qCDebug(FILEMANAGER) << "error in list job:" << job->error() << job->errorString(); } handleResults(entryList); entryList.clear(); } -void KDevelop::FileManagerListJob::handleResults(const KIO::UDSEntryList& entriesIn) +void FileManagerListJob::handleResults(const KIO::UDSEntryList& entriesIn) { if (m_aborted) { return; } #ifdef TIME_IMPORT_JOB { auto waited = m_subTimer.elapsed(); m_subWaited += waited; qDebug() << "TIME FOR SUB JOB:" << waited << m_subWaited; } #endif emit entries(this, m_item, entriesIn); if( m_listQueue.isEmpty() ) { emitResult(); #ifdef TIME_IMPORT_JOB qDebug() << "TIME FOR LISTJOB:" << m_timer.elapsed(); #endif } else { emit nextJob(); } } void FileManagerListJob::abort() { m_aborted = true; bool killed = kill(); Q_ASSERT(killed); Q_UNUSED(killed); } void FileManagerListJob::start() { startNextJob(); } diff --git a/shell/problemmodel.cpp b/shell/problemmodel.cpp index e0556d67dd..06f34e299c 100644 --- a/shell/problemmodel.cpp +++ b/shell/problemmodel.cpp @@ -1,340 +1,349 @@ /* * KDevelop Problem Reporter * * Copyright 2007 Hamish Rodda * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "problemmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QIcon iconForSeverity(KDevelop::IProblem::Severity severity) { switch (severity) { case KDevelop::IProblem::Hint: return QIcon::fromTheme(QStringLiteral("dialog-information")); case KDevelop::IProblem::Warning: return QIcon::fromTheme(QStringLiteral("dialog-warning")); case KDevelop::IProblem::Error: return QIcon::fromTheme(QStringLiteral("dialog-error")); } return {}; } } struct ProblemModelPrivate { ProblemModelPrivate(KDevelop::ProblemStore *store) : m_problems(store) , m_features(KDevelop::ProblemModel::NoFeatures) { } QScopedPointer m_problems; KDevelop::ProblemModel::Features m_features; }; namespace KDevelop { ProblemModel::ProblemModel(QObject * parent, ProblemStore *store) : QAbstractItemModel(parent) , d(new ProblemModelPrivate(store)) { if (!d->m_problems) { d->m_problems.reset(new FilteredProblemStore()); d->m_features = ScopeFilter | SeverityFilter | Grouping | CanByPassScopeFilter; } setScope(CurrentDocument); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ProblemModel::setCurrentDocument); + connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &ProblemModel::closedDocument); /// CompletionSettings include a list of todo markers we care for, so need to update connect(ICore::self()->languageController()->completionSettings(), &ICompletionSettings::settingsChanged, this, &ProblemModel::forceFullUpdate); if (ICore::self()->documentController()->activeDocument()) { setCurrentDocument(ICore::self()->documentController()->activeDocument()); } connect(d->m_problems.data(), &ProblemStore::beginRebuild, this, &ProblemModel::onBeginRebuild); connect(d->m_problems.data(), &ProblemStore::endRebuild, this, &ProblemModel::onEndRebuild); } ProblemModel::~ ProblemModel() { } int ProblemModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) { return d->m_problems->count(); } else { return d->m_problems->count(reinterpret_cast(parent.internalPointer())); } } static QString displayUrl(const QUrl &url, const QUrl &base) { if (base.isParentOf(url)) { return url.toDisplayString(QUrl::PreferLocalFile).mid(base.toDisplayString(QUrl::PreferLocalFile).length()); } else { return ICore::self()->projectController()->prettyFileName(url, IProjectController::FormatPlain); } } QVariant ProblemModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); QUrl baseDirectory = d->m_problems->currentDocument().toUrl().adjusted(QUrl::RemoveFilename); IProblem::Ptr p = problemForIndex(index); if (!p.constData()) { if (role == Qt::DisplayRole && index.column() == Error) { ProblemStoreNode *node = reinterpret_cast(index.internalPointer()); if (node) { return node->label(); } } return {}; } if (role == SeverityRole) { return p->severity(); } else if (role == ProblemRole) { return QVariant::fromValue(p); } switch (role) { case Qt::DisplayRole: switch (index.column()) { case Source: return p->sourceString(); case Error: return p->description(); case File: return displayUrl(p->finalLocation().document.toUrl(), baseDirectory); case Line: if (p->finalLocation().isValid()) { return QString::number(p->finalLocation().start().line() + 1); } break; case Column: if (p->finalLocation().isValid()) { return QString::number(p->finalLocation().start().column() + 1); } break; } break; case Qt::DecorationRole: if (index.column() == Error) { return iconForSeverity(p->severity()); } break; case Qt::ToolTipRole: return p->explanation(); default: break; } return {}; } QModelIndex ProblemModel::parent(const QModelIndex& index) const { ProblemStoreNode *node = reinterpret_cast(index.internalPointer()); if (!node) { return {}; } ProblemStoreNode *parent = node->parent(); if (!parent || parent->isRoot()) { return {}; } int idx = parent->index(); return createIndex(idx, 0, parent); } QModelIndex ProblemModel::index(int row, int column, const QModelIndex& parent) const { if (row < 0 || row >= rowCount(parent) || column < 0 || column >= LastColumn) { return QModelIndex(); } ProblemStoreNode *parentNode = reinterpret_cast(parent.internalPointer()); const ProblemStoreNode *node = d->m_problems->findNode(row, parentNode); return createIndex(row, column, (void*)node); } int ProblemModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent) return LastColumn; } IProblem::Ptr ProblemModel::problemForIndex(const QModelIndex& index) const { ProblemStoreNode *node = reinterpret_cast(index.internalPointer()); if (!node) { return {}; } else { return node->problem(); } } ProblemModel::Features ProblemModel::features() const { return d->m_features; } void ProblemModel::setFeatures(Features features) { d->m_features = features; } void ProblemModel::addProblem(const IProblem::Ptr &problem) { int c = d->m_problems->count(); beginInsertRows(QModelIndex(), c, c + 1); d->m_problems->addProblem(problem); endInsertRows(); } void ProblemModel::setProblems(const QVector &problems) { beginResetModel(); d->m_problems->setProblems(problems); endResetModel(); } void ProblemModel::clearProblems() { beginResetModel(); d->m_problems->clear(); endResetModel(); } QVariant ProblemModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation); if (role != Qt::DisplayRole) return {}; switch (section) { case Source: return i18nc("@title:column source of problem", "Source"); case Error: return i18nc("@title:column problem description", "Problem"); case File: return i18nc("@title:column file where problem was found", "File"); case Line: return i18nc("@title:column line number with problem", "Line"); case Column: return i18nc("@title:column column number with problem", "Column"); } return {}; } void ProblemModel::setCurrentDocument(IDocument* document) { Q_ASSERT(thread() == QThread::currentThread()); QUrl currentDocument = document->url(); /// Will trigger signals beginRebuild(), endRebuild() if problems change and are rebuilt d->m_problems->setCurrentDocument(IndexedString(currentDocument)); } +void ProblemModel::closedDocument(IDocument* document) +{ + if (IndexedString(document->url()) == d->m_problems->currentDocument()) + { // reset current document + d->m_problems->setCurrentDocument(IndexedString()); + } +} + void ProblemModel::onBeginRebuild() { beginResetModel(); } void ProblemModel::onEndRebuild() { endResetModel(); } void ProblemModel::setScope(int scope) { Q_ASSERT(thread() == QThread::currentThread()); /// Will trigger signals beginRebuild(), endRebuild() if problems change and are rebuilt d->m_problems->setScope(scope); } void ProblemModel::setSeverity(int severity) { switch (severity) { case KDevelop::IProblem::Error: setSeverities(KDevelop::IProblem::Error); break; case KDevelop::IProblem::Warning: setSeverities(KDevelop::IProblem::Error | KDevelop::IProblem::Warning); break; case KDevelop::IProblem::Hint: setSeverities(KDevelop::IProblem::Error | KDevelop::IProblem::Warning | KDevelop::IProblem::Hint); break; } } void ProblemModel::setSeverities(KDevelop::IProblem::Severities severities) { Q_ASSERT(thread() == QThread::currentThread()); /// Will trigger signals beginRebuild(), endRebuild() if problems change and are rebuilt d->m_problems->setSeverities(severities); } void ProblemModel::setGrouping(int grouping) { /// Will trigger signals beginRebuild(), endRebuild() if problems change and are rebuilt d->m_problems->setGrouping(grouping); } ProblemStore* ProblemModel::store() const { return d->m_problems.data(); } } diff --git a/shell/problemmodel.h b/shell/problemmodel.h index 89c844874e..be962be9bc 100644 --- a/shell/problemmodel.h +++ b/shell/problemmodel.h @@ -1,184 +1,186 @@ /* * KDevelop Problem Reporter * * Copyright 2007 Hamish Rodda * Copyright 2015 Laszlo Kis-Adam * * 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. */ #ifndef PROBLEMMODEL_H #define PROBLEMMODEL_H #include #include #include #include struct ProblemModelPrivate; namespace KDevelop { class IDocument; class ProblemStore; /** * @brief Wraps a ProblemStore and adds the QAbstractItemModel interface, so the it can be used in a model/view architecture. * * By default ProblemModel instantiates a FilteredProblemStore, with the following features on: * \li ScopeFilter * \li SeverityFilter * \li Grouping * \li CanByPassScopeFilter * * Has to following columns: * \li Error * \li Source * \li File * \li Line * \li Column * \li LastColumn * * Possible ProblemModel features * \li NoFeatures * \li CanDoFullUpdate * \li CanShowImports * \li ScopeFilter * \li SeverityFilter * \li Grouping * \li CanByPassScopeFilter * * Scope, severity, grouping, imports can be set using the slots named after these features. * * Usage example: * @code * IProblem::Ptr problem(new DetectedProblem); * problem->setDescription(QStringLiteral("Problem")); * ProblemModel *model = new ProblemModel(nullptr); * model->addProblem(problem); * model->rowCount(); // returns 1 * QModelIndex idx = model->index(0, 0); * model->data(index); // "Problem" * @endcode * */ class KDEVPLATFORMSHELL_EXPORT ProblemModel : public QAbstractItemModel { Q_OBJECT public: /// List of supportable features enum FeatureCode { NoFeatures = 0, /// No features :( CanDoFullUpdate = 1, /// Reload/Reparse problems CanShowImports = 2, /// Show problems from imported files. E.g.: Header files in C/C++ ScopeFilter = 4, /// Filter problems by scope. E.g.: current document, open documents, etc SeverityFilter = 8, /// Filter problems by severity. E.g.: hint, warning, error, etc Grouping = 16, /// Can group problems CanByPassScopeFilter = 32 /// Can bypass scope filter }; Q_DECLARE_FLAGS(Features, FeatureCode) explicit ProblemModel(QObject *parent, ProblemStore *store = NULL); ~ProblemModel() override; enum Columns { Error, Source, File, Line, Column, LastColumn }; enum Roles { ProblemRole = Qt::UserRole + 1, SeverityRole }; int columnCount(const QModelIndex & parent = QModelIndex()) const override; QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex & index) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex & parent = QModelIndex()) const override; QVariant headerData ( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; IProblem::Ptr problemForIndex(const QModelIndex& index) const; /// Adds a new problem to the model void addProblem(const IProblem::Ptr &problem); /// Clears the problems, then adds a new set of them void setProblems(const QVector &problems); /// Clears the problems void clearProblems(); /// Retrieve the supported features Features features() const; /// Set the supported features void setFeatures(Features features); public slots: /// Show imports virtual void setShowImports(bool){} /// Sets the scope filter. Uses int to be able to use QSignalMapper virtual void setScope(int scope); /// Sets the severity filter. Uses int to be able to use QSignalMapper virtual void setSeverity(int severity);///old-style severity filtering virtual void setSeverities(KDevelop::IProblem::Severities severities);///new-style severity filtering void setGrouping(int grouping); /** * Force a full problem update. * E.g.: Reparse the source code. * Obviously it doesn't make sense for run-time problem checkers. */ virtual void forceFullUpdate(){} protected slots: /// Triggered when problems change virtual void onProblemsChanged(){} private slots: /// Triggered when the current document changes virtual void setCurrentDocument(IDocument* doc); + virtual void closedDocument(IDocument* doc); + /// Triggered before the problems are rebuilt void onBeginRebuild(); /// Triggered once the problems have been rebuilt void onEndRebuild(); protected: ProblemStore *store() const; private: QScopedPointer d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(ProblemModel::Features) } #endif // PROBLEMMODEL_H diff --git a/vcs/models/vcsfilechangesmodel.h b/vcs/models/vcsfilechangesmodel.h index 7aafe117de..c4cf6bdb82 100644 --- a/vcs/models/vcsfilechangesmodel.h +++ b/vcs/models/vcsfilechangesmodel.h @@ -1,160 +1,148 @@ /* This file is part of KDevelop Copyright 2010 Aleix Pol Split into separate class Copyright 2011 Andrey Batyiev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_VCSFILECHANGESMODEL_H #define KDEVPLATFORM_VCSFILECHANGESMODEL_H #include #include #include #include class QUrl; namespace KDevelop { class VcsStatusInfo; class KDEVPLATFORMVCS_EXPORT VcsFileChangesSortProxyModel : public QSortFilterProxyModel { Q_OBJECT public: VcsFileChangesSortProxyModel(QObject* parent = nullptr); bool lessThan(const QModelIndex& rLeft, const QModelIndex& rRight) const override; }; /** * This class holds and represents information about changes in files. * Also it is possible to provide tree like models by inheriting this class, see protected members. * All stuff should be pulled in by @p updateState. */ class VcsFileChangesModelPrivate; class KDEVPLATFORMVCS_EXPORT VcsFileChangesModel : public QStandardItemModel { Q_OBJECT public: enum ItemRoles { VcsStatusInfoRole = Qt::UserRole+1, UrlRole, StateRole, LastItemRole }; enum Column { PathColumn = 0, StatusColumn = 1 }; /** * Constructor for class. * @param isCheckable if true, model will show checkboxes on items. */ explicit VcsFileChangesModel(QObject *parent, bool isCheckable = false); ~VcsFileChangesModel() override; QVariant data(const QModelIndex &index, int role) const override; - /** - * Returns list of currently checked statuses. - */ - QList checkedStatuses() const { - return checkedStatuses(invisibleRootItem()); - } - /** * Returns list of currently checked urls. */ QList checkedUrls() const { return checkedUrls(invisibleRootItem()); } /** * Returns urls of all files * */ QList urls() const { return urls(invisibleRootItem()); } /** * Set the checked urls * */ void setCheckedUrls(const QList& urls) const { return checkUrls(invisibleRootItem(), urls); } /** * Changes the check-state of all files to the given state * */ void setAllChecked(bool checked); void setIsCheckbable(bool checkable); bool isCheckable() const; bool removeUrl(const QUrl& url); public slots: /** * Used to post update of status of some file. Any status except UpToDate * and Unknown will update (or add) item representation. */ void updateState(const KDevelop::VcsStatusInfo &status) { updateState(invisibleRootItem(), status); } protected: /** * Post update of status of some file. * @return changed row or -1 if row is deleted */ int updateState(QStandardItem *parent, const KDevelop::VcsStatusInfo &status); - /** - * Returns list of currently checked statuses. - */ - QList checkedStatuses(QStandardItem *parent) const; - /** * Returns list of currently checked urls. */ QList checkedUrls(QStandardItem *parent) const; /** * Checks the given urls, unchecks all others. * */ void checkUrls(QStandardItem *parent, const QList& urls) const; /** * Returns all urls * */ QList urls(QStandardItem *parent) const; /** * Returns item for particular url. */ QStandardItem* fileItemForUrl(QStandardItem *parent, const QUrl &url) const; private: QScopedPointer const d; }; } Q_DECLARE_METATYPE(KDevelop::VcsStatusInfo::State) #endif // KDEVPLATFORM_VCSFILECHANGESMODEL_H