diff --git a/debugger/variable/variablecollection.cpp b/debugger/variable/variablecollection.cpp index 41e4a9a162..2b70318f6d 100644 --- a/debugger/variable/variablecollection.cpp +++ b/debugger/variable/variablecollection.cpp @@ -1,542 +1,544 @@ /* * KDevelop Debugger Support * * Copyright 2007 Hamish Rodda * Copyright 2008 Vladimir Prus * Copyright 2009 Niko Sams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "variablecollection.h" #include #include #include #include #include #include #include #include #include "../../interfaces/icore.h" #include "../../interfaces/idocumentcontroller.h" #include "../../interfaces/iuicontroller.h" #include "../../sublime/controller.h" #include "../../sublime/view.h" #include "../../interfaces/idebugcontroller.h" #include "../interfaces/idebugsession.h" #include "../interfaces/ivariablecontroller.h" #include "util/debug.h" #include "util/texteditorhelpers.h" #include "variabletooltip.h" #include namespace KDevelop { IDebugSession* currentSession() { return ICore::self()->debugController()->currentSession(); } IDebugSession::DebuggerState currentSessionState() { if (!currentSession()) return IDebugSession::NotStartedState; return currentSession()->state(); } bool hasStartedSession() { IDebugSession::DebuggerState s = currentSessionState(); return s != IDebugSession::NotStartedState && s != IDebugSession::EndedState; } Variable::Variable(TreeModel* model, TreeItem* parent, const QString& expression, const QString& display) : TreeItem(model, parent), inScope_(true), topLevel_(true), changed_(false), showError_(false), m_format(Natural) { expression_ = expression; // FIXME: should not duplicate the data, instead overload 'data' // and return expression_ directly. if (display.isEmpty()) setData(QVector() << expression << QString() << QString()); else setData(QVector() << display << QString() << QString()); } QString Variable::expression() const { return expression_; } bool Variable::inScope() const { return inScope_; } void Variable::setValue(const QString& v) { itemData[VariableCollection::ValueColumn] = v; reportChange(); } QString Variable::value() const { return itemData[VariableCollection::ValueColumn].toString(); } void Variable::setType(const QString& type) { itemData[VariableCollection::TypeColumn] = type; reportChange(); } QString Variable::type() const { return itemData[VariableCollection::TypeColumn].toString(); } void Variable::setTopLevel(bool v) { topLevel_ = v; } void Variable::setInScope(bool v) { inScope_ = v; for (int i=0; i < childCount(); ++i) { if (Variable *var = dynamic_cast(child(i))) { var->setInScope(v); } } reportChange(); } void Variable::setShowError (bool v) { showError_ = v; reportChange(); } bool Variable::showError() { return showError_; } Variable::~Variable() { } void Variable::die() { removeSelf(); deleteLater(); } void Variable::setChanged(bool c) { changed_=c; reportChange(); } void Variable::resetChanged() { setChanged(false); for (int i=0; i(childItem)) { static_cast(childItem)->resetChanged(); } } } Variable::format_t Variable::str2format(const QString& str) { if(str==QLatin1String("Binary") || str==QLatin1String("binary")) return Binary; if(str==QLatin1String("Octal") || str==QLatin1String("octal")) return Octal; if(str==QLatin1String("Decimal") || str==QLatin1String("decimal")) return Decimal; if(str==QLatin1String("Hexadecimal") || str==QLatin1String("hexadecimal"))return Hexadecimal; return Natural; // maybe most reasonable default } QString Variable::format2str(format_t format) { switch(format) { case Natural: return QStringLiteral("natural"); case Binary: return QStringLiteral("binary"); case Octal: return QStringLiteral("octal"); case Decimal: return QStringLiteral("decimal"); case Hexadecimal: return QStringLiteral("hexadecimal"); default: return QString(); } } void Variable::setFormat(Variable::format_t format) { if (m_format != format) { m_format = format; formatChanged(); } } void Variable::formatChanged() { } bool Variable::isPotentialProblematicValue() const { const auto value = data(VariableCollection::ValueColumn, Qt::DisplayRole).toString(); return value == QLatin1String("0x0"); } QVariant Variable::data(int column, int role) const { if (showError_) { if (role == Qt::FontRole) { QVariant ret = TreeItem::data(column, role); QFont font = ret.value(); font.setStyle(QFont::StyleItalic); return font; } else if (column == 1 && role == Qt::DisplayRole) { return i18n("Error"); } } if (column == 1 && role == Qt::TextColorRole) { KColorScheme scheme(QPalette::Active); if (!inScope_) { return scheme.foreground(KColorScheme::InactiveText).color(); } else if (isPotentialProblematicValue()) { return scheme.foreground(KColorScheme::NegativeText).color(); } else if (changed_) { return scheme.foreground(KColorScheme::NeutralText).color(); } } if (role == Qt::ToolTipRole) { return TreeItem::data(column, Qt::DisplayRole); } return TreeItem::data(column, role); } Watches::Watches(TreeModel* model, TreeItem* parent) : TreeItem(model, parent), finishResult_(0) { setData(QVector() << i18n("Auto") << QString()); } Variable* Watches::add(const QString& expression) { if (!hasStartedSession()) return 0; Variable* v = currentSession()->variableController()->createVariable( model(), this, expression); appendChild(v); v->attachMaybe(); if (childCount() == 1 && !isExpanded()) { setExpanded(true); } return v; } Variable *Watches::addFinishResult(const QString& convenienceVarible) { if( finishResult_ ) { removeFinishResult(); } finishResult_ = currentSession()->variableController()->createVariable( model(), this, convenienceVarible, QStringLiteral("$ret")); appendChild(finishResult_); finishResult_->attachMaybe(); if (childCount() == 1 && !isExpanded()) { setExpanded(true); } return finishResult_; } void Watches::removeFinishResult() { if (finishResult_) { finishResult_->die(); finishResult_ = 0; } } void Watches::resetChanged() { for (int i=0; i(childItem)) { static_cast(childItem)->resetChanged(); } } } QVariant Watches::data(int column, int role) const { #if 0 if (column == 0 && role == Qt::FontRole) { /* FIXME: is creating font again and agian efficient? */ QFont f = font(); f.setBold(true); return f; } #endif return TreeItem::data(column, role); } void Watches::reinstall() { for (int i = 0; i < childItems.size(); ++i) { Variable* v = static_cast(child(i)); v->attachMaybe(); } } Locals::Locals(TreeModel* model, TreeItem* parent, const QString &name) : TreeItem(model, parent) { setData(QVector() << name << QString()); } QList Locals::updateLocals(QStringList locals) { QSet existing, current; for (int i = 0; i < childItems.size(); i++) { Q_ASSERT(dynamic_cast(child(i))); Variable* var= static_cast(child(i)); existing << var->expression(); } foreach (const QString& var, locals) { current << var; // If we currently don't display this local var, add it. if( !existing.contains( var ) ) { // FIXME: passing variableCollection this way is awkward. // In future, variableCollection probably should get a // method to create variable. Variable* v = currentSession()->variableController()->createVariable( ICore::self()->debugController()->variableCollection(), this, var ); appendChild( v, false ); } } for (int i = 0; i < childItems.size(); ++i) { KDevelop::Variable* v = static_cast(child(i)); if (!current.contains(v->expression())) { removeChild(i); --i; // FIXME: check that -var-delete is sent. delete v; } } if (hasMore()) { setHasMore(false); } // Variables which changed just value are updated by a call to -var-update. // Variables that changed type -- likewise. QList ret; foreach (TreeItem *i, childItems) { Q_ASSERT(dynamic_cast(i)); ret << static_cast(i); } return ret; } void Locals::resetChanged() { for (int i=0; i(childItem)) { static_cast(childItem)->resetChanged(); } } } VariablesRoot::VariablesRoot(TreeModel* model) : TreeItem(model) { watches_ = new Watches(model, this); appendChild(watches_, true); } Locals* VariablesRoot::locals(const QString& name) { if (!locals_.contains(name)) { locals_[name] = new Locals(model(), this, name); appendChild(locals_[name]); } return locals_[name]; } QHash VariablesRoot::allLocals() const { return locals_; } void VariablesRoot::resetChanged() { watches_->resetChanged(); foreach (Locals *l, locals_) { l->resetChanged(); } } VariableCollection::VariableCollection(IDebugController* controller) -: TreeModel(QVector() << i18n( "Name" ) << i18n( "Value" ) << i18n("Type"), controller), m_widgetVisible(false) + : TreeModel({i18n("Name"), i18n("Value"), i18n("Type")}, controller) + , m_widgetVisible(false) + , m_textHintProvider(this) { universe_ = new VariablesRoot(this); setRootItem(universe_); //new ModelTest(this); connect (ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &VariableCollection::textDocumentCreated ); connect(controller, &IDebugController::currentSessionChanged, this, &VariableCollection::updateAutoUpdate); // Qt5 signal slot syntax does not support default arguments auto callUpdateAutoUpdate = [&] { updateAutoUpdate(); }; connect(locals(), &Locals::expanded, this, callUpdateAutoUpdate); connect(locals(), &Locals::collapsed, this, callUpdateAutoUpdate); connect(watches(), &Watches::expanded, this, callUpdateAutoUpdate); connect(watches(), &Watches::collapsed, this, callUpdateAutoUpdate); } void VariableCollection::variableWidgetHidden() { m_widgetVisible = false; updateAutoUpdate(); } void VariableCollection::variableWidgetShown() { m_widgetVisible = true; updateAutoUpdate(); } void VariableCollection::updateAutoUpdate(IDebugSession* session) { if (!session) session = currentSession(); qCDebug(DEBUGGER) << session; if (!session) return; if (!m_widgetVisible) { session->variableController()->setAutoUpdate(IVariableController::UpdateNone); } else { QFlags t = IVariableController::UpdateNone; if (locals()->isExpanded()) t |= IVariableController::UpdateLocals; if (watches()->isExpanded()) t |= IVariableController::UpdateWatches; session->variableController()->setAutoUpdate(t); } } VariableCollection::~ VariableCollection() { } void VariableCollection::textDocumentCreated(IDocument* doc) { connect( doc->textDocument(), &KTextEditor::Document::viewCreated, this, &VariableCollection::viewCreated ); foreach( KTextEditor::View* view, doc->textDocument()->views() ) viewCreated( doc->textDocument(), view ); } void VariableCollection::viewCreated(KTextEditor::Document* doc, KTextEditor::View* view) { Q_UNUSED(doc); using namespace KTextEditor; TextHintInterface *iface = dynamic_cast(view); if( !iface ) return; - iface->registerTextHintProvider(new VariableProvider(this)); + iface->registerTextHintProvider(&m_textHintProvider); } VariableProvider::VariableProvider(VariableCollection* collection) : KTextEditor::TextHintProvider() , m_collection(collection) { } QString VariableProvider::textHint(KTextEditor::View* view, const KTextEditor::Cursor& cursor) { if (!hasStartedSession()) return QString(); if (ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("debug")) return QString(); //TODO: These keyboardModifiers should also hide already opened tooltip, and show another one for code area. if (QApplication::keyboardModifiers() == Qt::ControlModifier || QApplication::keyboardModifiers() == Qt::AltModifier){ return QString(); } KTextEditor::Document* doc = view->document(); KTextEditor::Range expressionRange = currentSession()->variableController()->expressionRangeUnderCursor(doc, cursor); if (!expressionRange.isValid()) return QString(); QString expression = doc->text(expressionRange).trimmed(); // Don't do anything if there's already an open tooltip with matching range if (m_collection->activeTooltip_ && m_collection->activeTooltip_->variable()->expression() == expression) return QString(); if (expression.isEmpty()) return QString(); QPoint local = view->cursorToCoordinate(cursor); QPoint global = view->mapToGlobal(local); QWidget* w = view->childAt(local); if (!w) w = view; m_collection->activeTooltip_ = new VariableToolTip(w, global+QPoint(30,30), expression); m_collection->activeTooltip_->setHandleRect(getItemBoundingRect(view, expressionRange)); return QString(); } } diff --git a/debugger/variable/variablecollection.h b/debugger/variable/variablecollection.h index fb24d3a30e..ad6dbd0d15 100644 --- a/debugger/variable/variablecollection.h +++ b/debugger/variable/variablecollection.h @@ -1,253 +1,254 @@ /* * KDevelop Debugger Support * * Copyright 2007 Hamish Rodda * Copyright 2008 Vladimir Prus * Copyright 2009 Niko Sams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_VARIABLECOLLECTION_H #define KDEVPLATFORM_VARIABLECOLLECTION_H #include #include #include #include #include #include #include "../util/treemodel.h" #include "../util/treeitem.h" #include "../../interfaces/idocument.h" #include "../interfaces/idebugsession.h" #include "../../interfaces/idebugcontroller.h" namespace GDBDebugger { class GdbTest; } namespace KDevelop { class VariableToolTip; class KDEVPLATFORMDEBUGGER_EXPORT Variable : public TreeItem { Q_OBJECT friend class GDBDebugger::GdbTest; public: protected: Variable(TreeModel* model, TreeItem* parent, const QString& expression, const QString& display = {}); public: enum format_t { Natural, Binary, Octal, Decimal, Hexadecimal }; static format_t str2format(const QString& str); static QString format2str(format_t format); QString expression() const; bool inScope() const; void setInScope(bool v); void setValue(const QString &v); QString value() const; void setType(const QString& type); QString type() const; void setTopLevel(bool v); void setShowError(bool v); bool showError(); using TreeItem::setHasMore; using TreeItem::setHasMoreInitial; using TreeItem::appendChild; using TreeItem::deleteChildren; using TreeItem::isExpanded; using TreeItem::parent; using TreeItem::model; ~Variable() override; /* Connects this variable to debugger, fetching the current value and otherwise preparing this variable to be displayed everywhere. The attempt may fail, for example if the expression is invalid. Calls slot 'callbackMethod' in 'callback' to notify of the result. The slot should be taking 'bool ok' parameter. */ virtual void attachMaybe(QObject *callback = 0, const char *callbackMethod = 0) = 0; virtual bool canSetFormat() const { return false; } void setFormat(format_t format); format_t format() const { return m_format; } virtual void formatChanged(); // get/set 'changed' state, if the variable changed it will be highlighted bool isChanged() const { return changed_; } void setChanged(bool c); void resetChanged(); public slots: void die(); protected: bool topLevel() const { return topLevel_; } private: // TreeItem overrides QVariant data(int column, int role) const override; private: bool isPotentialProblematicValue() const; QString expression_; bool inScope_; bool topLevel_; bool changed_; bool showError_; format_t m_format; }; class KDEVPLATFORMDEBUGGER_EXPORT TooltipRoot : public TreeItem { Q_OBJECT public: explicit TooltipRoot(TreeModel* model) : TreeItem(model) {} void init(Variable *var) { appendChild(var); } void fetchMoreChildren() override {} }; class KDEVPLATFORMDEBUGGER_EXPORT Watches : public TreeItem { Q_OBJECT friend class GDBDebugger::GdbTest; public: Watches(TreeModel* model, TreeItem* parent); Variable* add(const QString& expression); void reinstall(); Variable *addFinishResult(const QString& convenienceVarible); void removeFinishResult(); void resetChanged(); using TreeItem::childCount; friend class VariableCollection; friend class IVariableController; private: QVariant data(int column, int role) const override; void fetchMoreChildren() override {} Variable* finishResult_; }; class KDEVPLATFORMDEBUGGER_EXPORT Locals : public TreeItem { Q_OBJECT public: Locals(TreeModel* model, TreeItem* parent, const QString &name); QList updateLocals(QStringList locals); void resetChanged(); using TreeItem::deleteChildren; using TreeItem::setHasMore; friend class VariableCollection; friend class IVariableController; private: void fetchMoreChildren() override {} }; class KDEVPLATFORMDEBUGGER_EXPORT VariablesRoot : public TreeItem { Q_OBJECT public: explicit VariablesRoot(TreeModel* model); Watches *watches() const { return watches_; } Locals *locals(const QString &name = QStringLiteral("Locals")); QHash allLocals() const; void fetchMoreChildren() override {} void resetChanged(); private: Watches *watches_; QHash locals_; }; class VariableProvider : public KTextEditor::TextHintProvider { public: explicit VariableProvider(VariableCollection* collection); QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; private: VariableCollection* m_collection; }; class KDEVPLATFORMDEBUGGER_EXPORT VariableCollection : public TreeModel { Q_OBJECT public: enum Column { NameColumn, ValueColumn, TypeColumn }; explicit VariableCollection(IDebugController* parent); ~VariableCollection() override; VariablesRoot* root() const { return universe_; } Watches* watches() const { return universe_->watches(); } Locals* locals(const QString &name = i18n("Locals")) const { return universe_->locals(name); } QHash allLocals() const { return universe_->allLocals(); } public Q_SLOTS: void variableWidgetShown(); void variableWidgetHidden(); private Q_SLOTS: void updateAutoUpdate(KDevelop::IDebugSession* session = 0); void textDocumentCreated( KDevelop::IDocument*); void viewCreated(KTextEditor::Document*, KTextEditor::View*); private: VariablesRoot* universe_; QPointer activeTooltip_; bool m_widgetVisible; friend class VariableProvider; + VariableProvider m_textHintProvider; }; } #endif diff --git a/interfaces/iproblem.h b/interfaces/iproblem.h index b97aeda6d7..a3f2b93f27 100644 --- a/interfaces/iproblem.h +++ b/interfaces/iproblem.h @@ -1,120 +1,123 @@ /* * 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 IPROBLEM_H #define IPROBLEM_H #include #include #include #include namespace KDevelop { class IAssistant; /// Interface for the Problem classes class IProblem : public QSharedData { public: typedef QExplicitlySharedDataPointer Ptr; /// The source of the problem. That is which tool / which part found this problem. enum Source { Unknown, Disk, Preprocessor, Lexer, Parser, DUChainBuilder, SemanticAnalysis, ToDo, Plugin /// The source is a problem checker plugin }; /// Severity of the problem. That is, how serious is the found problem. enum Severity { - Error, - Warning, - Hint + NoSeverity = 0, + Error = 1, + Warning = 2, + Hint = 4 }; - + Q_DECLARE_FLAGS(Severities, Severity) IProblem(){} virtual ~IProblem(){} /// Returns the source of the problem virtual Source source() const = 0; /// Sets the source of the problem virtual void setSource(Source source) = 0; /// Returns a string containing the source of the problem virtual QString sourceString() const = 0; /// Returns the location of the problem (path, line, column) virtual KDevelop::DocumentRange finalLocation() const = 0; /// Sets the location of the problem (path, line, column) virtual void setFinalLocation(const KDevelop::DocumentRange& location) = 0; /// Returns the short description of the problem. virtual QString description() const = 0; /// Sets the short description of the problem virtual void setDescription(const QString& description) = 0; /// Returns the detailed explanation of the problem. virtual QString explanation() const = 0; /// Sets the detailed explanation of the problem virtual void setExplanation(const QString& explanation) = 0; /// Returns the severity of the problem virtual Severity severity() const = 0; /// Sets the severity of the problem virtual void setSeverity(Severity severity) = 0; /// Returns a string containing the severity of the problem virtual QString severityString() const = 0; /// Returns the diagnostics of the problem. virtual QVector diagnostics() const = 0; /// Sets the diagnostics of the problem virtual void setDiagnostics(const QVector &diagnostics) = 0; /// Adds a diagnostic line to the problem virtual void addDiagnostic(const Ptr &diagnostic) = 0; /// Clears all diagnostics virtual void clearDiagnostics() = 0; /// Returns a solution assistant for the problem, if applicable that is. virtual QExplicitlySharedDataPointer solutionAssistant() const = 0; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(IProblem::Severities) + } Q_DECLARE_METATYPE(KDevelop::IProblem::Ptr); #endif diff --git a/language/duchain/duchainregister.cpp b/language/duchain/duchainregister.cpp index 8e92ddfa72..09d8a2f0f5 100644 --- a/language/duchain/duchainregister.cpp +++ b/language/duchain/duchainregister.cpp @@ -1,80 +1,87 @@ /* 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 "duchainregister.h" #include "duchainbase.h" #include #define ENSURE_VALID_CLASSID(id) \ qFatal("Invalid class id: %i", id); namespace KDevelop { + +DUChainItemSystem::~DUChainItemSystem() +{ + qDeleteAll(m_factories); +} + DUChainBase* DUChainItemSystem::create(DUChainBaseData* data) const { if(uint(m_factories.size()) <= data->classId || m_factories[data->classId] == 0) return 0; return m_factories[data->classId]->create(data); } DUChainBaseData* DUChainItemSystem::cloneData(const DUChainBaseData& data) const { if(uint(m_factories.size()) <= data.classId || m_factories[data.classId] == 0) { ENSURE_VALID_CLASSID(data.classId) return 0; } return m_factories[data.classId]->cloneData(data); } void DUChainItemSystem::callDestructor(DUChainBaseData* data) const { if(uint(m_factories.size()) <= data->classId || m_factories[data->classId] == 0) return; return m_factories[data->classId]->callDestructor(data); } void DUChainItemSystem::freeDynamicData(KDevelop::DUChainBaseData* data) const { if(uint(m_factories.size()) <= data->classId || m_factories[data->classId] == 0) return; return m_factories[data->classId]->freeDynamicData(data); } uint DUChainItemSystem::dynamicSize(const DUChainBaseData& data) const { if(uint(m_factories.size()) <= data.classId || m_factories[data.classId] == 0) return 0; return m_factories[data.classId]->dynamicSize(data); } uint DUChainItemSystem::dataClassSize(const DUChainBaseData& data) const { if(uint(m_dataClassSizes.size()) <= data.classId || m_dataClassSizes[data.classId] == 0) return 0; return m_dataClassSizes[data.classId]; } void DUChainItemSystem::copy(const DUChainBaseData& from, DUChainBaseData& to, bool constant) const { if(uint(m_factories.size()) <= from.classId || m_factories[from.classId] == 0) { ENSURE_VALID_CLASSID(from.classId) return; } return m_factories[from.classId]->copy(from, to, constant); } DUChainItemSystem& DUChainItemSystem::self() { static DUChainItemSystem system; return system; } + } diff --git a/language/duchain/duchainregister.h b/language/duchain/duchainregister.h index f6ff9890e1..8b3dfa7410 100644 --- a/language/duchain/duchainregister.h +++ b/language/duchain/duchainregister.h @@ -1,208 +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_DUCHAINREGISTER_H #define KDEVPLATFORM_DUCHAINREGISTER_H #include "duchainbase.h" namespace KDevelop { class DUChainBase; class DUChainBaseData; ///This class is purely internal and doesn't need to be documented. It brings a "fake" type-info ///to classes that don't have type-info in the normal C++ way. ///Never use this directly, use the REGISTER_DUCHAIN_ITEM macro instead. class KDEVPLATFORMLANGUAGE_EXPORT DUChainBaseFactory { public: virtual DUChainBase* create(DUChainBaseData* data) const = 0; virtual void callDestructor(DUChainBaseData* data) const = 0; virtual void freeDynamicData(DUChainBaseData* data) const = 0; virtual void copy(const DUChainBaseData& from, DUChainBaseData& to, bool constant) const = 0; virtual DUChainBaseData* cloneData(const DUChainBaseData& data) const = 0; virtual uint dynamicSize(const DUChainBaseData& data) const = 0; virtual ~DUChainBaseFactory() { } }; ///Never use this directly, use the REGISTER_DUCHAIN_ITEM macro instead. template class DUChainItemFactory : public DUChainBaseFactory { public: DUChainBase* create(DUChainBaseData* data) const override { return new T(*static_cast(data)); } void copy(const DUChainBaseData& from, DUChainBaseData& to, bool constant) const override { Q_ASSERT(from.classId == T::Identity); bool& isConstant = DUChainBaseData::shouldCreateConstantData(); const bool previousConstant = isConstant; if (previousConstant != constant) { isConstant = constant; } new (&to) Data(static_cast(from)); //Call the copy constructor to initialize the target if (previousConstant != constant) { isConstant = previousConstant; } } void callDestructor(DUChainBaseData* data) const override { Q_ASSERT(data->classId == T::Identity); static_cast(data)->~Data(); } void freeDynamicData(DUChainBaseData* data) const override { Q_ASSERT(data->classId == T::Identity); static_cast(data)->freeDynamicData(); } uint dynamicSize(const DUChainBaseData& data) const override { Q_ASSERT(data.classId == T::Identity); return static_cast(data).dynamicSize(); } DUChainBaseData* cloneData(const DUChainBaseData& data) const override { Q_ASSERT(data.classId == T::Identity); return new Data(static_cast(data)); } }; /** * \short A class which registers data types and creates factories for them. * * DUChainItemSystem is a global static class which allows you to register new * DUChainBase subclasses for creation. */ class KDEVPLATFORMLANGUAGE_EXPORT DUChainItemSystem { public: /** * Register a new DUChainBase subclass. */ template void registerTypeClass() { if(m_factories.size() <= T::Identity) { m_factories.resize(T::Identity+1); m_dataClassSizes.resize(T::Identity+1); } Q_ASSERT_X(!m_factories[T::Identity], Q_FUNC_INFO, "This identity is already registered"); m_factories[T::Identity] = new DUChainItemFactory(); m_dataClassSizes[T::Identity] = sizeof(Data); } /** * Unregister an DUChainBase subclass. */ template void unregisterTypeClass() { Q_ASSERT(m_factories.size() > T::Identity); Q_ASSERT(m_factories[T::Identity]); delete m_factories[T::Identity]; m_factories[T::Identity] = 0; m_dataClassSizes[T::Identity] = 0; } /** * Create an DUChainBase for the given data. The returned type must be put into a DUChainBase::Ptr immediately. * Can return null if no type-factory is available for the given data (for example when a language-part is not loaded) */ DUChainBase* create(DUChainBaseData* data) const; ///Creates a dynamic copy of the given data DUChainBaseData* cloneData(const DUChainBaseData& data) const; /** * This just calls the correct constructor on the target. The target must be big enough to hold all the data. * If constant is true, it must be as big as dynamicSize(from). */ void copy(const DUChainBaseData& from, DUChainBaseData& to, bool constant) const; ///Calls the dynamicSize(..) member on the given data, in the most special class. Since we cannot use virtual functions, this is the only way. uint dynamicSize(const DUChainBaseData& data) const; ///Returns the size of the derived class, not including dynamic data. ///Returns zero if the class is not known. uint dataClassSize(const DUChainBaseData& data) const; ///Calls the destructor, but does not delete anything. This is needed because the data classes must not contain virtual members. ///This should only be called when a duchain data-pointer is semantically deleted, eg. when it does not persist on disk. void callDestructor(DUChainBaseData* data) const; ///Does not call the destructor, but frees all special data associated to dynamic data(the appendedlists stuff) ///This needs to be called whenever a dynamic duchain data-pointer is being deleted. void freeDynamicData(DUChainBaseData* data) const; /// Access the static DUChainItemSystem instance. static DUChainItemSystem& self(); private: + ~DUChainItemSystem(); + QVector m_factories; QVector m_dataClassSizes; }; template struct DUChainType {}; /// Use this in the header to declare DUChainType #define DUCHAIN_DECLARE_TYPE(Type) \ namespace KDevelop { \ template<> struct DUChainType { \ static void registerType(); \ static void unregisterType(); \ }; \ } /// Use this in the source file to define functions in DUChainType #define DUCHAIN_DEFINE_TYPE_WITH_DATA(Type, Data) \ void KDevelop::DUChainType::registerType() { DUChainItemSystem::self().registerTypeClass(); } \ void KDevelop::DUChainType::unregisterType() { DUChainItemSystem::self().unregisterTypeClass(); } #define DUCHAIN_DEFINE_TYPE(Type) \ DUCHAIN_DEFINE_TYPE_WITH_DATA(Type, Type##Data) /// Register @param T to DUChainItemSystem template void duchainRegisterType() { DUChainType::registerType(); } /// Unregister @param T to DUChainItemSystem template void duchainUnregisterType() { DUChainType::unregisterType(); } /// Helper class to register an DUChainBase subclass. /// /// Just use the REGISTER_TYPE(YourTypeClass) macro in your code, and you're done. template struct DUChainItemRegistrator { DUChainItemRegistrator() { DUChainItemSystem::self().registerTypeClass(); } ~DUChainItemRegistrator() { DUChainItemSystem::self().unregisterTypeClass(); } }; ///You must add this into your source-files for every DUChainBase based class ///For this to work, the class must have an "Identity" enumerator. ///It should be a unique value, but as small as possible, because a buffer at least as big as that number is created internally. #define REGISTER_DUCHAIN_ITEM(Class) KDevelop::DUChainItemRegistrator register ## Class #define REGISTER_DUCHAIN_ITEM_WITH_DATA(Class, Data) KDevelop::DUChainItemRegistrator register ## Class } #endif diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp index 1e40158413..cd1487ac1a 100644 --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -1,1346 +1,1347 @@ /* * 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 "contextbrowser.h" #include "contextbrowserview.h" #include "browsemanager.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(PLUGIN_CONTEXTBROWSER, "kdevplatform.plugins.contextbrowser") using KTextEditor::Attribute; using KTextEditor::View; // Helper that follows the QObject::parent() chain, and returns the highest widget that has no parent. QWidget* masterWidget(QWidget* w) { while(w && w->parent() && qobject_cast(w->parent())) w = qobject_cast(w->parent()); return w; } namespace { const unsigned int highlightingTimeout = 150; const float highlightingZDepth = -5000; const int maxHistoryLength = 30; // Helper that determines the context to use for highlighting at a specific position DUContext* contextForHighlightingAt(const KTextEditor::Cursor& position, TopDUContext* topContext) { DUContext* ctx = topContext->findContextAt(topContext->transformToLocalRevision(position)); while(ctx && ctx->parentContext() && (ctx->type() == DUContext::Template || ctx->type() == DUContext::Helper || ctx->localScopeIdentifier().isEmpty())) { ctx = ctx->parentContext(); } return ctx; } ///Duchain must be locked DUContext* getContextAt(const QUrl& url, KTextEditor::Cursor cursor) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (!topContext) return 0; return contextForHighlightingAt(KTextEditor::Cursor(cursor), topContext); } DeclarationPointer cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return DeclarationPointer(); } DUChainReadLocker lock; Declaration *decl = DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor(view->cursorPosition()))); return DeclarationPointer(decl); } } class ContextBrowserViewFactory: public KDevelop::IToolViewFactory { public: ContextBrowserViewFactory(ContextBrowserPlugin *plugin): m_plugin(plugin) {} QWidget* create(QWidget *parent = 0) override { ContextBrowserView* ret = new ContextBrowserView(m_plugin, parent); return ret; } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ContextBrowser"); } private: ContextBrowserPlugin *m_plugin; }; KXMLGUIClient* ContextBrowserPlugin::createGUIForMainWindow( Sublime::MainWindow* window ) { KXMLGUIClient* ret = KDevelop::IPlugin::createGUIForMainWindow( window ); m_browseManager = new BrowseManager(this); connect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); m_previousButton = new QToolButton(); m_previousButton->setToolTip(i18n("Go back in context history")); m_previousButton->setPopupMode(QToolButton::MenuButtonPopup); m_previousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); m_previousButton->setEnabled(false); m_previousButton->setFocusPolicy(Qt::NoFocus); m_previousMenu = new QMenu(); m_previousButton->setMenu(m_previousMenu); connect(m_previousButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyPrevious); connect(m_previousMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::previousMenuAboutToShow); m_nextButton = new QToolButton(); m_nextButton->setToolTip(i18n("Go forward in context history")); m_nextButton->setPopupMode(QToolButton::MenuButtonPopup); m_nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); m_nextButton->setEnabled(false); m_nextButton->setFocusPolicy(Qt::NoFocus); m_nextMenu = new QMenu(); m_nextButton->setMenu(m_nextMenu); connect(m_nextButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyNext); connect(m_nextMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::nextMenuAboutToShow); IQuickOpen* quickOpen = KDevelop::ICore::self()->pluginController()->extensionForPlugin(QStringLiteral("org.kdevelop.IQuickOpen")); if(quickOpen) { m_outlineLine = quickOpen->createQuickOpenLine(QStringList(), QStringList() << i18n("Outline"), IQuickOpen::Outline); m_outlineLine->setDefaultText(i18n("Outline...")); m_outlineLine->setToolTip(i18n("Navigate outline of active document, click to browse.")); } connect(m_browseManager, &BrowseManager::startDelayedBrowsing, this, &ContextBrowserPlugin::startDelayedBrowsing); connect(m_browseManager, &BrowseManager::stopDelayedBrowsing, this, &ContextBrowserPlugin::stopDelayedBrowsing); m_toolbarWidget = toolbarWidgetForMainWindow(window); m_toolbarWidgetLayout = new QHBoxLayout; m_toolbarWidgetLayout->setSizeConstraint(QLayout::SetMaximumSize); m_previousButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_nextButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_toolbarWidgetLayout->setMargin(0); m_toolbarWidgetLayout->addWidget(m_previousButton); if (m_outlineLine) { m_toolbarWidgetLayout->addWidget(m_outlineLine); m_outlineLine->setMaximumWidth(600); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, m_outlineLine.data(), &IQuickOpenLine::clear); } m_toolbarWidgetLayout->addWidget(m_nextButton); if(m_toolbarWidget->children().isEmpty()) m_toolbarWidget->setLayout(m_toolbarWidgetLayout); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ContextBrowserPlugin::documentActivated); return ret; } void ContextBrowserPlugin::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevcontextbrowser.rc") ; QAction* previousContext = actions.addAction(QStringLiteral("previous_context")); previousContext->setText( i18n("&Previous Visited Context") ); previousContext->setIcon( QIcon::fromTheme(QStringLiteral("go-previous-context") ) ); actions.setDefaultShortcut( previousContext, Qt::META | Qt::Key_Left ); QObject::connect(previousContext, &QAction::triggered, this, &ContextBrowserPlugin::previousContextShortcut); QAction* nextContext = actions.addAction(QStringLiteral("next_context")); nextContext->setText( i18n("&Next Visited Context") ); nextContext->setIcon( QIcon::fromTheme(QStringLiteral("go-next-context") ) ); actions.setDefaultShortcut( nextContext, Qt::META | Qt::Key_Right ); QObject::connect(nextContext, &QAction::triggered, this, &ContextBrowserPlugin::nextContextShortcut); QAction* previousUse = actions.addAction(QStringLiteral("previous_use")); previousUse->setText( i18n("&Previous Use") ); previousUse->setIcon( QIcon::fromTheme(QStringLiteral("go-previous-use")) ); actions.setDefaultShortcut( previousUse, Qt::META | Qt::SHIFT | Qt::Key_Left ); QObject::connect(previousUse, &QAction::triggered, this, &ContextBrowserPlugin::previousUseShortcut); QAction* nextUse = actions.addAction(QStringLiteral("next_use")); nextUse->setText( i18n("&Next Use") ); nextUse->setIcon( QIcon::fromTheme(QStringLiteral("go-next-use")) ); actions.setDefaultShortcut( nextUse, Qt::META | Qt::SHIFT | Qt::Key_Right ); QObject::connect(nextUse, &QAction::triggered, this, &ContextBrowserPlugin::nextUseShortcut); QWidgetAction* outline = new QWidgetAction(this); outline->setText(i18n("Context Browser")); QWidget* w = toolbarWidgetForMainWindow(window); w->setHidden(false); outline->setDefaultWidget(w); actions.addAction(QStringLiteral("outline_line"), outline); // Add to the actioncollection so one can set global shortcuts for the action actions.addAction(QStringLiteral("find_uses"), m_findUses); } void ContextBrowserPlugin::nextContextShortcut() { // TODO: cleanup historyNext(); } void ContextBrowserPlugin::previousContextShortcut() { // TODO: cleanup historyPrevious(); } K_PLUGIN_FACTORY_WITH_JSON(ContextBrowserFactory, "kdevcontextbrowser.json", registerPlugin();) ContextBrowserPlugin::ContextBrowserPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevcontextbrowser"), parent) , m_viewFactory(new ContextBrowserViewFactory(this)) , m_nextHistoryIndex(0) + , m_textHintProvider(this) { KDEV_USE_EXTENSION_INTERFACE( IContextBrowser ) core()->uiController()->addToolView(i18n("Code Browser"), m_viewFactory); connect( core()->documentController(), &IDocumentController::textDocumentCreated, this, &ContextBrowserPlugin::textDocumentCreated ); connect( DUChain::self(), &DUChain::updateReady, this, &ContextBrowserPlugin::updateReady); connect( DUChain::self(), &DUChain::declarationSelected, this, &ContextBrowserPlugin::declarationSelectedInUI ); m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect( m_updateTimer, &QTimer::timeout, this, &ContextBrowserPlugin::updateViews ); //Needed global action for the context-menu extensions m_findUses = new QAction(i18n("Find Uses"), this); connect(m_findUses, &QAction::triggered, this, &ContextBrowserPlugin::findUses); } ContextBrowserPlugin::~ContextBrowserPlugin() { ///TODO: QObject inheritance should suffice? delete m_nextMenu; delete m_previousMenu; delete m_toolbarWidgetLayout; delete m_previousButton; delete m_outlineLine; delete m_nextButton; } void ContextBrowserPlugin::unload() { core()->uiController()->removeToolView(m_viewFactory); } KDevelop::ContextMenuExtension ContextBrowserPlugin::contextMenuExtension(KDevelop::Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker lock(DUChain::lock()); if(!codeContext->declaration().data()) return menuExt; qRegisterMetaType("KDevelop::IndexedDeclaration"); menuExt.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, m_findUses); return menuExt; } void ContextBrowserPlugin::showUses(const DeclarationPointer& declaration) { QMetaObject::invokeMethod(this, "showUsesDelayed", Qt::QueuedConnection, Q_ARG(KDevelop::DeclarationPointer, declaration)); } void ContextBrowserPlugin::showUsesDelayed(const DeclarationPointer& declaration) { DUChainReadLocker lock; Declaration* decl = declaration.data(); if(!decl) { return; } QWidget* toolView = ICore::self()->uiController()->findToolView(i18n("Code Browser"), m_viewFactory, KDevelop::IUiController::CreateAndRaise); if(!toolView) { return; } ContextBrowserView* view = dynamic_cast(toolView); Q_ASSERT(view); view->allowLockedUpdate(); view->setDeclaration(decl, decl->topContext(), true); //We may get deleted while the call to acceptLink, so make sure we don't crash in that case QPointer widget = dynamic_cast(view->navigationWidget()); if(widget && widget->context()) { NavigationContextPointer nextContext = widget->context()->execute( NavigationAction(declaration, KDevelop::NavigationAction::ShowUses)); if(widget) { widget->setContext( nextContext ); } } } void ContextBrowserPlugin::findUses() { showUses(cursorDeclaration()); } ContextBrowserHintProvider::ContextBrowserHintProvider(ContextBrowserPlugin* plugin) : m_plugin(plugin) { } QString ContextBrowserHintProvider::textHint(View* view, const KTextEditor::Cursor& cursor) { m_plugin->m_mouseHoverCursor = KTextEditor::Cursor(cursor); if(!view) { qWarning() << "could not cast to view"; }else{ m_plugin->m_mouseHoverDocument = view->document()->url(); m_plugin->m_updateViews << view; } m_plugin->m_updateTimer->start(1); // triggers updateViews() m_plugin->showToolTip(view, cursor); return QString(); } void ContextBrowserPlugin::stopDelayedBrowsing() { hideToolTip(); } void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if(!m_currentToolTip) { showToolTip(view, view->cursorPosition()); } } void ContextBrowserPlugin::hideToolTip() { if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = 0; m_currentNavigationWidget = 0; } } void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { ContextBrowserView* contextView = browserViewForWidget(view); if(contextView && contextView->isVisible() && !contextView->isLocked()) return; // If the context-browser view is visible, it will care about updating by itself QUrl viewUrl = view->document()->url(); auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); QWidget* navigationWidget = 0; { DUChainReadLocker lock(DUChain::lock()); foreach (const auto language, languages) { auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, KTextEditor::Cursor(position)); navigationWidget = qobject_cast(widget); if(navigationWidget) break; } if(!navigationWidget) { Declaration* decl = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(viewUrl, KTextEditor::Cursor(position)) ); if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { if(m_currentToolTipDeclaration == IndexedDeclaration(decl) && m_currentToolTip) return; m_currentToolTipDeclaration = IndexedDeclaration(decl); navigationWidget = decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); } } } if(navigationWidget) { // If we have an invisible context-view, assign the tooltip navigation-widget to it. // If the user makes the context-view visible, it will instantly contain the correct widget. if(contextView && !contextView->isLocked()) contextView->setNavigationWidget(navigationWidget); if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = 0; m_currentNavigationWidget = 0; } KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, view->mapToGlobal(view->cursorToCoordinate(position)) + QPoint(20, 40), navigationWidget); KTextEditor::Range itemRange; { DUChainReadLocker lock; itemRange = DUChainUtils::itemRangeUnderCursor(viewUrl, KTextEditor::Cursor(position)); } tooltip->setHandleRect(getItemBoundingRect(view, itemRange)); tooltip->resize( navigationWidget->sizeHint() + QSize(10, 10) ); qCDebug(PLUGIN_CONTEXTBROWSER) << "tooltip size" << tooltip->size(); m_currentToolTip = tooltip; m_currentNavigationWidget = navigationWidget; ActiveToolTip::showToolTip(tooltip); if ( ! navigationWidget->property("DoNotCloseOnCursorMove").toBool() ) { connect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip, Qt::UniqueConnection); } else { disconnect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip); } }else{ qCDebug(PLUGIN_CONTEXTBROWSER) << "not showing tooltip, no navigation-widget"; } } void ContextBrowserPlugin::clearMouseHover() { m_mouseHoverCursor = KTextEditor::Cursor::invalid(); m_mouseHoverDocument.clear(); } Attribute::Ptr highlightedUseAttribute() { static Attribute::Ptr standardAttribute = Attribute::Ptr(); if( !standardAttribute ) { standardAttribute= Attribute::Ptr( new Attribute() ); standardAttribute->setBackgroundFillWhitespace(true); // mixing (255, 255, 0, 100) with white yields this: standardAttribute->setBackground(QColor(251, 250, 150)); // force a foreground color to overwrite default Kate highlighting, i.e. of Q_OBJECT or similar // foreground color could change, hence apply it everytime standardAttribute->setForeground(QColor(0, 0, 0, 255)); //Don't use alpha here, as kate uses the alpha only to blend with the document background color } return standardAttribute; } Attribute::Ptr highlightedSpecialObjectAttribute() { static Attribute::Ptr standardAttribute = Attribute::Ptr(); if( !standardAttribute ) { standardAttribute = Attribute::Ptr( new Attribute() ); standardAttribute->setBackgroundFillWhitespace(true); // mixing (90, 255, 0, 100) with white yields this: standardAttribute->setBackground(QColor(190, 255, 155)); // force a foreground color to overwrite default Kate highlighting, i.e. of Q_OBJECT or similar // foreground color could change, hence apply it everytime standardAttribute->setForeground(QColor(0, 0, 0, 255)); //Don't use alpha here, as kate uses the alpha only to blend with the document background color } return standardAttribute; } void ContextBrowserPlugin::addHighlight( View* view, KDevelop::Declaration* decl ) { if( !view || !decl ) { qCDebug(PLUGIN_CONTEXTBROWSER) << "invalid view/declaration"; return; } ViewHighlights& highlights(m_highlightedRanges[view]); KDevelop::DUChainReadLocker lock; // Highlight the declaration highlights.highlights << decl->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); // Highlight uses { QMap< IndexedString, QList< KTextEditor::Range > > currentRevisionUses = decl->usesCurrentRevision(); for(QMap< IndexedString, QList< KTextEditor::Range > >::iterator fileIt = currentRevisionUses.begin(); fileIt != currentRevisionUses.end(); ++fileIt) { for(QList< KTextEditor::Range >::const_iterator useIt = (*fileIt).constBegin(); useIt != (*fileIt).constEnd(); ++useIt) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(*useIt, fileIt.key())); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } } } if( FunctionDefinition* def = FunctionDefinition::definition(decl) ) { highlights.highlights << def->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } } Declaration* ContextBrowserPlugin::findDeclaration(View* view, const KTextEditor::Cursor& position, bool mouseHighlight) { Q_UNUSED(mouseHighlight); Declaration* foundDeclaration = 0; if(m_useDeclaration.data()) { foundDeclaration = m_useDeclaration.data(); }else{ //If we haven't found a special language object, search for a use/declaration and eventually highlight it foundDeclaration = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(view->document()->url(), position) ); if (foundDeclaration && foundDeclaration->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(foundDeclaration); Q_ASSERT(alias); DUChainReadLocker lock; foundDeclaration = alias->aliasedDeclaration().declaration(); } } return foundDeclaration; } ContextBrowserView* ContextBrowserPlugin::browserViewForWidget(QWidget* widget) { foreach(ContextBrowserView* contextView, m_views) { if(masterWidget(contextView) == masterWidget(widget)) { return contextView; } } return 0; } void ContextBrowserPlugin::updateForView(View* view) { bool allowHighlight = true; if(view->selection()) { // If something is selected, we unhighlight everything, so that we don't conflict with the // kate plugin that highlights occurrences of the selected string, and also to reduce the // overall amount of concurrent highlighting. allowHighlight = false; } if(m_highlightedRanges[view].keep) { m_highlightedRanges[view].keep = false; return; } // Clear all highlighting m_highlightedRanges.clear(); // Re-highlight ViewHighlights& highlights = m_highlightedRanges[view]; QUrl url = view->document()->url(); IDocument* activeDoc = core()->documentController()->activeDocument(); bool mouseHighlight = (url == m_mouseHoverDocument) && (m_mouseHoverCursor.isValid()); bool shouldUpdateBrowser = (mouseHighlight || (view == ICore::self()->documentController()->activeTextDocumentView() && activeDoc && activeDoc->textDocument() == view->document())); KTextEditor::Cursor highlightPosition; if (mouseHighlight) highlightPosition = m_mouseHoverCursor; else highlightPosition = KTextEditor::Cursor(view->cursorPosition()); ///Pick a language ILanguageSupport* language = nullptr; if(ICore::self()->languageController()->languagesForUrl(url).isEmpty()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "found no language for document" << url; return; }else{ language = ICore::self()->languageController()->languagesForUrl(url).front(); } ///Check whether there is a special language object to highlight (for example a macro) KTextEditor::Range specialRange = language->specialLanguageObjectRange(url, highlightPosition); ContextBrowserView* updateBrowserView = shouldUpdateBrowser ? browserViewForWidget(view) : 0; if(specialRange.isValid()) { // Highlight a special language object if(allowHighlight) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(specialRange, IndexedString(url))); highlights.highlights.back()->setAttribute(highlightedSpecialObjectAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } if(updateBrowserView) updateBrowserView->setSpecialNavigationWidget(language->specialLanguageObjectNavigationWidget(url, highlightPosition)); }else{ KDevelop::DUChainReadLocker lock( DUChain::lock(), 100 ); if(!lock.locked()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "Failed to lock du-chain in time"; return; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (!topContext) return; DUContext* ctx = contextForHighlightingAt(highlightPosition, topContext); if (!ctx) return; //Only update the history if this context is around the text cursor if(core()->documentController()->activeDocument() && highlightPosition == KTextEditor::Cursor(view->cursorPosition()) && view->document() == core()->documentController()->activeDocument()->textDocument()) { updateHistory(ctx, highlightPosition); } Declaration* foundDeclaration = findDeclaration(view, highlightPosition, mouseHighlight); if( foundDeclaration ) { m_lastHighlightedDeclaration = highlights.declaration = IndexedDeclaration(foundDeclaration); if(allowHighlight) addHighlight( view, foundDeclaration ); if(updateBrowserView) updateBrowserView->setDeclaration(foundDeclaration, topContext); }else{ if(updateBrowserView) updateBrowserView->setContext(ctx); } } } void ContextBrowserPlugin::updateViews() { foreach( View* view, m_updateViews ) { updateForView(view); } m_updateViews.clear(); m_useDeclaration = IndexedDeclaration(); } void ContextBrowserPlugin::declarationSelectedInUI(const DeclarationPointer& decl) { m_useDeclaration = IndexedDeclaration(decl.data()); KTextEditor::View* view = core()->documentController()->activeTextDocumentView(); if(view) m_updateViews << view; if(!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); // triggers updateViews() } void ContextBrowserPlugin::updateReady(const IndexedString& file, const ReferencedTopDUContext& /*topContext*/) { const auto url = file.toUrl(); for(QMap< View*, ViewHighlights >::iterator it = m_highlightedRanges.begin(); it != m_highlightedRanges.end(); ++it) { if(it.key()->document()->url() == url) { if(!m_updateViews.contains(it.key())) { qCDebug(PLUGIN_CONTEXTBROWSER) << "adding view for update"; m_updateViews << it.key(); // Don't change the highlighted declaration after finished parse-jobs (*it).keep = true; } } } if(!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); } void ContextBrowserPlugin::textDocumentCreated( KDevelop::IDocument* document ) { Q_ASSERT(document->textDocument()); connect( document->textDocument(), &KTextEditor::Document::viewCreated, this, &ContextBrowserPlugin::viewCreated ); foreach( View* view, document->textDocument()->views() ) viewCreated( document->textDocument(), view ); } void ContextBrowserPlugin::documentActivated( IDocument* doc ) { if (m_outlineLine) m_outlineLine->clear(); if (View* view = doc->activeTextView()) { cursorPositionChanged(view, view->cursorPosition()); } } void ContextBrowserPlugin::viewDestroyed( QObject* obj ) { m_highlightedRanges.remove(static_cast(obj)); m_updateViews.remove(static_cast(obj)); } void ContextBrowserPlugin::selectionChanged( View* view ) { clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout/2); // triggers updateViews() } void ContextBrowserPlugin::cursorPositionChanged( View* view, const KTextEditor::Cursor& newPosition ) { if(view->document() == m_lastInsertionDocument && newPosition == m_lastInsertionPos) { //Do not update the highlighting while typing m_lastInsertionDocument = 0; m_lastInsertionPos = KTextEditor::Cursor(); if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = true; }else{ if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = false; } clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout/2); // triggers updateViews() } void ContextBrowserPlugin::textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text) { m_lastInsertionDocument = doc; m_lastInsertionPos = cursor + KTextEditor::Cursor(0, text.size()); } void ContextBrowserPlugin::viewCreated( KTextEditor::Document* , View* v ) { disconnect( v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged ); ///Just to make sure that multiple connections don't happen connect( v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged ); connect( v, &View::destroyed, this, &ContextBrowserPlugin::viewDestroyed ); disconnect( v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); connect(v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); disconnect(v, &View::selectionChanged, this, &ContextBrowserPlugin::selectionChanged); KTextEditor::TextHintInterface *iface = dynamic_cast(v); if( !iface ) return; iface->setTextHintDelay(highlightingTimeout); - iface->registerTextHintProvider(new ContextBrowserHintProvider(this)); + iface->registerTextHintProvider(&m_textHintProvider); } void ContextBrowserPlugin::registerToolView(ContextBrowserView* view) { m_views << view; } void ContextBrowserPlugin::previousUseShortcut() { switchUse(false); } void ContextBrowserPlugin::nextUseShortcut() { switchUse(true); } KTextEditor::Range cursorToRange(KTextEditor::Cursor cursor) { return KTextEditor::Range(cursor, cursor); } void ContextBrowserPlugin::switchUse(bool forward) { View* view = core()->documentController()->activeTextDocumentView(); if(view) { KTextEditor::Document* doc = view->document(); KDevelop::DUChainReadLocker lock( DUChain::lock() ); KDevelop::TopDUContext* chosen = DUChainUtils::standardContextForUrl(doc->url()); if( chosen ) { KTextEditor::Cursor cCurrent(view->cursorPosition()); KDevelop::CursorInRevision c = chosen->transformToLocalRevision(cCurrent); Declaration* decl = 0; //If we have a locked declaration, use that for jumping foreach(ContextBrowserView* view, m_views) { decl = view->lockedDeclaration().data(); ///@todo Somehow match the correct context-browser view if there is multiple if(decl) break; } if(!decl) //Try finding a declaration under the cursor decl = DUChainUtils::itemUnderCursor(doc->url(), cCurrent); if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { Declaration* target = 0; if(forward) //Try jumping from definition to declaration target = DUChainUtils::declarationForDefinition(decl, chosen); else if(decl->url().toUrl() == doc->url() && decl->range().contains(c)) //Try jumping from declaration to definition target = FunctionDefinition::definition(decl); if(target && target != decl) { KTextEditor::Cursor jumpTo = target->rangeInCurrentRevision().start(); QUrl document = target->url().toUrl(); lock.unlock(); core()->documentController()->openDocument( document, cursorToRange(jumpTo) ); return; }else{ //Always work with the declaration instead of the definition decl = DUChainUtils::declarationForDefinition(decl, chosen); } } if(!decl) { //Pick the last use we have highlighted decl = m_lastHighlightedDeclaration.data(); } if(decl) { KDevVarLengthArray usingFiles = DUChain::uses()->uses(decl->id()); if(DUChainUtils::contextHasUse(decl->topContext(), decl) && usingFiles.indexOf(decl->topContext()) == -1) usingFiles.insert(0, decl->topContext()); if(decl->range().contains(c) && decl->url() == chosen->url()) { //The cursor is directly on the declaration. Jump to the first or last use. if(!usingFiles.isEmpty()) { TopDUContext* top = (forward ? usingFiles[0] : usingFiles.back()).data(); if(top) { QList useRanges = allUses(top, decl, true); std::sort(useRanges.begin(), useRanges.end()); if(!useRanges.isEmpty()) { QUrl url = top->url().toUrl(); KTextEditor::Range selectUse = chosen->transformFromLocalRevision(forward ? useRanges.first() : useRanges.back()); lock.unlock(); core()->documentController()->openDocument(url, cursorToRange(selectUse.start())); } } } return; } //Check whether we are within a use QList localUses = allUses(chosen, decl, true); std::sort(localUses.begin(), localUses.end()); for(int a = 0; a < localUses.size(); ++a) { int nextUse = (forward ? a+1 : a-1); bool pick = localUses[a].contains(c); if(!pick && forward && a+1 < localUses.size() && localUses[a].end <= c && localUses[a+1].start > c) { //Special case: We aren't on a use, but we are jumping forward, and are behind this and the next use pick = true; } if(!pick && !forward && a-1 >= 0 && c < localUses[a].start && c >= localUses[a-1].end) { //Special case: We aren't on a use, but we are jumping backward, and are in front of this use, but behind the previous one pick = true; } if(!pick && a == 0 && c < localUses[a].start) { if(!forward) { //Will automatically jump to previous file }else{ nextUse = 0; //We are before the first use, so jump to it. } pick = true; } if(!pick && a == localUses.size()-1 && c >= localUses[a].end) { if(forward) { //Will automatically jump to next file }else{ //We are behind the last use, but moving backward. So pick the last use. nextUse = a; } pick = true; } if(pick) { //Make sure we end up behind the use if(nextUse != a) while(forward && nextUse < localUses.size() && (localUses[nextUse].start <= localUses[a].end || localUses[nextUse].isEmpty())) ++nextUse; //Make sure we end up before the use if(nextUse != a) while(!forward && nextUse >= 0 && (localUses[nextUse].start >= localUses[a].start || localUses[nextUse].isEmpty())) --nextUse; //Jump to the next use qCDebug(PLUGIN_CONTEXTBROWSER) << "count of uses:" << localUses.size() << "nextUse" << nextUse; if(nextUse < 0 || nextUse == localUses.size()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "jumping to next file"; //Jump to the first use in the next using top-context int indexInFiles = usingFiles.indexOf(chosen); if(indexInFiles != -1) { int nextFile = (forward ? indexInFiles+1 : indexInFiles-1); qCDebug(PLUGIN_CONTEXTBROWSER) << "current file" << indexInFiles << "nextFile" << nextFile; if(nextFile < 0 || nextFile >= usingFiles.size()) { //Open the declaration, or the definition if(nextFile >= usingFiles.size()) { Declaration* definition = FunctionDefinition::definition(decl); if(definition) decl = definition; } QUrl u = decl->url().toUrl(); KTextEditor::Range range = decl->rangeInCurrentRevision(); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); return; }else{ TopDUContext* nextTop = usingFiles[nextFile].data(); QUrl u = nextTop->url().toUrl(); QList nextTopUses = allUses(nextTop, decl, true); std::sort(nextTopUses.begin(), nextTopUses.end()); if(!nextTopUses.isEmpty()) { KTextEditor::Range range = chosen->transformFromLocalRevision(forward ? nextTopUses.front() : nextTopUses.back()); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); } return; } }else{ qCDebug(PLUGIN_CONTEXTBROWSER) << "not found own file in use list"; } }else{ QUrl url = chosen->url().toUrl(); KTextEditor::Range range = chosen->transformFromLocalRevision(localUses[nextUse]); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(url, range); return; } } } } } } } void ContextBrowserPlugin::unRegisterToolView(ContextBrowserView* view) { m_views.removeAll(view); } // history browsing QWidget* ContextBrowserPlugin::toolbarWidgetForMainWindow( Sublime::MainWindow* window ) { //TODO: support multiple windows (if that ever gets revived) if (!m_toolbarWidget) { m_toolbarWidget = new QWidget(window); } return m_toolbarWidget; } void ContextBrowserPlugin::documentJumpPerformed( KDevelop::IDocument* newDocument, const KTextEditor::Cursor& newCursor, KDevelop::IDocument* previousDocument, const KTextEditor::Cursor& previousCursor) { DUChainReadLocker lock(DUChain::lock()); /*TODO: support multiple windows if that ever gets revived if(newDocument && newDocument->textDocument() && newDocument->textDocument()->activeView() && masterWidget(newDocument->textDocument()->activeView()) != masterWidget(this)) return; */ if(previousDocument && previousCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump source"; DUContext* context = getContextAt(previousDocument->url(), previousCursor); if(context) { updateHistory(context, KTextEditor::Cursor(previousCursor), true); }else{ //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(previousDocument->url()), KTextEditor::Cursor(previousCursor)))); ++m_nextHistoryIndex; } } qCDebug(PLUGIN_CONTEXTBROWSER) << "new doc: " << newDocument << " new cursor: " << newCursor; if(newDocument && newCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump target"; DUContext* context = getContextAt(newDocument->url(), newCursor); if(context) { updateHistory(context, KTextEditor::Cursor(newCursor), true); }else{ //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(newDocument->url()), KTextEditor::Cursor(newCursor)))); ++m_nextHistoryIndex; if (m_outlineLine) m_outlineLine->clear(); } } } void ContextBrowserPlugin::updateButtonState() { m_nextButton->setEnabled( m_nextHistoryIndex < m_history.size() ); m_previousButton->setEnabled( m_nextHistoryIndex >= 2 ); } void ContextBrowserPlugin::historyNext() { if(m_nextHistoryIndex >= m_history.size()) { return; } openDocument(m_nextHistoryIndex); // opening the document at given position // will update the widget for us ++m_nextHistoryIndex; updateButtonState(); } void ContextBrowserPlugin::openDocument(int historyIndex) { Q_ASSERT_X(historyIndex >= 0, "openDocument", "negative history index"); Q_ASSERT_X(historyIndex < m_history.size(), "openDocument", "history index out of range"); DocumentCursor c = m_history[historyIndex].computePosition(); if (c.isValid() && !c.document.str().isEmpty()) { disconnect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); ICore::self()->documentController()->openDocument(c.document.toUrl(), c); connect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); updateDeclarationListBox(m_history[historyIndex].context.data()); } } void ContextBrowserPlugin::historyPrevious() { if(m_nextHistoryIndex < 2) { return; } --m_nextHistoryIndex; openDocument(m_nextHistoryIndex-1); // opening the document at given position // will update the widget for us updateButtonState(); } QString ContextBrowserPlugin::actionTextFor(int historyIndex) const { const HistoryEntry& entry = m_history.at(historyIndex); QString actionText = entry.context.data() ? entry.context.data()->scopeIdentifier(true).toString() : QString(); if(actionText.isEmpty()) actionText = entry.alternativeString; if(actionText.isEmpty()) actionText = QStringLiteral(""); actionText += QLatin1String(" @ "); QString fileName = entry.absoluteCursorPosition.document.toUrl().fileName(); actionText += QStringLiteral("%1:%2").arg(fileName).arg(entry.absoluteCursorPosition.line()+1); return actionText; } /* inline QDebug operator<<(QDebug debug, const ContextBrowserPlugin::HistoryEntry &he) { DocumentCursor c = he.computePosition(); debug << "\n\tHistoryEntry " << c.line << " " << c.document.str(); return debug; } */ void ContextBrowserPlugin::nextMenuAboutToShow() { QList indices; for(int a = m_nextHistoryIndex; a < m_history.size(); ++a) { indices << a; } fillHistoryPopup(m_nextMenu, indices); } void ContextBrowserPlugin::previousMenuAboutToShow() { QList indices; for(int a = m_nextHistoryIndex-2; a >= 0; --a) { indices << a; } fillHistoryPopup(m_previousMenu, indices); } void ContextBrowserPlugin::fillHistoryPopup(QMenu* menu, const QList& historyIndices) { menu->clear(); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); foreach(int index, historyIndices) { QAction* action = new QAction(actionTextFor(index), menu); action->setData(index); menu->addAction(action); connect(action, &QAction::triggered, this, &ContextBrowserPlugin::actionTriggered); } } bool ContextBrowserPlugin::isPreviousEntry(KDevelop::DUContext* context, const KTextEditor::Cursor& /*position*/) const { if (m_nextHistoryIndex == 0) return false; Q_ASSERT(m_nextHistoryIndex <= m_history.count()); const HistoryEntry& he = m_history.at(m_nextHistoryIndex-1); KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); // is this necessary?? Q_ASSERT(context); return IndexedDUContext(context) == he.context; } void ContextBrowserPlugin::updateHistory(KDevelop::DUContext* context, const KTextEditor::Cursor& position, bool force) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating history"; if(m_outlineLine && m_outlineLine->isVisible()) updateDeclarationListBox(context); if(!context || (!context->owner() && !force)) { return; //Only add history-entries for contexts that have owners, which in practice should be functions and classes //This keeps the history cleaner } if (isPreviousEntry(context, position)) { if(m_nextHistoryIndex) { HistoryEntry& he = m_history[m_nextHistoryIndex-1]; he.setCursorPosition(position); } return; } else { // Append new history entry m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(IndexedDUContext(context), position)); ++m_nextHistoryIndex; updateButtonState(); if(m_history.size() > (maxHistoryLength + 5)) { m_history = m_history.mid(m_history.size() - maxHistoryLength); m_nextHistoryIndex = m_history.size(); } } } void ContextBrowserPlugin::updateDeclarationListBox(DUContext* context) { if(!context || !context->owner()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "not updating box"; m_listUrl = IndexedString(); ///@todo Compute the context in the document here if (m_outlineLine) m_outlineLine->clear(); return; } Declaration* decl = context->owner(); m_listUrl = context->url(); Declaration* specialDecl = SpecializationStore::self().applySpecialization(decl, decl->topContext()); FunctionType::Ptr function = specialDecl->type(); QString text = specialDecl->qualifiedIdentifier().toString(); if(function) text += function->partToString(KDevelop::FunctionType::SignatureArguments); if(m_outlineLine && !m_outlineLine->hasFocus()) { m_outlineLine->setText(text); m_outlineLine->setCursorPosition(0); } qCDebug(PLUGIN_CONTEXTBROWSER) << "updated" << text; } void ContextBrowserPlugin::actionTriggered() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); Q_ASSERT(action->data().type() == QVariant::Int); int historyPosition = action->data().toInt(); // qCDebug(PLUGIN_CONTEXTBROWSER) << "history pos" << historyPosition << m_history.size() << m_history; if(historyPosition >= 0 && historyPosition < m_history.size()) { m_nextHistoryIndex = historyPosition + 1; openDocument(historyPosition); updateButtonState(); } } void ContextBrowserPlugin::doNavigate(NavigationActionType action) { KTextEditor::View* view = qobject_cast(sender()); if(!view) { qWarning() << "sender is not a view"; return; } KTextEditor::CodeCompletionInterface* iface = qobject_cast(view); if(!iface || iface->isCompletionActive()) return; // If code completion is active, the actions should be handled by the completion widget QWidget* widget = m_currentNavigationWidget.data(); if(!widget || !widget->isVisible()) { ContextBrowserView* contextView = browserViewForWidget(view); if(contextView) widget = contextView->navigationWidget(); } if(widget) { AbstractNavigationWidget* navWidget = qobject_cast(widget); if (navWidget) { switch(action) { case Accept: navWidget->accept(); break; case Back: navWidget->back(); break; case Left: navWidget->previous(); break; case Right: navWidget->next(); break; case Up: navWidget->up(); break; case Down: navWidget->down(); break; } } } } void ContextBrowserPlugin::navigateAccept() { doNavigate(Accept); } void ContextBrowserPlugin::navigateBack() { doNavigate(Back); } void ContextBrowserPlugin::navigateDown() { doNavigate(Down); } void ContextBrowserPlugin::navigateLeft() { doNavigate(Left); } void ContextBrowserPlugin::navigateRight() { doNavigate(Right); } void ContextBrowserPlugin::navigateUp() { doNavigate(Up); } //BEGIN HistoryEntry ContextBrowserPlugin::HistoryEntry::HistoryEntry(KDevelop::DocumentCursor pos) : absoluteCursorPosition(pos) { } ContextBrowserPlugin::HistoryEntry::HistoryEntry(IndexedDUContext ctx, const KTextEditor::Cursor& cursorPosition) : context(ctx) { //Use a position relative to the context setCursorPosition(cursorPosition); if(ctx.data()) alternativeString = ctx.data()->scopeIdentifier(true).toString(); if(!alternativeString.isEmpty()) alternativeString += i18n("(changed)"); //This is used when the context was deleted in between } DocumentCursor ContextBrowserPlugin::HistoryEntry::computePosition() const { KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); DocumentCursor ret; if(context.data()) { ret = DocumentCursor(context.data()->url(), relativeCursorPosition); ret.setLine(ret.line() + context.data()->range().start.line); }else{ ret = absoluteCursorPosition; } return ret; } void ContextBrowserPlugin::HistoryEntry::setCursorPosition(const KTextEditor::Cursor& cursorPosition) { KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); if(context.data()) { absoluteCursorPosition = DocumentCursor(context.data()->url(), cursorPosition); relativeCursorPosition = cursorPosition; relativeCursorPosition.setLine(relativeCursorPosition.line() - context.data()->range().start.line); } } // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on #include "contextbrowser.moc" diff --git a/plugins/contextbrowser/contextbrowser.h b/plugins/contextbrowser/contextbrowser.h index 976cb13670..260694cb09 100644 --- a/plugins/contextbrowser/contextbrowser.h +++ b/plugins/contextbrowser/contextbrowser.h @@ -1,269 +1,271 @@ /* * 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_CONTEXTBROWSERPLUGIN_H #define KDEVPLATFORM_PLUGIN_CONTEXTBROWSERPLUGIN_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class QHBoxLayout; class QMenu; class QToolButton; namespace Sublime { class MainWindow; } namespace KDevelop { class IDocument; class DUContext; class TopDUContext; class ReferencedTopDUContext; class DUChainBase; class AbstractNavigationWidget; } namespace KTextEditor { class Document; class View; } class ContextBrowserViewFactory; class ContextBrowserView; +class ContextBrowserPlugin; class BrowseManager; +class ContextBrowserHintProvider : public KTextEditor::TextHintProvider +{ +public: + explicit ContextBrowserHintProvider(ContextBrowserPlugin* plugin); + QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; + +private: + ContextBrowserPlugin* m_plugin; +}; + QWidget* masterWidget(QWidget* w); struct ViewHighlights { ViewHighlights() : keep(false) { } // Whether the same highlighting should be kept highlighted (usually during typing) bool keep; // The declaration that is highlighted for this view KDevelop::IndexedDeclaration declaration; // Highlighted ranges. Those may also be contained by different views. QList highlights; }; class ContextBrowserPlugin : public KDevelop::IPlugin, public KDevelop::IContextBrowser { Q_OBJECT Q_INTERFACES( KDevelop::IContextBrowser ) public: explicit ContextBrowserPlugin(QObject *parent, const QVariantList & = QVariantList() ); ~ContextBrowserPlugin() override; void unload() override; void registerToolView(ContextBrowserView* view); void unRegisterToolView(ContextBrowserView* view); KDevelop::ContextMenuExtension contextMenuExtension(KDevelop::Context*) override; KXMLGUIClient* createGUIForMainWindow( Sublime::MainWindow* window ) override; ///duchain must be locked ///@param force When this is true, the history-entry is added, no matter whether the context is "interesting" or not void updateHistory(KDevelop::DUContext* context, const KTextEditor::Cursor& cursorPosition, bool force = false); void updateDeclarationListBox(KDevelop::DUContext* context); void showUses(const KDevelop::DeclarationPointer& declaration) override; public Q_SLOTS: void showUsesDelayed(const KDevelop::DeclarationPointer& declaration); void previousContextShortcut(); void nextContextShortcut(); void startDelayedBrowsing(KTextEditor::View* view); void stopDelayedBrowsing(); void previousUseShortcut(); void nextUseShortcut(); void declarationSelectedInUI(const KDevelop::DeclarationPointer& decl); void updateReady(const KDevelop::IndexedString& url, const KDevelop::ReferencedTopDUContext& topContext); void textDocumentCreated( KDevelop::IDocument* document ); void documentActivated( KDevelop::IDocument* ); void viewDestroyed( QObject* obj ); void cursorPositionChanged( KTextEditor::View* view, const KTextEditor::Cursor& newPosition ); void viewCreated( KTextEditor::Document* , KTextEditor::View* ); void updateViews(); void hideToolTip(); void findUses(); void textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text); void selectionChanged(KTextEditor::View*); void historyNext(); void historyPrevious(); private slots: // history browsing void documentJumpPerformed( KDevelop::IDocument* newDocument, const KTextEditor::Cursor& newCursor, KDevelop::IDocument* previousDocument, const KTextEditor::Cursor& previousCursor); void nextMenuAboutToShow(); void previousMenuAboutToShow(); void actionTriggered(); void navigateLeft(); void navigateRight(); void navigateUp(); void navigateDown(); void navigateAccept(); void navigateBack(); private: QWidget* toolbarWidgetForMainWindow(Sublime::MainWindow* window); void createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) override; void switchUse(bool forward); void clearMouseHover(); void addHighlight( KTextEditor::View* view, KDevelop::Declaration* decl ); /** helper for updateBrowserView(). * Tries to find a 'specialLanguageObject' (eg macro) in @p view under cursor @c. * If found returns true and sets @p pickedLanguage to the language this object belongs to */ KDevelop::Declaration* findDeclaration(KTextEditor::View* view, const KTextEditor::Cursor&, bool mouseHighlight); void updateForView(KTextEditor::View* view); // history browsing bool isPreviousEntry(KDevelop::DUContext*, const KTextEditor::Cursor& cursor) const; QString actionTextFor(int historyIndex) const; void updateButtonState(); void openDocument(int historyIndex); void fillHistoryPopup(QMenu* menu, const QList& historyIndices); enum NavigationActionType { Accept, Back, Down, Up, Left, Right }; void doNavigate(NavigationActionType action); private: // Returns the currently active and visible context browser view that belongs // to the same context (mainwindow and area) as the given widget ContextBrowserView* browserViewForWidget(QWidget* widget); void showToolTip(KTextEditor::View* view, KTextEditor::Cursor position); QTimer* m_updateTimer; //Contains the range, the old attribute, and the attribute it was replaced with QSet m_updateViews; QMap m_highlightedRanges; //Holds a list of all active context browser tool views QList m_views; //Used to override the next declaration that will be highlighted KDevelop::IndexedDeclaration m_useDeclaration; KDevelop::IndexedDeclaration m_lastHighlightedDeclaration; QUrl m_mouseHoverDocument; KTextEditor::Cursor m_mouseHoverCursor; ContextBrowserViewFactory* m_viewFactory; QPointer m_currentToolTip; QPointer m_currentNavigationWidget; KDevelop::IndexedDeclaration m_currentToolTipDeclaration; QAction* m_findUses; QPointer m_lastInsertionDocument; KTextEditor::Cursor m_lastInsertionPos; // outline toolbar QPointer m_outlineLine; QPointer m_toolbarWidgetLayout; QPointer m_toolbarWidget; // history browsing struct HistoryEntry { //Duchain must be locked HistoryEntry(KDevelop::IndexedDUContext ctx = KDevelop::IndexedDUContext(), const KTextEditor::Cursor& cursorPosition = KTextEditor::Cursor()); HistoryEntry(KDevelop::DocumentCursor pos); //Duchain must be locked void setCursorPosition(const KTextEditor::Cursor& cursorPosition); //Duchain does not need to be locked KDevelop::DocumentCursor computePosition() const; KDevelop::IndexedDUContext context; KDevelop::DocumentCursor absoluteCursorPosition; KTextEditor::Cursor relativeCursorPosition; //Cursor position relative to the start line of the context QString alternativeString; }; QVector m_history; QPointer m_previousButton; QPointer m_nextButton; QPointer m_previousMenu, m_nextMenu; QList m_listDeclarations; KDevelop::IndexedString m_listUrl; BrowseManager* m_browseManager; //Used to not record jumps triggered by the context-browser as history entries QPointer m_focusBackWidget; int m_nextHistoryIndex; friend class ContextBrowserHintProvider; -}; - -class ContextBrowserHintProvider : public KTextEditor::TextHintProvider -{ -public: - explicit ContextBrowserHintProvider(ContextBrowserPlugin* plugin); - QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; - -private: - ContextBrowserPlugin* m_plugin; + ContextBrowserHintProvider m_textHintProvider; }; #endif // KDEVPLATFORM_PLUGIN_CONTEXTBROWSERPLUGIN_H // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on diff --git a/plugins/openwith/kdevopenwith.json b/plugins/openwith/kdevopenwith.json index 5736041c4f..29d89c997d 100644 --- a/plugins/openwith/kdevopenwith.json +++ b/plugins/openwith/kdevopenwith.json @@ -1,68 +1,69 @@ { "KPlugin": { "Authors": [ { "Name": "Andreas Pakulat", "Name[x-test]": "xxAndreas Pakulatxx" } ], "Category": "Core", "Description": "This plugin allows one to open files with associated external applications.", "Description[ca]": "Aquest connector permet obrir fitxers amb les aplicacions externes associades.", "Description[de]": "Mit diesem Modul können Dateien mit ihnen zugewiesenen externen Anwendungen gestartet werden.", + "Description[en_GB]": "This plugin allows one to open files with associated external applications.", "Description[es]": "Este complemento permite abrir archivos con las aplicaciones externas asociadas.", "Description[fi]": "Tämä liitännäinen mahdollistaa ulkoisiin sovelluksiin liitettyjen tiedostojen avaamisen.", "Description[gl]": "Este complemento permítelle abrir ficheiros cos programas externos asociados.", "Description[nl]": "Deze plugin biedt u het openen van bestanden met geassocieerde externe toepassingen.", "Description[pl]": "Ta wtyczka umożliwia otwieranie plików, z którymi są powiązane zewnętrzne programy.", "Description[pt]": "Este 'plugin' permite abrir os ficheiros com as aplicações externas associadas.", "Description[pt_BR]": "Este plugin permite abrir os arquivos com os aplicativos externos associados.", "Description[sl]": "Vstavek omogoča odpiranje datotek v povezanih zunanjih programih.", "Description[uk]": "За допомогою цього додатка ви зможете відкривати файли у пов’язаній з ними зовнішній програмі.", "Description[x-test]": "xxThis plugin allows one to open files with associated external applications.xx", "Icon": "document-open", "Id": "kdevopenwith", "License": "GPL", "Name": "Open With", "Name[ar]": "افتح بِـ", "Name[bg]": "Отваряне с", "Name[bs]": "Otvori pomoću", "Name[ca@valencia]": "Obri amb", "Name[ca]": "Obre amb", "Name[da]": "Åbn med", "Name[de]": "Öffnen mit", "Name[el]": "Άνοιγμα με", "Name[es]": "Abrir con", "Name[et]": "Avamine rakendusega", "Name[fi]": "Avaa ohjelmalla", "Name[gl]": "Abrir con", "Name[hu]": "Megnyitás ezzel", "Name[it]": "Apri con", "Name[kk]": "Мынамен ашу", "Name[mr]": "यामध्ये उघडा", "Name[nb]": "Åpne med", "Name[nds]": "Opmaken mit", "Name[nl]": "Openen met", "Name[pl]": "Otwórz za pomocą", "Name[pt]": "Abrir Com", "Name[pt_BR]": "Abrir com", "Name[ru]": "Открыть с помощью", "Name[sk]": "Otvoriť s", "Name[sl]": "Odpri z", "Name[sv]": "Öppna med", "Name[tr]": "Birlikte Aç", "Name[ug]": "بۇنىڭدا ئېچىش", "Name[uk]": "Відкриття у зовнішніх програмах", "Name[x-test]": "xxOpen Withxx", "Name[zh_CN]": "打开方式", "Name[zh_TW]": "開啟方式", "ServiceTypes": [ "KDevelop/Plugin" ] }, "X-KDevelop-Category": "Global", "X-KDevelop-Interfaces": [ "org.kdevelop.IOpenWith" ], "X-KDevelop-Mode": "GUI" } \ No newline at end of file diff --git a/plugins/problemreporter/problemhighlighter.cpp b/plugins/problemreporter/problemhighlighter.cpp index 9dd6ddabb2..02848111e3 100644 --- a/plugins/problemreporter/problemhighlighter.cpp +++ b/plugins/problemreporter/problemhighlighter.cpp @@ -1,259 +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 "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(new ProblemTextHintProvider(this)); + 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/problemhighlighter.h b/plugins/problemreporter/problemhighlighter.h index 0a0b7414fc..5a9f10c706 100644 --- a/plugins/problemreporter/problemhighlighter.h +++ b/plugins/problemreporter/problemhighlighter.h @@ -1,70 +1,73 @@ /* * 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. */ #ifndef KDEVPLATFORM_PLUGIN_PROBLEM_HIGHLIGHT_H #define KDEVPLATFORM_PLUGIN_PROBLEM_HIGHLIGHT_H #include #include #include #include #include +class ProblemHighlighter; + +class ProblemTextHintProvider : public KTextEditor::TextHintProvider +{ +public: + explicit ProblemTextHintProvider(ProblemHighlighter* highlighter); + + QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; + +private: + ProblemHighlighter* m_highlighter; + KTextEditor::Range m_currentHintRange; +}; + class ProblemHighlighter : public QObject { Q_OBJECT public: explicit ProblemHighlighter(KTextEditor::Document* document); ~ProblemHighlighter() override; void setProblems(const QVector& problems); private slots: void viewCreated(KTextEditor::Document*, KTextEditor::View*); void aboutToRemoveText(const KTextEditor::Range& range); void clearProblems(); private: QPointer m_document; QList m_topHLRanges; QVector m_problems; QMap m_problemsForRanges; friend class ProblemTextHintProvider; + ProblemTextHintProvider m_textHintProvider; public slots: void settingsChanged(); }; -class ProblemTextHintProvider : public KTextEditor::TextHintProvider -{ -public: - explicit ProblemTextHintProvider(ProblemHighlighter* highlighter); - - QString textHint(KTextEditor::View* view, const KTextEditor::Cursor& position) override; - -private: - ProblemHighlighter* m_highlighter; - KTextEditor::Range m_currentHintRange; -}; - #endif // KDEVPLATFORM_PLUGIN_PROBLEM_HIGHLIGHT_H diff --git a/plugins/problemreporter/problemreportermodel.cpp b/plugins/problemreporter/problemreportermodel.cpp index 517804cd80..c9fea11080 100644 --- a/plugins/problemreporter/problemreportermodel.cpp +++ b/plugins/problemreporter/problemreportermodel.cpp @@ -1,192 +1,192 @@ /* * 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. */ #include "problemreportermodel.h" #include #include #include #include #include #include #include #include #include #include #include -#include +#include #include using namespace KDevelop; const int ProblemReporterModel::MinTimeout = 1000; const int ProblemReporterModel::MaxTimeout = 5000; ProblemReporterModel::ProblemReporterModel(QObject* parent) - : ProblemModel(parent, new ProblemStore()) + : ProblemModel(parent, new FilteredProblemStore()) , m_showImports(false) { setFeatures(CanDoFullUpdate | CanShowImports | ScopeFilter | SeverityFilter); m_minTimer = new QTimer(this); m_minTimer->setInterval(MinTimeout); m_minTimer->setSingleShot(true); connect(m_minTimer, &QTimer::timeout, this, &ProblemReporterModel::timerExpired); m_maxTimer = new QTimer(this); m_maxTimer->setInterval(MaxTimeout); m_maxTimer->setSingleShot(true); connect(m_maxTimer, &QTimer::timeout, this, &ProblemReporterModel::timerExpired); - connect(store(), &ProblemStore::changed, this, &ProblemReporterModel::onProblemsChanged); + connect(store(), &FilteredProblemStore::changed, this, &ProblemReporterModel::onProblemsChanged); } ProblemReporterModel::~ProblemReporterModel() { } QVector ProblemReporterModel::problems(const KDevelop::IndexedString& url, bool showImports) const { QVector result; QSet visitedContexts; KDevelop::DUChainReadLocker lock; problemsInternal(KDevelop::DUChain::self()->chainForDocument(url), showImports, visitedContexts, result); return result; } QVector ProblemReporterModel::problems(const QSet& urls, bool showImports) const { QVector result; QSet visitedContexts; KDevelop::DUChainReadLocker lock; foreach (const KDevelop::IndexedString& url, urls) { problemsInternal(KDevelop::DUChain::self()->chainForDocument(url), showImports, visitedContexts, result); } return result; } void ProblemReporterModel::forceFullUpdate() { Q_ASSERT(thread() == QThread::currentThread()); QSet documents = store()->documents()->get(); KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); foreach (const KDevelop::IndexedString& document, documents) { if (document.isEmpty()) continue; KDevelop::TopDUContext::Features updateType = KDevelop::TopDUContext::ForceUpdate; if (documents.size() == 1) updateType = KDevelop::TopDUContext::ForceUpdateRecursive; KDevelop::DUChain::self()->updateContextForUrl( document, (KDevelop::TopDUContext::Features)(updateType | KDevelop::TopDUContext::VisibleDeclarationsAndContexts)); } } void ProblemReporterModel::problemsInternal(KDevelop::TopDUContext* context, bool showImports, QSet& visitedContexts, QVector& result) const { if (!context || visitedContexts.contains(context)) { return; } foreach (KDevelop::ProblemPointer p, context->problems()) { if (p && p->severity() <= store()->severity()) { result.append(p); } } visitedContexts.insert(context); if (showImports) { bool isProxy = context->parsingEnvironmentFile() && context->parsingEnvironmentFile()->isProxyContext(); foreach (const KDevelop::DUContext::Import& ctx, context->importedParentContexts()) { if (!ctx.indexedContext().indexedTopContext().isLoaded()) continue; KDevelop::TopDUContext* topCtx = dynamic_cast(ctx.context(0)); if (topCtx) { /// If we are starting at a proxy-context, only recurse into other proxy-contexts, because those contain the problems. if (!isProxy || (topCtx->parsingEnvironmentFile() && topCtx->parsingEnvironmentFile()->isProxyContext())) problemsInternal(topCtx, showImports, visitedContexts, result); } } } } void ProblemReporterModel::onProblemsChanged() { rebuildProblemList(); } void ProblemReporterModel::timerExpired() { m_minTimer->stop(); m_maxTimer->stop(); rebuildProblemList(); } void ProblemReporterModel::setCurrentDocument(KDevelop::IDocument* doc) { Q_ASSERT(thread() == QThread::currentThread()); beginResetModel(); QUrl currentDocument = doc->url(); /// Will trigger signal changed() if problems change store()->setCurrentDocument(KDevelop::IndexedString(currentDocument)); endResetModel(); } void ProblemReporterModel::problemsUpdated(const KDevelop::IndexedString& url) { Q_ASSERT(thread() == QThread::currentThread()); if (store()->documents()->get().contains(url)) { /// m_minTimer will expire in MinTimeout unless some other parsing job finishes in this period. m_minTimer->start(); /// m_maxTimer will expire unconditionally in MaxTimeout if (!m_maxTimer->isActive()) { m_maxTimer->start(); } } } void ProblemReporterModel::setShowImports(bool showImports) { if (m_showImports != showImports) { Q_ASSERT(thread() == QThread::currentThread()); m_showImports = showImports; rebuildProblemList(); } } void ProblemReporterModel::rebuildProblemList() { QVector problems; /// No locking here, because it may be called from an already locked context beginResetModel(); problems = this->problems(store()->documents()->get(), m_showImports); store()->setProblems(problems); endResetModel(); } diff --git a/plugins/problemreporter/problemtreeview.cpp b/plugins/problemreporter/problemtreeview.cpp index d69f479c98..d56e6c8346 100644 --- a/plugins/problemreporter/problemtreeview.cpp +++ b/plugins/problemreporter/problemtreeview.cpp @@ -1,388 +1,393 @@ /* * KDevelop Problem Reporter * * Copyright (c) 2006-2007 Hamish Rodda * Copyright 2006 Adam Treat * * 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 "problemtreeview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "problemreporterplugin.h" #include #include #include //#include "modeltest.h" using namespace KDevelop; namespace KDevelop { class ProblemTreeViewItemDelegate : public QItemDelegate { Q_OBJECT public: explicit ProblemTreeViewItemDelegate(QObject* parent = nullptr); void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; } ProblemTreeViewItemDelegate::ProblemTreeViewItemDelegate(QObject* parent) : QItemDelegate(parent) { } void ProblemTreeViewItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem newOption(option); newOption.textElideMode = index.column() == ProblemModel::File ? Qt::ElideMiddle : Qt::ElideRight; QItemDelegate::paint(painter, newOption, index); } ProblemTreeView::ProblemTreeView(QWidget* parent, QAbstractItemModel* itemModel) : QTreeView(parent) , m_proxy(new QSortFilterProxyModel(this)) + , errorSeverityAction(nullptr) + , warningSeverityAction(nullptr) + , hintSeverityAction(nullptr) { - setObjectName(QStringLiteral("Problem Reporter Tree")); setWhatsThis(i18n("Problems")); setItemDelegate(new ProblemTreeViewItemDelegate(this)); setSelectionBehavior(QAbstractItemView::SelectRows); m_proxy->setSortRole(ProblemModel::SeverityRole); m_proxy->setDynamicSortFilter(true); m_proxy->sort(0, Qt::AscendingOrder); ProblemModel* problemModel = dynamic_cast(itemModel); Q_ASSERT(problemModel); setModel(problemModel); header()->setStretchLastSection(false); if (problemModel->features().testFlag(ProblemModel::CanDoFullUpdate)) { QAction* fullUpdateAction = new QAction(this); fullUpdateAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); fullUpdateAction->setText(i18n("Force Full Update")); fullUpdateAction->setToolTip(i18nc("@info:tooltip", "Re-parse all watched documents")); fullUpdateAction->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect(fullUpdateAction, &QAction::triggered, model(), &ProblemModel::forceFullUpdate); addAction(fullUpdateAction); } if (problemModel->features().testFlag(ProblemModel::CanShowImports)) { QAction* showImportsAction = new QAction(this); addAction(showImportsAction); showImportsAction->setCheckable(true); showImportsAction->setChecked(false); showImportsAction->setText(i18n("Show Imports")); showImportsAction->setToolTip(i18nc("@info:tooltip", "Display problems in imported files")); this->model()->setShowImports(false); connect(showImportsAction, &QAction::triggered, model(), &ProblemModel::setShowImports); } if (problemModel->features().testFlag(ProblemModel::ScopeFilter)) { KActionMenu* scopeMenu = new KActionMenu(this); scopeMenu->setDelayed(false); scopeMenu->setToolTip(i18nc("@info:tooltip", "Which files to display the problems for")); scopeMenu->setObjectName(QStringLiteral("scopeMenu")); QActionGroup* scopeActions = new QActionGroup(this); QAction* currentDocumentAction = new QAction(this); currentDocumentAction->setText(i18n("Current Document")); currentDocumentAction->setToolTip(i18nc("@info:tooltip", "Display problems in current document")); QAction* openDocumentsAction = new QAction(this); openDocumentsAction->setText(i18n("Open Documents")); openDocumentsAction->setToolTip(i18nc("@info:tooltip", "Display problems in all open documents")); QAction* currentProjectAction = new QAction(this); currentProjectAction->setText(i18n("Current Project")); currentProjectAction->setToolTip(i18nc("@info:tooltip", "Display problems in current project")); QAction* allProjectAction = new QAction(this); allProjectAction->setText(i18n("All Projects")); allProjectAction->setToolTip(i18nc("@info:tooltip", "Display problems in all projects")); QVector actions; actions.push_back(currentDocumentAction); actions.push_back(openDocumentsAction); actions.push_back(currentProjectAction); actions.push_back(allProjectAction); if (problemModel->features().testFlag(ProblemModel::CanByPassScopeFilter)) { QAction* showAllAction = new QAction(this); showAllAction->setText(i18n("Show All")); showAllAction->setToolTip(i18nc("@info:tooltip", "Display ALL problems")); actions.push_back(showAllAction); } foreach (QAction* action, actions) { action->setCheckable(true); scopeActions->addAction(action); scopeMenu->addAction(action); } addAction(scopeMenu); setScope(CurrentDocument); // Show All should be default if it's supported. It helps with error messages that are otherwise invisible if (problemModel->features().testFlag(ProblemModel::CanByPassScopeFilter)) { actions.last()->setChecked(true); model()->setScope(BypassScopeFilter); } else { currentDocumentAction->setChecked(true); model()->setScope(CurrentDocument); } QSignalMapper* scopeMapper = new QSignalMapper(this); scopeMapper->setMapping(currentDocumentAction, CurrentDocument); scopeMapper->setMapping(openDocumentsAction, OpenDocuments); scopeMapper->setMapping(currentProjectAction, CurrentProject); scopeMapper->setMapping(allProjectAction, AllProjects); connect(currentDocumentAction, &QAction::triggered, scopeMapper, static_cast(&QSignalMapper::map)); connect(openDocumentsAction, &QAction::triggered, scopeMapper, static_cast(&QSignalMapper::map)); connect(currentProjectAction, &QAction::triggered, scopeMapper, static_cast(&QSignalMapper::map)); connect(allProjectAction, &QAction::triggered, scopeMapper, static_cast(&QSignalMapper::map)); if (problemModel->features().testFlag(ProblemModel::CanByPassScopeFilter)) { scopeMapper->setMapping(actions.last(), BypassScopeFilter); connect(actions.last(), &QAction::triggered, scopeMapper, static_cast(&QSignalMapper::map)); } connect(scopeMapper, static_cast(&QSignalMapper::mapped), this, &ProblemTreeView::setScope); } if (problemModel->features().testFlag(ProblemModel::SeverityFilter)) { QActionGroup* severityActions = new QActionGroup(this); - auto errorSeverityAction = new QAction(this); - errorSeverityAction->setToolTip(i18nc("@info:tooltip", "Display only errors")); + + errorSeverityAction = new QAction(this); + errorSeverityAction->setToolTip(i18nc("@info:tooltip", "Display errors")); errorSeverityAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error"))); - auto warningSeverityAction = new QAction(this); - warningSeverityAction->setToolTip(i18nc("@info:tooltip", "Display errors and warnings")); + warningSeverityAction = new QAction(this); + warningSeverityAction->setToolTip(i18nc("@info:tooltip", "Display warnings")); warningSeverityAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); - auto hintSeverityAction = new QAction(this); - hintSeverityAction->setToolTip(i18nc("@info:tooltip", "Display errors, warnings and hints")); + hintSeverityAction = new QAction(this); + hintSeverityAction->setToolTip(i18nc("@info:tooltip", "Display hints")); hintSeverityAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); QAction* severityActionArray[] = { errorSeverityAction, warningSeverityAction, hintSeverityAction }; for (int i = 0; i < 3; ++i) { severityActionArray[i]->setCheckable(true); severityActions->addAction(severityActionArray[i]); addAction(severityActionArray[i]); } + severityActions->setExclusive(false); hintSeverityAction->setChecked(true); - model()->setSeverity(IProblem::Hint); - QSignalMapper* severityMapper = new QSignalMapper(this); - severityMapper->setMapping(errorSeverityAction, IProblem::Error); - severityMapper->setMapping(warningSeverityAction, IProblem::Warning); - severityMapper->setMapping(hintSeverityAction, IProblem::Hint); - connect(errorSeverityAction, &QAction::triggered, severityMapper, - static_cast(&QSignalMapper::map)); - connect(warningSeverityAction, &QAction::triggered, severityMapper, - static_cast(&QSignalMapper::map)); - connect(hintSeverityAction, &QAction::triggered, severityMapper, - static_cast(&QSignalMapper::map)); - connect(severityMapper, static_cast(&QSignalMapper::mapped), model(), - &ProblemModel::setSeverity); + warningSeverityAction->setChecked(true); + errorSeverityAction->setChecked(true); + + model()->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); + connect(errorSeverityAction, &QAction::toggled, this, &ProblemTreeView::handleSeverityActionToggled); + connect(warningSeverityAction, &QAction::toggled, this, &ProblemTreeView::handleSeverityActionToggled); + connect(hintSeverityAction, &QAction::toggled, this, &ProblemTreeView::handleSeverityActionToggled); } if (problemModel->features().testFlag(ProblemModel::Grouping)) { KActionMenu* groupingMenu = new KActionMenu(i18n("Grouping"), this); groupingMenu->setDelayed(false); QActionGroup* groupingActions = new QActionGroup(this); QAction* noGroupingAction = new QAction(i18n("None"), this); QAction* pathGroupingAction = new QAction(i18n("Path"), this); QAction* severityGroupingAction = new QAction(i18n("Severity"), this); QAction* groupingActionArray[] = { noGroupingAction, pathGroupingAction, severityGroupingAction }; for (unsigned i = 0; i < sizeof(groupingActionArray) / sizeof(QAction*); ++i) { QAction* action = groupingActionArray[i]; action->setCheckable(true); groupingActions->addAction(action); groupingMenu->addAction(action); } addAction(groupingMenu); noGroupingAction->setChecked(true); QSignalMapper* groupingMapper = new QSignalMapper(this); groupingMapper->setMapping(noGroupingAction, NoGrouping); groupingMapper->setMapping(pathGroupingAction, PathGrouping); groupingMapper->setMapping(severityGroupingAction, SeverityGrouping); connect(noGroupingAction, &QAction::triggered, groupingMapper, static_cast(&QSignalMapper::map)); connect(pathGroupingAction, &QAction::triggered, groupingMapper, static_cast(&QSignalMapper::map)); connect(severityGroupingAction, &QAction::triggered, groupingMapper, static_cast(&QSignalMapper::map)); connect(groupingMapper, static_cast(&QSignalMapper::mapped), model(), &ProblemModel::setGrouping); } connect(this, &ProblemTreeView::clicked, this, &ProblemTreeView::itemActivated); connect(model(), &QAbstractItemModel::rowsInserted, this, &ProblemTreeView::changed); connect(model(), &QAbstractItemModel::rowsRemoved, this, &ProblemTreeView::changed); connect(model(), &QAbstractItemModel::modelReset, this, &ProblemTreeView::changed); } ProblemTreeView::~ProblemTreeView() { } void ProblemTreeView::openDocumentForCurrentProblem() { itemActivated(currentIndex()); } void ProblemTreeView::itemActivated(const QModelIndex& index) { if (!index.isValid()) return; KTextEditor::Cursor start; QUrl url; { // TODO: is this really necessary? DUChainReadLocker lock(DUChain::lock()); const auto problem = index.data(ProblemModel::ProblemRole).value(); if (!problem) return; url = problem->finalLocation().document.toUrl(); start = problem->finalLocation().start(); } ICore::self()->documentController()->openDocument(url, start); } +void ProblemTreeView::handleSeverityActionToggled() +{ + model()->setSeverities( (errorSeverityAction->isChecked() ? IProblem::Error : IProblem::Severities()) | + (warningSeverityAction->isChecked() ? IProblem::Warning : IProblem::Severities()) | + (hintSeverityAction->isChecked() ? IProblem::Hint : IProblem::Severities()) ); +} + void ProblemTreeView::setScope(int scope) { foreach (auto action, actions()) { if (action->objectName() == QLatin1String("scopeMenu")) { action->setText(i18n("Scope: %1", action->menu()->actions().at(scope)->text())); } } model()->setScope(scope); } void ProblemTreeView::resizeColumns() { for (int i = 0; i < model()->columnCount(); ++i) resizeColumnToContents(i); } void ProblemTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) { QTreeView::dataChanged(topLeft, bottomRight, roles); resizeColumns(); } void ProblemTreeView::reset() { QTreeView::reset(); resizeColumns(); } ProblemModel* ProblemTreeView::model() const { return static_cast(m_proxy->sourceModel()); } void ProblemTreeView::setModel(QAbstractItemModel* model) { Q_ASSERT(qobject_cast(model)); m_proxy->setSourceModel(model); QTreeView::setModel(m_proxy); } void ProblemTreeView::contextMenuEvent(QContextMenuEvent* event) { QModelIndex index = indexAt(event->pos()); if (index.isValid()) { const auto problem = index.data(ProblemModel::ProblemRole).value(); if (!problem) { return; } QExplicitlySharedDataPointer solution = problem->solutionAssistant(); if (!solution) { return; } QList actions; foreach (KDevelop::IAssistantAction::Ptr action, solution->actions()) { actions << action->toKAction(); } if (!actions.isEmpty()) { QString title = solution->title(); title = KDevelop::htmlToPlainText(title); title.replace(QLatin1String("'"), QLatin1String("\'")); QPointer m = new QMenu(this); m->addSection(title); m->addActions(actions); m->exec(event->globalPos()); delete m; } } } void ProblemTreeView::showEvent(QShowEvent* event) { Q_UNUSED(event) resizeColumns(); } #include "problemtreeview.moc" diff --git a/plugins/problemreporter/problemtreeview.h b/plugins/problemreporter/problemtreeview.h index 092ce88e17..7ff80ceb60 100644 --- a/plugins/problemreporter/problemtreeview.h +++ b/plugins/problemreporter/problemtreeview.h @@ -1,76 +1,80 @@ /* * KDevelop Problem Reporter * * Copyright (c) 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. */ #ifndef KDEVPLATFORM_PLUGIN_PROBLEMWIDGET_H #define KDEVPLATFORM_PLUGIN_PROBLEMWIDGET_H #include namespace KDevelop { class TopDUContext; class IDocument; class ProblemModel; } class ProblemReporterPlugin; class QSortFilterProxyModel; class ProblemTreeView : public QTreeView { Q_OBJECT public: ProblemTreeView(QWidget* parent, QAbstractItemModel* itemModel); ~ProblemTreeView() override; KDevelop::ProblemModel* model() const; void setModel(QAbstractItemModel* model) override; void contextMenuEvent(QContextMenuEvent*) override; void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles = QVector()) override; void reset() override; public slots: void openDocumentForCurrentProblem(); signals: // Emitted when the model's rows change (added/removed/reset) void changed(); protected: void showEvent(QShowEvent* event) override; private slots: void itemActivated(const QModelIndex& index); + void handleSeverityActionToggled(); void setScope(int scope); private: void resizeColumns(); ProblemReporterPlugin* m_plugin; QSortFilterProxyModel* m_proxy; + QAction* errorSeverityAction; + QAction* warningSeverityAction; + QAction* hintSeverityAction; }; #endif // kate: space-indent on; indent-width 2; tab-width: 4; replace-tabs on; auto-insert-doxygen on diff --git a/plugins/projectfilter/filter.cpp b/plugins/projectfilter/filter.cpp index 8bd3588e91..331cab8bf4 100644 --- a/plugins/projectfilter/filter.cpp +++ b/plugins/projectfilter/filter.cpp @@ -1,159 +1,162 @@ /* * This file is part of KDevelop * Copyright 2013 Milian Wolff * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "filter.h" #include using namespace KDevelop; Filter::Filter() : targets(Files | Folders) , type(Exclusive) { } Filter::Filter(const SerializedFilter& filter) : pattern(QString(), Qt::CaseSensitive, QRegExp::WildcardUnix) , targets(filter.targets) , type(filter.type) { QString pattern = filter.pattern; if (!filter.pattern.startsWith('/') && !filter.pattern.startsWith('*')) { // implicitly match against trailing relative path pattern.prepend(QLatin1String("*/")); } if (pattern.endsWith('/') && targets != Filter::Files) { // implicitly match against folders targets = Filter::Folders; pattern.chop(1); } this->pattern.setPattern(pattern); } SerializedFilter::SerializedFilter() : targets(Filter::Files | Filter::Folders) , type(Filter::Exclusive) { } SerializedFilter::SerializedFilter(const QString& pattern, Filter::Targets targets, Filter::Type type) : pattern(pattern) , targets(targets) , type(type) { } namespace KDevelop { SerializedFilters defaultFilters() { SerializedFilters ret; // filter hidden files ret << SerializedFilter(QStringLiteral(".*"), Filter::Targets(Filter::Files | Filter::Folders)); + // but do show some with special meaning + ret << SerializedFilter(QStringLiteral(".gitignore"), Filter::Files, Filter::Inclusive) + << SerializedFilter(QStringLiteral(".gitmodules"), Filter::Files, Filter::Inclusive); // common vcs folders which we want to hide static const QVector invalidFolders = QVector() << QStringLiteral(".git") << QStringLiteral("CVS") << QStringLiteral(".svn") << QStringLiteral("_svn") << QStringLiteral("SCCS") << QStringLiteral("_darcs") << QStringLiteral(".hg") << QStringLiteral(".bzr") << QStringLiteral("__pycache__"); foreach(const QString& folder, invalidFolders) { ret << SerializedFilter(folder, Filter::Folders); } // common files which we want to hide static const QVector filePatterns = QVector() // binary files << QStringLiteral("*.o") << QStringLiteral("*.a") << QStringLiteral("*.so") << QStringLiteral("*.so.*") // generated files << QStringLiteral("moc_*.cpp") << QStringLiteral("*.moc") << QStringLiteral("ui_*.h") << QStringLiteral("qrc_*.cpp") // backup files << QStringLiteral("*~") << QStringLiteral("*.orig") << QStringLiteral(".*.kate-swp") << QStringLiteral(".*.swp") // python cache and object files << QStringLiteral("*.pyc") << QStringLiteral("*.pyo"); foreach(const QString& filePattern, filePatterns) { ret << SerializedFilter(filePattern, Filter::Files); } return ret; } SerializedFilters readFilters(const KSharedConfigPtr& config) { if (!config->hasGroup("Filters")) { return defaultFilters(); } const KConfigGroup& group = config->group("Filters"); const int size = group.readEntry("size", -1); if (size == -1) { // fallback return defaultFilters(); } SerializedFilters filters; filters.reserve(size); for (int i = 0; i < size; ++i) { const QByteArray subGroup = QByteArray::number(i); if (!group.hasGroup(subGroup)) { continue; } const KConfigGroup& subConfig = group.group(subGroup); const QString pattern = subConfig.readEntry("pattern", QString()); Filter::Targets targets(subConfig.readEntry("targets", 0)); Filter::Type type = static_cast(subConfig.readEntry("inclusive", 0)); filters << SerializedFilter(pattern, targets, type); } return filters; } void writeFilters(const SerializedFilters& filters, KSharedConfigPtr config) { // clear existing config->deleteGroup("Filters"); // write new KConfigGroup group = config->group("Filters"); group.writeEntry("size", filters.size()); int i = 0; foreach(const SerializedFilter& filter, filters) { KConfigGroup subGroup = group.group(QByteArray::number(i++)); subGroup.writeEntry("pattern", filter.pattern); subGroup.writeEntry("targets", static_cast(filter.targets)); subGroup.writeEntry("inclusive", static_cast(filter.type)); } config->sync(); } Filters deserialize(const SerializedFilters& filters) { Filters ret; ret.reserve(filters.size()); foreach(const SerializedFilter& filter, filters) { ret << Filter(filter); } return ret; } } diff --git a/plugins/projectfilter/tests/test_projectfilter.cpp b/plugins/projectfilter/tests/test_projectfilter.cpp index 669830db81..6accdce0ad 100644 --- a/plugins/projectfilter/tests/test_projectfilter.cpp +++ b/plugins/projectfilter/tests/test_projectfilter.cpp @@ -1,396 +1,398 @@ /* * This file is part of KDevelop * Copyright 2013 Milian Wolff * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_projectfilter.h" #include #include #include #include #include #include "../projectfilter.h" QTEST_GUILESS_MAIN(TestProjectFilter); using namespace KDevelop; typedef QSharedPointer TestFilter; Q_DECLARE_METATYPE(TestFilter) namespace { const bool Invalid = false; const bool Valid = true; const bool Folder = true; const bool File = false; struct MatchTest { QString path; bool isFolder; bool shouldMatch; }; void addTests(const QString& tag, const TestProject& project, const TestFilter& filter, MatchTest* tests, uint numTests) { for (uint i = 0; i < numTests; ++i) { const MatchTest& test = tests[i]; QTest::newRow(qstrdup(qPrintable(tag + ':' + test.path))) << filter << Path(project.path(), test.path) << test.isFolder << test.shouldMatch; if (test.isFolder) { // also test folder with trailing slash - should not make a difference QTest::newRow(qstrdup(qPrintable(tag + ':' + test.path + '/'))) << filter << Path(project.path(), test.path) << test.isFolder << test.shouldMatch; } } } ///FIXME: remove once we can use c++11 #define ADD_TESTS(tag, project, filter, tests) addTests(QStringLiteral(tag), project, filter, tests, sizeof(tests) / sizeof(tests[0])) struct BenchData { BenchData(const Path &path = Path(), bool isFolder = false) : path(path) , isFolder(isFolder) {} Path path; bool isFolder; }; } Q_DECLARE_METATYPE(QVector) void TestProjectFilter::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType >(); } void TestProjectFilter::cleanupTestCase() { TestCore::shutdown(); } void TestProjectFilter::match() { QFETCH(TestFilter, filter); QFETCH(KDevelop::Path, path); QFETCH(bool, isFolder); QFETCH(bool, expectedIsValid); QCOMPARE(filter->isValid(path, isFolder), expectedIsValid); } void TestProjectFilter::match_data() { QTest::addColumn("filter"); QTest::addColumn("path"); QTest::addColumn("isFolder"); QTest::addColumn("expectedIsValid"); { // test default filters const TestProject project; TestFilter filter(new ProjectFilter(&project, deserialize(defaultFilters()))); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("folder"), Folder, Valid}, {QStringLiteral("folder/folder"), Folder, Valid}, {QStringLiteral("file"), File, Valid}, {QStringLiteral("folder/file"), File, Valid}, {QStringLiteral(".file"), File, Invalid}, {QStringLiteral(".folder"), Folder, Invalid}, {QStringLiteral("folder/.folder"), Folder, Invalid}, {QStringLiteral("folder/.file"), File, Invalid}, {QStringLiteral(".git"), Folder, Invalid}, + {QStringLiteral(".gitignore"), File, Valid}, + {QStringLiteral(".gitmodules"), File, Valid}, {QStringLiteral("_darcs"), Folder, Invalid}, {QStringLiteral("_svn"), Folder, Invalid}, {QStringLiteral(".svn"), Folder, Invalid}, {QStringLiteral("CVS"), Folder, Invalid}, {QStringLiteral("SCCS"), Folder, Invalid}, {QStringLiteral(".hg"), Folder, Invalid}, {QStringLiteral(".bzr"), Folder, Invalid}, {QStringLiteral("foo.o"), File, Invalid}, {QStringLiteral("foo.so"), File, Invalid}, {QStringLiteral("foo.so.1"), File, Invalid}, {QStringLiteral("foo.a"), File, Invalid}, {QStringLiteral("moc_foo.cpp"), File, Invalid}, {QStringLiteral("ui_foo.h"), File, Invalid}, {QStringLiteral("qrc_foo.cpp"), File, Invalid}, {QStringLiteral("foo.cpp~"), File, Invalid}, {QStringLiteral(".foo.cpp.kate-swp"), File, Invalid}, {QStringLiteral(".foo.cpp.swp"), File, Invalid} }; ADD_TESTS("default", project, filter, tests); } { // test exclude files, basename const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("*.cpp"), Filter::Files)); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("folder"), Folder, Valid}, {QStringLiteral("file"), File, Valid}, {QStringLiteral("file.cpp"), File, Invalid}, {QStringLiteral("folder.cpp"), Folder, Valid}, {QStringLiteral("folder/file.cpp"), File, Invalid}, {QStringLiteral("folder/folder.cpp"), Folder, Valid} }; ADD_TESTS("exclude:*.cpp", project, filter, tests); } { // test excludes on folders const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("foo"), Filter::Folders)); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("folder"), Folder, Valid}, {QStringLiteral("file"), File, Valid}, {QStringLiteral("foo"), Folder, Invalid}, {QStringLiteral("folder/file"), File, Valid}, {QStringLiteral("folder/foo"), Folder, Invalid}, {QStringLiteral("folder/foo"), File, Valid} }; ADD_TESTS("exclude:foo", project, filter, tests); } { // test includes const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("*"), Filter::Files)) << Filter(SerializedFilter(QStringLiteral("*.cpp"), Filter::Files, Filter::Inclusive)); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("folder"), Folder, Valid}, {QStringLiteral("file"), File, Invalid}, {QStringLiteral("file.cpp"), File, Valid}, {QStringLiteral(".file.cpp"), File, Valid}, {QStringLiteral("folder/file.cpp"), File, Valid}, {QStringLiteral("folder/.file.cpp"), File, Valid} }; ADD_TESTS("include:*.cpp", project, filter, tests); project.projectConfiguration(); } { // test mixed stuff const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("*"), Filter::Files, Filter::Exclusive)) << Filter(SerializedFilter(QStringLiteral("*.inc"), Filter::Files, Filter::Inclusive)) << Filter(SerializedFilter(QStringLiteral("*ex.inc"), Filter::Files, Filter::Exclusive)) << Filter(SerializedFilter(QStringLiteral("bar"), Filter::Folders, Filter::Exclusive)); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("folder"), Folder, Valid}, {QStringLiteral("file"), File, Invalid}, {QStringLiteral("file.inc"), File, Valid}, {QStringLiteral("file.ex.inc"), File, Invalid}, {QStringLiteral("folder/file"), File, Invalid}, {QStringLiteral("folder/file.inc"), File, Valid}, {QStringLiteral("folder/file.ex.inc"), File, Invalid}, {QStringLiteral("bar"), Folder, Invalid}, }; ADD_TESTS("mixed", project, filter, tests); } { // relative path const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("/foo/*bar"), Filter::Targets(Filter::Files | Filter::Folders))); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("foo"), Folder, Valid}, {QStringLiteral("bar"), File, Valid}, {QStringLiteral("foo/bar"), Folder, Invalid}, {QStringLiteral("foo/bar"), File, Invalid}, {QStringLiteral("foo/asdf/bar"), Folder, Invalid}, {QStringLiteral("foo/asdf/bar"), File, Invalid}, {QStringLiteral("foo/asdf_bar"), Folder, Invalid}, {QStringLiteral("foo/asdf_bar"), File, Invalid}, {QStringLiteral("asdf/bar"), File, Valid}, {QStringLiteral("asdf/foo/bar"), File, Valid}, }; ADD_TESTS("relative", project, filter, tests); } { // trailing slash const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("bar/"), Filter::Targets(Filter::Files | Filter::Folders))); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("foo"), Folder, Valid}, {QStringLiteral("bar"), File, Valid}, {QStringLiteral("bar"), Folder, Invalid}, {QStringLiteral("foo/bar"), File, Valid}, {QStringLiteral("foo/bar"), Folder, Invalid} }; ADD_TESTS("trailingslash", project, filter, tests); } { // escaping const TestProject project; const Filters filters = Filters() << Filter(SerializedFilter(QStringLiteral("foo\\*bar"), Filter::Files)); TestFilter filter(new ProjectFilter(&project, filters)); QTest::newRow("projectRoot") << filter << project.path() << Folder << Valid; QTest::newRow("project.kdev4") << filter << project.projectFile() << File << Invalid; MatchTest tests[] = { //{path, isFolder, isValid} {QStringLiteral(".kdev4"), Folder, Invalid}, {QStringLiteral("foobar"), Folder, Valid}, {QStringLiteral("fooasdfbar"), File, Valid}, {QStringLiteral("foo*bar"), File, Invalid}, {QStringLiteral("foo/bar"), Folder, Valid} }; ADD_TESTS("escaping", project, filter, tests); } } static QVector createBenchData(const Path& base, int folderDepth, int foldersPerFolder, int filesPerFolder) { QVector data; data << BenchData(base, true); for(int i = 0; i < filesPerFolder; ++i) { if (i % 2) { data << BenchData(Path(base, QStringLiteral("file%1.cpp").arg(i)), false); } else { data << BenchData(Path(base, QStringLiteral("file%1.h").arg(i)), true); } } for(int i = 0; i < foldersPerFolder && folderDepth > 0; ++i) { data += createBenchData(Path(base, QStringLiteral("folder%1").arg(i)), folderDepth - 1, foldersPerFolder, filesPerFolder); } return data; } void TestProjectFilter::bench() { QFETCH(TestFilter, filter); QFETCH(QVector, data); QBENCHMARK { foreach(const BenchData& bench, data) { filter->isValid(bench.path, bench.isFolder); } } } void TestProjectFilter::bench_data() { QTest::addColumn("filter"); QTest::addColumn >("data"); const TestProject project; QVector > dataSets = QVector >() << createBenchData(project.path(), 3, 5, 10) << createBenchData(project.path(), 3, 5, 20) << createBenchData(project.path(), 4, 5, 10) << createBenchData(project.path(), 3, 10, 10); { TestFilter filter(new ProjectFilter(&project, Filters())); foreach(const QVector& data, dataSets) { QTest::newRow(qstrdup(QByteArray("baseline-") + QByteArray::number(data.size()))) << filter << data; } } { TestFilter filter(new ProjectFilter(&project, deserialize(defaultFilters()))); foreach(const QVector& data, dataSets) { QTest::newRow(qstrdup(QByteArray("defaults-") + QByteArray::number(data.size()))) << filter << data; } } } diff --git a/project/abstractfilemanagerplugin.cpp b/project/abstractfilemanagerplugin.cpp index a2072a70a7..52f65bea7a 100644 --- a/project/abstractfilemanagerplugin.cpp +++ b/project/abstractfilemanagerplugin.cpp @@ -1,652 +1,659 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2010-2012 Milian Wolff * * * * 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 "abstractfilemanagerplugin.h" #include "filemanagerlistjob.h" #include "projectmodel.h" #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include "projectfiltermanager.h" #include "debug.h" #define ifDebug(x) using namespace KDevelop; //BEGIN Helper namespace { /** * Returns the parent folder item for a given item or the project root item if there is no parent. */ ProjectFolderItem* getParentFolder(ProjectBaseItem* item) { if ( item->parent() ) { return static_cast(item->parent()); } else { return item->project()->projectItem(); } } } //END Helper //BEGIN Private struct AbstractFileManagerPlugin::Private { explicit Private(AbstractFileManagerPlugin* qq) : q(qq) { } AbstractFileManagerPlugin* q; - KIO::Job* eventuallyReadFolder( ProjectFolderItem* item ); + /** + * The just returned must be started in one way or another for this method + * to have any affect. The job will then auto-delete itself upon completion. + */ + KIO::Job* eventuallyReadFolder( ProjectFolderItem* item ) Q_REQUIRED_RESULT; void addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries); void deleted(const QString &path); void created(const QString &path); void projectClosing(IProject* project); void jobFinished(KJob* job); /// Stops watching the given folder for changes, only useful for local files. void stopWatcher(ProjectFolderItem* folder); /// Continues watching the given folder for changes. void continueWatcher(ProjectFolderItem* folder); /// Common renaming function. bool rename(ProjectBaseItem* item, const Path& newPath); void removeFolder(ProjectFolderItem* folder); QHash m_watchers; QHash > m_projectJobs; QVector m_stoppedFolders; ProjectFilterManager m_filters; }; void AbstractFileManagerPlugin::Private::projectClosing(IProject* project) { if ( m_projectJobs.contains(project) ) { // make sure the import job does not live longer than the project // see also addLotsOfFiles test foreach( FileManagerListJob* job, m_projectJobs[project] ) { qCDebug(FILEMANAGER) << "killing project job:" << job; job->abort(); } m_projectJobs.remove(project); } delete m_watchers.take(project); m_filters.remove(project); } KIO::Job* AbstractFileManagerPlugin::Private::eventuallyReadFolder( ProjectFolderItem* item ) { FileManagerListJob* listJob = new FileManagerListJob( item ); m_projectJobs[ item->project() ] << listJob; qCDebug(FILEMANAGER) << "adding job" << listJob << item << item->path() << "for project" << item->project(); q->connect( listJob, &FileManagerListJob::finished, q, [&] (KJob* job) { jobFinished(job); } ); q->connect( listJob, &FileManagerListJob::entries, q, [&] (FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { addJobItems(job, baseItem, entries); } ); return listJob; } void AbstractFileManagerPlugin::Private::jobFinished(KJob* job) { FileManagerListJob* gmlJob = qobject_cast(job); if (gmlJob) { ifDebug(qCDebug(FILEMANAGER) << job << gmlJob << gmlJob->item();) m_projectJobs[ gmlJob->item()->project() ].removeOne( gmlJob ); } else { // job emitted its finished signal from its destructor // ensure we don't keep a dangling point in our list foreach (auto jobs, m_projectJobs) { if (jobs.removeOne(reinterpret_cast(job))) { break; } } } } void AbstractFileManagerPlugin::Private::addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { if ( entries.empty() ) { return; } qCDebug(FILEMANAGER) << "reading entries of" << baseItem->path(); // build lists of valid files and folders with paths relative to the project folder Path::List files; Path::List folders; foreach ( const KIO::UDSEntry& entry, entries ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if (name == QLatin1String(".") || name == QLatin1String("..")) { continue; } Path path(baseItem->path(), name); if ( !q->isValid( path, entry.isDir(), baseItem->project() ) ) { continue; } else { if ( entry.isDir() ) { if( entry.isLink() ) { const Path linkedPath = baseItem->path().cd(entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST )); // make sure we don't end in an infinite loop if( linkedPath.isParentOf( baseItem->project()->path() ) || baseItem->project()->path().isParentOf( linkedPath ) || linkedPath == baseItem->project()->path() ) { continue; } } folders << path; } else { files << path; } } } ifDebug(qCDebug(FILEMANAGER) << "valid folders:" << folders;) ifDebug(qCDebug(FILEMANAGER) << "valid files:" << files;) // remove obsolete rows for ( int j = 0; j < baseItem->rowCount(); ++j ) { if ( ProjectFolderItem* f = baseItem->child(j)->folder() ) { // check if this is still a valid folder int index = folders.indexOf( f->path() ); if ( index == -1 ) { // folder got removed or is now invalid removeFolder(f); --j; } else { // this folder already exists in the view folders.remove( index ); // no need to add this item, but we still want to recurse into it job->addSubDir( f ); emit q->reloadedFolderItem( f ); } } else if ( ProjectFileItem* f = baseItem->child(j)->file() ) { // check if this is still a valid file int index = files.indexOf( f->path() ); if ( index == -1 ) { // file got removed or is now invalid ifDebug(qCDebug(FILEMANAGER) << "removing file:" << f << f->path();) baseItem->removeRow( j ); --j; } else { // this file already exists in the view files.remove( index ); emit q->reloadedFileItem( f ); } } } // add new rows foreach ( const Path& path, files ) { ProjectFileItem* file = q->createFileItem( baseItem->project(), path, baseItem ); if (file) { emit q->fileAdded( file ); } } foreach ( const Path& path, folders ) { ProjectFolderItem* folder = q->createFolderItem( baseItem->project(), path, baseItem ); if (folder) { emit q->folderAdded( folder ); job->addSubDir( folder ); } } } void AbstractFileManagerPlugin::Private::created(const QString &path_) { qCDebug(FILEMANAGER) << "created:" << path_; QFileInfo info(path_); ///FIXME: share memory with parent const Path path(path_); const IndexedString indexedPath(path.pathOrUrl()); const IndexedString indexedParent(path.parent().pathOrUrl()); foreach ( IProject* p, m_watchers.keys() ) { if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } if ( !q->isValid(path, info.isDir(), p) ) { continue; } if ( info.isDir() ) { bool found = false; foreach ( ProjectFolderItem* folder, p->foldersForPath(indexedPath) ) { // exists already in this project, happens e.g. when we restart the dirwatcher // or if we delete and remove folders consecutively https://bugs.kde.org/show_bug.cgi?id=260741 qCDebug(FILEMANAGER) << "force reload of" << path << folder; - eventuallyReadFolder( folder ); + auto job = eventuallyReadFolder( folder ); + job->start(); found = true; } if ( found ) { continue; } } else if (!p->filesForPath(indexedPath).isEmpty()) { // also gets triggered for kate's backup files continue; } foreach ( ProjectFolderItem* parentItem, p->foldersForPath(indexedParent) ) { if ( info.isDir() ) { ProjectFolderItem* folder = q->createFolderItem( p, path, parentItem ); if (folder) { emit q->folderAdded( folder ); - eventuallyReadFolder( folder ); + auto job = eventuallyReadFolder( folder ); + job->start(); } } else { ProjectFileItem* file = q->createFileItem( p, path, parentItem ); if (file) { emit q->fileAdded( file ); } } } } } void AbstractFileManagerPlugin::Private::deleted(const QString &path_) { if ( QFile::exists(path_) ) { // stopDirScan... return; } // ensure that the path is not inside a stopped folder foreach(const QString& folder, m_stoppedFolders) { if (path_.startsWith(folder)) { return; } } qCDebug(FILEMANAGER) << "deleted:" << path_; const Path path(path_); const IndexedString indexed(path.pathOrUrl()); foreach ( IProject* p, m_watchers.keys() ) { if (path == p->path()) { KMessageBox::error(qApp->activeWindow(), i18n("The base folder of project %1" " got deleted or moved outside of KDevelop.\n" "The project has to be closed.", p->name()), i18n("Project Folder Deleted") ); ICore::self()->projectController()->closeProject(p); continue; } if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } foreach ( ProjectFolderItem* item, p->foldersForPath(indexed) ) { removeFolder(item); } foreach ( ProjectFileItem* item, p->filesForPath(indexed) ) { emit q->fileRemoved(item); ifDebug(qCDebug(FILEMANAGER) << "removing file" << item;) item->parent()->removeRow(item->row()); } } } bool AbstractFileManagerPlugin::Private::rename(ProjectBaseItem* item, const Path& newPath) { if ( !q->isValid(newPath, true, item->project()) ) { int cancel = KMessageBox::warningContinueCancel( qApp->activeWindow(), i18n("You tried to rename '%1' to '%2', but the latter is filtered and will be hidden.\n" "Do you want to continue?", item->text(), newPath.lastPathSegment()), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("GenericManagerRenameToFiltered") ); if ( cancel == KMessageBox::Cancel ) { return false; } } foreach ( ProjectFolderItem* parent, item->project()->foldersForPath(IndexedString(newPath.parent().pathOrUrl())) ) { if ( parent->folder() ) { stopWatcher(parent); const Path source = item->path(); bool success = renameUrl( item->project(), source.toUrl(), newPath.toUrl() ); if ( success ) { item->setPath( newPath ); item->parent()->takeRow( item->row() ); parent->appendRow( item ); if (item->file()) { emit q->fileRenamed(source, item->file()); } else { Q_ASSERT(item->folder()); emit q->folderRenamed(source, item->folder()); } } continueWatcher(parent); return success; } } return false; } void AbstractFileManagerPlugin::Private::stopWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->stopDirScan(path); m_stoppedFolders.append(path); } void AbstractFileManagerPlugin::Private::continueWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->restartDirScan(path); const int idx = m_stoppedFolders.indexOf(path); if (idx != -1) { m_stoppedFolders.remove(idx); } } bool isChildItem(ProjectBaseItem* parent, ProjectBaseItem* child) { do { if (child == parent) { return true; } child = child->parent(); } while(child); return false; } void AbstractFileManagerPlugin::Private::removeFolder(ProjectFolderItem* folder) { ifDebug(qCDebug(FILEMANAGER) << "removing folder:" << folder << folder->path();) foreach(FileManagerListJob* job, m_projectJobs[folder->project()]) { if (isChildItem(folder, job->item())) { qCDebug(FILEMANAGER) << "killing list job for removed folder" << job << folder->path(); job->abort(); Q_ASSERT(!m_projectJobs.value(folder->project()).contains(job)); } else { job->removeSubDir(folder); } } folder->parent()->removeRow( folder->row() ); } //END Private //BEGIN Plugin AbstractFileManagerPlugin::AbstractFileManagerPlugin( const QString& componentName, QObject *parent, const QVariantList & /*args*/ ) : IProjectFileManager(), IPlugin( componentName, parent ), d(new Private(this)) { KDEV_USE_EXTENSION_INTERFACE( IProjectFileManager ) connect(core()->projectController(), &IProjectController::projectClosing, this, [&] (IProject* project) { d->projectClosing(project); }); } AbstractFileManagerPlugin::~AbstractFileManagerPlugin() { delete d; } IProjectFileManager::Features AbstractFileManagerPlugin::features() const { return Features( Folders | Files ); } QList AbstractFileManagerPlugin::parse( ProjectFolderItem *item ) { // we are async, can't return anything here qCDebug(FILEMANAGER) << "note: parse will always return an empty list"; Q_UNUSED(item); return QList(); } ProjectFolderItem *AbstractFileManagerPlugin::import( IProject *project ) { ProjectFolderItem *projectRoot = createFolderItem( project, project->path(), 0 ); emit folderAdded( projectRoot ); qCDebug(FILEMANAGER) << "imported new project" << project->name() << "at" << projectRoot->path(); ///TODO: check if this works for remote files when something gets changed through another KDE app if ( project->path().isLocalFile() ) { d->m_watchers[project] = new KDirWatch( project ); connect(d->m_watchers[project], &KDirWatch::created, this, [&] (const QString& path_) { d->created(path_); }); connect(d->m_watchers[project], &KDirWatch::deleted, this, [&] (const QString& path_) { d->deleted(path_); }); d->m_watchers[project]->addDir(project->path().toLocalFile(), KDirWatch::WatchSubDirs | KDirWatch:: WatchFiles ); } d->m_filters.add(project); return projectRoot; } KJob* AbstractFileManagerPlugin::createImportJob(ProjectFolderItem* item) { return d->eventuallyReadFolder(item); } bool AbstractFileManagerPlugin::reload( ProjectFolderItem* item ) { qCDebug(FILEMANAGER) << "reloading item" << item->path(); - d->eventuallyReadFolder( item->folder() ); + auto job = d->eventuallyReadFolder( item->folder() ); + job->start(); return true; } ProjectFolderItem* AbstractFileManagerPlugin::addFolder( const Path& folder, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding folder" << folder << "to" << parent->path(); ProjectFolderItem* created = 0; d->stopWatcher(parent); if ( createFolder(folder.toUrl()) ) { created = createFolderItem( parent->project(), folder, parent ); if (created) { emit folderAdded(created); } } d->continueWatcher(parent); return created; } ProjectFileItem* AbstractFileManagerPlugin::addFile( const Path& file, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding file" << file << "to" << parent->path(); ProjectFileItem* created = 0; d->stopWatcher(parent); if ( createFile(file.toUrl()) ) { created = createFileItem( parent->project(), file, parent ); if (created) { emit fileAdded(created); } } d->continueWatcher(parent); return created; } bool AbstractFileManagerPlugin::renameFolder(ProjectFolderItem* folder, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a folder:" << folder->path() << newPath; return d->rename(folder, newPath); } bool AbstractFileManagerPlugin::renameFile(ProjectFileItem* file, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a file:" << file->path() << newPath; return d->rename(file, newPath); } bool AbstractFileManagerPlugin::removeFilesAndFolders(const QList &items) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* parent = getParentFolder(item); d->stopWatcher(parent); success &= removeUrl(parent->project(), item->path().toUrl(), true); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { Q_ASSERT(item->folder()); emit folderRemoved(item->folder()); } item->parent()->removeRow( item->row() ); } d->continueWatcher(parent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::moveFilesAndFolders(const QList< ProjectBaseItem* >& items, ProjectFolderItem* newParent) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* oldParent = getParentFolder(item); d->stopWatcher(oldParent); d->stopWatcher(newParent); const Path oldPath = item->path(); const Path newPath(newParent->path(), item->baseName()); success &= renameUrl(oldParent->project(), oldPath.toUrl(), newPath. toUrl()); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { emit folderRemoved(item->folder()); } oldParent->removeRow( item->row() ); KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(oldParent); d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::copyFilesAndFolders(const Path::List& items, ProjectFolderItem* newParent) { bool success = true; foreach(const Path& item, items) { d->stopWatcher(newParent); success &= copyUrl(newParent->project(), item.toUrl(), newParent->path().toUrl()); if ( success ) { KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::isValid( const Path& path, const bool isFolder, IProject* project ) const { return d->m_filters.isValid( path, isFolder, project ); } ProjectFileItem* AbstractFileManagerPlugin::createFileItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFileItem( project, path, parent ); } ProjectFolderItem* AbstractFileManagerPlugin::createFolderItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFolderItem( project, path, parent ); } KDirWatch* AbstractFileManagerPlugin::projectWatcher( IProject* project ) const { return d->m_watchers.value( project, 0 ); } //END Plugin #include "moc_abstractfilemanagerplugin.cpp" diff --git a/shell/filteredproblemstore.cpp b/shell/filteredproblemstore.cpp index 73e92e73af..cf5cbde2f7 100644 --- a/shell/filteredproblemstore.cpp +++ b/shell/filteredproblemstore.cpp @@ -1,327 +1,335 @@ /* * 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. */ #include "filteredproblemstore.h" #include "problem.h" #include "watcheddocumentset.h" #include "problemstorenode.h" #include using namespace KDevelop; namespace { /// Adds diagnostics as sub-nodes void addDiagnostics(ProblemStoreNode *node, const QVector &diagnostics) { foreach (const IProblem::Ptr &ptr, diagnostics) { ProblemNode *child = new ProblemNode(node, ptr); node->addChild(child); addDiagnostics(child, ptr->diagnostics()); } } /** * @brief Base class for grouping strategy classes * * These classes build the problem tree based on the respective strategies */ class GroupingStrategy { public: GroupingStrategy( ProblemStoreNode *root ) : m_rootNode(root) , m_groupedRootNode(new ProblemStoreNode()) { } virtual ~GroupingStrategy(){ } /// Add a problem to the appropriate group virtual void addProblem(const IProblem::Ptr &problem) = 0; /// Find the specified noe const ProblemStoreNode* findNode(int row, ProblemStoreNode *parent = nullptr) const { if (parent == nullptr) return m_groupedRootNode->child(row); else return parent->child(row); } /// Returns the number of children nodes int count(ProblemStoreNode *parent = nullptr) { if (parent == nullptr) return m_groupedRootNode->count(); else return parent->count(); } /// Clears the problems virtual void clear() { m_groupedRootNode->clear(); } protected: ProblemStoreNode *m_rootNode; QScopedPointer m_groupedRootNode; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Implements no grouping strategy, that is just stores the problems without any grouping class NoGroupingStrategy final : public GroupingStrategy { public: NoGroupingStrategy(ProblemStoreNode *root) : GroupingStrategy(root) { } void addProblem(const IProblem::Ptr &problem) override { ProblemNode *node = new ProblemNode(m_groupedRootNode.data(), problem); addDiagnostics(node, problem->diagnostics()); m_groupedRootNode->addChild(node); } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Implements grouping based on path class PathGroupingStrategy final : public GroupingStrategy { public: PathGroupingStrategy(ProblemStoreNode *root) : GroupingStrategy(root) { } void addProblem(const IProblem::Ptr &problem) override { QString path = problem->finalLocation().document.str(); /// See if we already have this path ProblemStoreNode *parent = nullptr; foreach (ProblemStoreNode *node, m_groupedRootNode->children()) { if (node->label() == path) { parent = node; break; } } /// If not add it! if (parent == nullptr) { parent = new LabelNode(m_groupedRootNode.data(), path); m_groupedRootNode->addChild(parent); } ProblemNode *node = new ProblemNode(parent, problem); addDiagnostics(node, problem->diagnostics()); parent->addChild(node); } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Implements grouping based on severity class SeverityGroupingStrategy final : public GroupingStrategy { public: enum SeverityGroups { GroupError = 0, GroupWarning = 1, GroupHint = 2 }; SeverityGroupingStrategy(ProblemStoreNode *root) : GroupingStrategy(root) { /// Create the groups on construction, so there's no need to search for them on addition m_groupedRootNode->addChild(new LabelNode(m_groupedRootNode.data(), i18n("Error"))); m_groupedRootNode->addChild(new LabelNode(m_groupedRootNode.data(), i18n("Warning"))); m_groupedRootNode->addChild(new LabelNode(m_groupedRootNode.data(), i18n("Hint"))); } void addProblem(const IProblem::Ptr &problem) override { ProblemStoreNode *parent = nullptr; switch (problem->severity()) { case IProblem::Error: parent = m_groupedRootNode->child(GroupError); break; case IProblem::Warning: parent = m_groupedRootNode->child(GroupWarning); break; case IProblem::Hint: parent = m_groupedRootNode->child(GroupHint); break; } ProblemNode *node = new ProblemNode(m_groupedRootNode.data(), problem); addDiagnostics(node, problem->diagnostics()); parent->addChild(node); } void clear() override { m_groupedRootNode->child(GroupError)->clear(); m_groupedRootNode->child(GroupWarning)->clear(); m_groupedRootNode->child(GroupHint)->clear(); } }; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// namespace KDevelop { struct FilteredProblemStorePrivate { FilteredProblemStorePrivate(FilteredProblemStore* q) : q(q) , m_strategy(new NoGroupingStrategy(q->rootNode())) , m_grouping(NoGrouping) , m_bypassScopeFilter(false) { } /// Tells if the problem matches the filters bool match(const IProblem::Ptr &problem) const; FilteredProblemStore* q; QScopedPointer m_strategy; GroupingMethod m_grouping; bool m_bypassScopeFilter; }; FilteredProblemStore::FilteredProblemStore(QObject *parent) : ProblemStore(parent) , d(new FilteredProblemStorePrivate(this)) { } FilteredProblemStore::~FilteredProblemStore() { } void FilteredProblemStore::addProblem(const IProblem::Ptr &problem) { ProblemStore::addProblem(problem); if (d->match(problem)) d->m_strategy->addProblem(problem); } const ProblemStoreNode* FilteredProblemStore::findNode(int row, ProblemStoreNode *parent) const { return d->m_strategy->findNode(row, parent); } int FilteredProblemStore::count(ProblemStoreNode *parent) const { return d->m_strategy->count(parent); } void FilteredProblemStore::clear() { d->m_strategy->clear(); ProblemStore::clear(); } void FilteredProblemStore::rebuild() { emit beginRebuild(); d->m_strategy->clear(); foreach (ProblemStoreNode *node, rootNode()->children()) { IProblem::Ptr problem = node->problem(); if (d->match(problem)) { d->m_strategy->addProblem(problem); } } emit endRebuild(); } void FilteredProblemStore::setGrouping(int grouping) { GroupingMethod g = GroupingMethod(grouping); if(g == d->m_grouping) return; d->m_grouping = g; switch (g) { case NoGrouping: d->m_strategy.reset(new NoGroupingStrategy(rootNode())); break; case PathGrouping: d->m_strategy.reset(new PathGroupingStrategy(rootNode())); break; case SeverityGrouping: d->m_strategy.reset(new SeverityGroupingStrategy(rootNode())); break; } rebuild(); emit changed(); } int FilteredProblemStore::grouping() const { return d->m_grouping; } void FilteredProblemStore::setBypassScopeFilter(bool bypass) { if (d->m_bypassScopeFilter != bypass) { d->m_bypassScopeFilter = bypass; rebuild(); emit changed(); } } bool FilteredProblemStore::bypassScopeFilter() const { return d->m_bypassScopeFilter; } bool FilteredProblemStorePrivate::match(const IProblem::Ptr &problem) const { - /// If the problem is less severe than our filter criterion then it's discarded - if(problem->severity() > q->severity()) - return false; + if(problem->severity()!=IProblem::NoSeverity) + { + /// If the problem severity isn't in the filter severities it's discarded + if(!q->severities().testFlag(problem->severity())) + return false; + } + else + { + if(!q->severities().testFlag(IProblem::Hint))//workaround for problems wothout correctly set severity + return false; + } /// If we have bypass on, don't check the scope if (!m_bypassScopeFilter) { /// If the problem isn't in a file that's in the watched document set, it's discarded const WatchedDocumentSet::DocumentSet &docs = q->documents()->get(); if(!docs.contains(problem->finalLocation().document)) return false; } return true; } } diff --git a/shell/loadedpluginsdialog.cpp b/shell/loadedpluginsdialog.cpp index 6293ffcc2f..2ba46129f0 100644 --- a/shell/loadedpluginsdialog.cpp +++ b/shell/loadedpluginsdialog.cpp @@ -1,313 +1,310 @@ /************************************************************************** * Copyright 2009 Andreas Pakulat * * Copyright 2010 Niko Sams * * * * 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 "loadedpluginsdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "plugincontroller.h" #define MARGIN 5 namespace { KPluginMetaData pluginInfo(KDevelop::IPlugin* plugin) { return KDevelop::Core::self()->pluginControllerInternal()->pluginInfo(plugin); }; QString displayName(KDevelop::IPlugin* plugin) { const auto name = pluginInfo(plugin).name(); return !name.isEmpty() ? name : plugin->componentName(); } bool sortPlugins(KDevelop::IPlugin* l, KDevelop::IPlugin* r) { return displayName(l) < displayName(r); } } class PluginsModel : public QAbstractListModel { Q_OBJECT public: enum ExtraRoles { DescriptionRole = Qt::UserRole+1 }; PluginsModel(QObject* parent = 0) : QAbstractListModel(parent) { m_plugins = KDevelop::Core::self()->pluginControllerInternal()->loadedPlugins(); std::sort(m_plugins.begin(), m_plugins.end(), sortPlugins); } KDevelop::IPlugin *pluginForIndex(const QModelIndex& index) const { if (!index.isValid()) return 0; if (index.parent().isValid()) return 0; if (index.column() != 0) return 0; if (index.row() >= m_plugins.count()) return 0; return m_plugins[index.row()]; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { KDevelop::IPlugin* plugin = pluginForIndex(index); if (!plugin) return QVariant(); switch (role) { case Qt::DisplayRole: return displayName(plugin); case DescriptionRole: return pluginInfo(plugin).description(); case Qt::DecorationRole: return pluginInfo(plugin).iconName(); default: return QVariant(); }; } int rowCount(const QModelIndex& parent = QModelIndex()) const override { if (!parent.isValid()) { return m_plugins.count(); } return 0; } private: QList m_plugins; }; class LoadedPluginsDelegate : public KWidgetItemDelegate { Q_OBJECT public: LoadedPluginsDelegate(QAbstractItemView *itemView, QObject *parent = 0) : KWidgetItemDelegate(itemView, parent) , pushButton(new QPushButton) { pushButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); // only for getting size matters } ~LoadedPluginsDelegate() override { delete pushButton; } QList createItemWidgets(const QModelIndex &/*index*/) const override { return QList(); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { int i = 5; int j = 1; QFont font = titleFont(option.font); QFontMetrics fmTitle(font); return QSize(qMax(fmTitle.width(index.model()->data(index, Qt::DisplayRole).toString()), option.fontMetrics.width(index.model()->data(index, PluginsModel::DescriptionRole).toString())) + KIconLoader::SizeMedium + MARGIN * i + pushButton->sizeHint().width() * j, qMax(KIconLoader::SizeMedium + MARGIN * 2, fmTitle.height() + option.fontMetrics.height() + MARGIN * 2)); } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (!index.isValid()) { return; } painter->save(); QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, 0); int iconSize = option.rect.height() - MARGIN * 2; - QPixmap pixmap = KIconLoader::global()->loadIcon(index.model()->data(index, Qt::DecorationRole).toString(), - KIconLoader::Desktop, iconSize, KIconLoader::DefaultState); - - painter->drawPixmap(QRect(dependantLayoutValue(MARGIN + option.rect.left(), iconSize, option.rect.width()), MARGIN + option.rect.top(), iconSize, iconSize), pixmap, QRect(0, 0, iconSize, iconSize)); - + QIcon icon = QIcon::fromTheme(index.model()->data(index, Qt::DecorationRole).toString()); + icon.paint(painter, QRect(dependantLayoutValue(MARGIN + option.rect.left(), iconSize, option.rect.width()), MARGIN + option.rect.top(), iconSize, iconSize)); QRect contentsRect(dependantLayoutValue(MARGIN * 2 + iconSize + option.rect.left(), option.rect.width() - MARGIN * 3 - iconSize, option.rect.width()), MARGIN + option.rect.top(), option.rect.width() - MARGIN * 3 - iconSize, option.rect.height() - MARGIN * 2); int lessHorizontalSpace = MARGIN * 2 + pushButton->sizeHint().width(); contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace); if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.highlightedText().color()); } if (itemView()->layoutDirection() == Qt::RightToLeft) { contentsRect.translate(lessHorizontalSpace, 0); } painter->save(); painter->save(); QFont font = titleFont(option.font); QFontMetrics fmTitle(font); painter->setFont(font); painter->drawText(contentsRect, Qt::AlignLeft | Qt::AlignTop, fmTitle.elidedText(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ElideRight, contentsRect.width())); painter->restore(); painter->drawText(contentsRect, Qt::AlignLeft | Qt::AlignBottom, option.fontMetrics.elidedText(index.model()->data(index, PluginsModel::DescriptionRole).toString(), Qt::ElideRight, contentsRect.width())); painter->restore(); painter->restore(); } QList createItemWidgets() const { QPushButton *button = new QPushButton(); button->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); setBlockedEventTypes(button, QList() << QEvent::MouseButtonPress << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick); connect(button, &QPushButton::clicked, this, &LoadedPluginsDelegate::info); return QList() << button; } void updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const override { Q_UNUSED(index); if ( widgets.isEmpty() ) { qDebug() << "Fixme: missing button?"; return; } QPushButton *aboutPushButton = static_cast(widgets[0]); QSize aboutPushButtonSizeHint = aboutPushButton->sizeHint(); aboutPushButton->resize(aboutPushButtonSizeHint); aboutPushButton->move(dependantLayoutValue(option.rect.width() - MARGIN - aboutPushButtonSizeHint.width(), aboutPushButtonSizeHint.width(), option.rect.width()), option.rect.height() / 2 - aboutPushButtonSizeHint.height() / 2); } int dependantLayoutValue(int value, int width, int totalWidth) const { if (itemView()->layoutDirection() == Qt::LeftToRight) { return value; } return totalWidth - width - value; } QFont titleFont(const QFont &baseFont) const { QFont retFont(baseFont); retFont.setBold(true); return retFont; } private Q_SLOTS: void info() { PluginsModel *m = static_cast(itemView()->model()); KDevelop::IPlugin *p = m->pluginForIndex(focusedIndex()); if (p) { // TODO KF5: Port // const K4AboutData *aboutData = p->componentData().aboutData(); // if (!aboutData->programName().isEmpty()) { // Be sure the about data is not completely empty // KAboutApplicationDialog aboutPlugin(aboutData, itemView()); // aboutPlugin.exec(); // return; // } } } private: QPushButton *pushButton; }; class PluginsView : public QListView { Q_OBJECT public: PluginsView(QWidget* parent = 0) :QListView(parent) { setModel(new PluginsModel()); setItemDelegate(new LoadedPluginsDelegate(this)); setVerticalScrollMode(QListView::ScrollPerPixel); } ~PluginsView() override { // explicitly delete the delegate here since otherwise // we get spammed by warnings that the QPushButton we return // in createItemWidgets is deleted before the delegate // *sigh* - even dfaure says KWidgetItemDelegate is a crude hack delete itemDelegate(); } QSize sizeHint() const override { QSize ret = QListView::sizeHint(); ret.setWidth(qMax(ret.width(), sizeHintForColumn(0) + 30)); return ret; } }; LoadedPluginsDialog::LoadedPluginsDialog( QWidget* parent ) : QDialog( parent ) { setWindowTitle(i18n("Loaded Plugins")); QVBoxLayout* vbox = new QVBoxLayout(this); KTitleWidget* title = new KTitleWidget(this); title->setPixmap(QIcon::fromTheme(KAboutData::applicationData().programIconName()), KTitleWidget::ImageLeft); title->setText(i18n("Plugins loaded for %1", KAboutData::applicationData().displayName())); vbox->addWidget(title); vbox->addWidget(new PluginsView()); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &LoadedPluginsDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &LoadedPluginsDialog::reject); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); vbox->addWidget(buttonBox); } #include "moc_loadedpluginsdialog.cpp" #include "loadedpluginsdialog.moc" diff --git a/shell/problemmodel.cpp b/shell/problemmodel.cpp index 5c785cfa7e..e0556d67dd 100644 --- a/shell/problemmodel.cpp +++ b/shell/problemmodel.cpp @@ -1,325 +1,340 @@ /* * 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); /// 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::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) { - Q_ASSERT(thread() == QThread::currentThread()); + 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->setSeverity(severity); + 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 0e4810d52f..89c844874e 100644 --- a/shell/problemmodel.h +++ b/shell/problemmodel.h @@ -1,182 +1,184 @@ /* * 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); + 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); /// 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/shell/problemstore.cpp b/shell/problemstore.cpp index c1bbe50e0f..cfd4911f76 100644 --- a/shell/problemstore.cpp +++ b/shell/problemstore.cpp @@ -1,211 +1,238 @@ /* * 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. */ #include "problemstore.h" #include #include #include "problemstorenode.h" struct ProblemStorePrivate { ProblemStorePrivate() : m_documents(nullptr) - , m_severity(KDevelop::IProblem::Hint) + , m_severities(KDevelop::IProblem::Error | KDevelop::IProblem::Warning | KDevelop::IProblem::Hint) , m_rootNode(new KDevelop::ProblemStoreNode()) { } /// Watched document set. Only problems that are in files in this set are stored. KDevelop::WatchedDocumentSet *m_documents; /// The severity filter setting - int m_severity; + KDevelop::IProblem::Severities m_severities; /// The problems list KDevelop::ProblemStoreNode *m_rootNode; /// Path of the currently open document KDevelop::IndexedString m_currentDocument; }; namespace KDevelop { ProblemStore::ProblemStore(QObject *parent) : QObject(parent), d(new ProblemStorePrivate) { } ProblemStore::~ProblemStore() { clear(); delete d->m_rootNode; } void ProblemStore::addProblem(const IProblem::Ptr &problem) { ProblemNode *node = new ProblemNode(d->m_rootNode); node->setProblem(problem); d->m_rootNode->addChild(node); } void ProblemStore::setProblems(const QVector &problems) { clear(); foreach (IProblem::Ptr problem, problems) { d->m_rootNode->addChild(new ProblemNode(d->m_rootNode, problem)); } rebuild(); } const ProblemStoreNode* ProblemStore::findNode(int row, ProblemStoreNode *parent) const { Q_UNUSED(parent); return d->m_rootNode->child(row); } int ProblemStore::count(ProblemStoreNode *parent) const { if(parent) return parent->count(); else return d->m_rootNode->count(); } void ProblemStore::clear() { d->m_rootNode->clear(); } void ProblemStore::rebuild() { } void ProblemStore::setSeverity(int severity) { - if(severity != d->m_severity) + switch (severity) { - d->m_severity = 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 ProblemStore::setSeverities(KDevelop::IProblem::Severities severities) +{ + if(severities != d->m_severities) + { + d->m_severities = severities; rebuild(); emit changed(); } } int ProblemStore::severity() const { - return d->m_severity; + if (d->m_severities.testFlag(KDevelop::IProblem::Hint)) + return KDevelop::IProblem::Hint; + if (d->m_severities.testFlag(KDevelop::IProblem::Warning)) + return KDevelop::IProblem::Warning; + if (d->m_severities.testFlag(KDevelop::IProblem::Error)) + return KDevelop::IProblem::Error; + return 0; +} + +KDevelop::IProblem::Severities ProblemStore::severities() const +{ + return d->m_severities; } WatchedDocumentSet* ProblemStore::documents() const { return d->m_documents; } void ProblemStore::setScope(int scope) { ProblemScope cast_scope = static_cast(scope); if (cast_scope == BypassScopeFilter) { setBypassScopeFilter(true); return; } setBypassScopeFilter(false); if (d->m_documents) { if(cast_scope == d->m_documents->getScope()) return; delete d->m_documents; } switch (cast_scope) { case CurrentDocument: d->m_documents = new CurrentDocumentSet(d->m_currentDocument, this); break; case OpenDocuments: d->m_documents = new OpenDocumentSet(this); break; case CurrentProject: d->m_documents = new CurrentProjectSet(d->m_currentDocument, this); break; case AllProjects: d->m_documents = new AllProjectSet(this); break; case BypassScopeFilter: // handled above break; } rebuild(); connect(d->m_documents, &WatchedDocumentSet::changed, this, &ProblemStore::onDocumentSetChanged); emit changed(); } int ProblemStore::scope() const { Q_ASSERT(d->m_documents != nullptr); return d->m_documents->getScope(); } void ProblemStore::setGrouping(int grouping) { Q_UNUSED(grouping); } void ProblemStore::setBypassScopeFilter(bool bypass) { Q_UNUSED(bypass); } void ProblemStore::setCurrentDocument(const IndexedString &doc) { d->m_currentDocument = doc; d->m_documents->setCurrentDocument(doc); } const KDevelop::IndexedString& ProblemStore::currentDocument() const { return d->m_currentDocument; } void ProblemStore::onDocumentSetChanged() { rebuild(); emit changed(); } ProblemStoreNode* ProblemStore::rootNode() { return d->m_rootNode; } } diff --git a/shell/problemstore.h b/shell/problemstore.h index 4209e8043b..7ca137f38c 100644 --- a/shell/problemstore.h +++ b/shell/problemstore.h @@ -1,148 +1,152 @@ /* * 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 PROBLEMSTORE_H #define PROBLEMSTORE_H #include #include #include #include #include struct ProblemStorePrivate; namespace KDevelop { class WatchedDocumentSet; class ProblemStoreNode; /** * @brief Stores and handles problems. Does no ordering or filtering, those should be done in subclasses. * * Used to store problems that are ordered, filtered somewhere else. For example: DUChain problems gathered by ProblemReporter. * Stores the problems in ProblemStoreNodes. * When implementing a subclass, first and foremost the rebuild method needs to be implemented, which is called every time there's a change in scope and severity filter. * If grouping is desired then also the setGrouping method must be implemented. * ProblemStore depending on settings uses CurrentDocumentSet, OpenDocumentSet, CurrentProjectSet, or AllProjectSet for scope support (NOTE: Filtering still has to be implemented in either a subclass, or somewhere else). * When the scope changes it emits the changed() signal. * * Scope set / query methods: * \li setScope() * \li scope() * * Valid scope settings: * \li CurrentDocument * \li OpenDocuments * \li CurrentProject * \li AllProjects * \li BypassScopeFilter * * Usage example: * @code * QVector problems; * // Add 4 problems * ... * ProblemStore *store = new ProblemStore(); * store->setProblems(problems); * store->count(); // Returns 4 * * ProblemStoreNode *node = store->findNode(0); // Returns the node with the first problem * @endcode * */ class KDEVPLATFORMSHELL_EXPORT ProblemStore : public QObject { Q_OBJECT public: explicit ProblemStore(QObject *parent = nullptr); ~ProblemStore() override; /// Adds a problem virtual void addProblem(const IProblem::Ptr &problem); /// Clears the current problems, and adds new ones from a list virtual void setProblems(const QVector &problems); /// Finds the specified node virtual const ProblemStoreNode* findNode(int row, ProblemStoreNode *parent = nullptr) const; /// Returns the number of problems virtual int count(ProblemStoreNode *parent = nullptr) const; /// Clears the problems virtual void clear(); /// Rebuild the problems list, if applicable. It does nothing in the base class. virtual void rebuild(); /// Specifies the severity filter - virtual void setSeverity(int severity); + virtual void setSeverity(int severity);///old-style severity access + + virtual void setSeverities(KDevelop::IProblem::Severities severities);///new-style severity access /// Retrives the severity filter settings - int severity() const; + int severity() const;///old-style severity access + + KDevelop::IProblem::Severities severities() const;//new-style severity access /// Retrieves the currently watched document set WatchedDocumentSet* documents() const; /// Sets the scope filter void setScope(int scope); /// Returns the current scope int scope() const; /// Sets the grouping method virtual void setGrouping(int grouping); /// Sets whether we should bypass the scope filter virtual void setBypassScopeFilter(bool bypass); /// Sets the currently shown document (in the editor, it's triggered by the IDE) void setCurrentDocument(const IndexedString &doc); /// Retrives the path of the current document const KDevelop::IndexedString& currentDocument() const; signals: /// Emitted when the problems change void changed(); /// Emitted before the problemlist is rebuilt void beginRebuild(); /// Emitted once the problemlist has been rebuilt void endRebuild(); private slots: /// Triggered when the watched document set changes. E.g.:document closed, new one added, etc virtual void onDocumentSetChanged(); protected: ProblemStoreNode* rootNode(); private: QScopedPointer d; }; } #endif diff --git a/shell/tests/test_filteredproblemstore.cpp b/shell/tests/test_filteredproblemstore.cpp index 7693505613..f9b5dcbe7d 100644 --- a/shell/tests/test_filteredproblemstore.cpp +++ b/shell/tests/test_filteredproblemstore.cpp @@ -1,511 +1,698 @@ /* * 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. */ #include #include #include #include #include #include #include #include #include namespace { const int ErrorCount = 1; -const int WarningCount = 1; -const int HintCount = 1; +const int WarningCount = 2; +const int HintCount = 3; +const int ProblemsCount = ErrorCount + WarningCount + HintCount; const int ErrorFilterProblemCount = ErrorCount; const int WarningFilterProblemCount = ErrorCount + WarningCount; const int HintFilterProblemCount = ErrorCount + WarningCount + HintCount; } using namespace KDevelop; class TestFilteredProblemStore : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testBypass(); void testSeverity(); + void testSeverities(); void testGrouping(); void testNoGrouping(); void testPathGrouping(); void testSeverityGrouping(); private: // Severity grouping testing bool checkCounts(int error, int warning, int hint); bool checkNodeLabels(); // --------------------------- void generateProblems(); QScopedPointer m_store; QVector m_problems; IProblem::Ptr m_diagnosticTestProblem; }; #define MYCOMPARE(actual, expected) \ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ return false #define MYVERIFY(statement) \ if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ return false void TestFilteredProblemStore::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); m_store.reset(new FilteredProblemStore()); generateProblems(); } void TestFilteredProblemStore::cleanupTestCase() { TestCore::shutdown(); } void TestFilteredProblemStore::testBypass() { QSignalSpy changedSpy(m_store.data(), &FilteredProblemStore::changed); QSignalSpy beginRebuildSpy(m_store.data(), &FilteredProblemStore::beginRebuild); QSignalSpy endRebuildSpy(m_store.data(), &FilteredProblemStore::endRebuild); QVERIFY(!m_store->bypassScopeFilter()); m_store->setBypassScopeFilter(true); QVERIFY(m_store->bypassScopeFilter()); QCOMPARE(changedSpy.count(), 1); QCOMPARE(beginRebuildSpy.count(), 1); QCOMPARE(endRebuildSpy.count(), 1); } void TestFilteredProblemStore::testSeverity() { QVERIFY(m_store->severity() == IProblem::Hint); QSignalSpy changedSpy(m_store.data(), &FilteredProblemStore::changed); QSignalSpy beginRebuildSpy(m_store.data(), &FilteredProblemStore::beginRebuild); QSignalSpy endRebuildSpy(m_store.data(), &FilteredProblemStore::endRebuild); m_store->setSeverity(IProblem::Error); QVERIFY(m_store->severity() == IProblem::Error); QCOMPARE(changedSpy.count(), 1); QCOMPARE(beginRebuildSpy.count(), 1); QCOMPARE(endRebuildSpy.count(), 1); m_store->setSeverity(IProblem::Hint); } +void TestFilteredProblemStore::testSeverities() +{ + QVERIFY(m_store->severities() == (IProblem::Error | IProblem::Warning | IProblem::Hint)); + + QSignalSpy changedSpy(m_store.data(), &FilteredProblemStore::changed); + QSignalSpy beginRebuildSpy(m_store.data(), &FilteredProblemStore::beginRebuild); + QSignalSpy endRebuildSpy(m_store.data(), &FilteredProblemStore::endRebuild); + + m_store->setSeverities(IProblem::Error | IProblem::Hint); + + QVERIFY(m_store->severities() == (IProblem::Error | IProblem::Hint)); + + QCOMPARE(changedSpy.count(), 1); + QCOMPARE(beginRebuildSpy.count(), 1); + QCOMPARE(endRebuildSpy.count(), 1); + + m_store->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); +} + void TestFilteredProblemStore::testGrouping() { QVERIFY(m_store->grouping() == NoGrouping); QSignalSpy changedSpy(m_store.data(), &FilteredProblemStore::changed); QSignalSpy beginRebuildSpy(m_store.data(), &FilteredProblemStore::beginRebuild); QSignalSpy endRebuildSpy(m_store.data(), &FilteredProblemStore::endRebuild); m_store->setGrouping(PathGrouping); QVERIFY(m_store->grouping() == PathGrouping); QCOMPARE(changedSpy.count(), 1); QCOMPARE(beginRebuildSpy.count(), 1); QCOMPARE(endRebuildSpy.count(), 1); m_store->setGrouping(NoGrouping); } // Compares the node and it's children to a reference problem and it's diagnostics bool checkDiagnodes(const ProblemStoreNode *node, const IProblem::Ptr &reference) { const ProblemNode *problemNode = dynamic_cast(node); MYVERIFY(problemNode != nullptr); MYCOMPARE(problemNode->problem()->description(), reference->description()); MYCOMPARE(problemNode->problem()->finalLocation().document.str(), reference->finalLocation().document.str()); MYCOMPARE(problemNode->count(), 1); const IProblem::Ptr diag = reference->diagnostics().at(0); const ProblemNode *diagNode = dynamic_cast(problemNode->child(0)); MYVERIFY(diagNode != nullptr); MYCOMPARE(diagNode->problem()->description(), diag->description()); MYCOMPARE(diagNode->count(), 1); const IProblem::Ptr diagdiag = diag->diagnostics().at(0); const ProblemNode *diagdiagNode = dynamic_cast(diagNode->child(0)); MYVERIFY(diagdiagNode != nullptr); MYCOMPARE(diagdiagNode->problem()->description(), diagdiag->description()); MYCOMPARE(diagdiagNode->count(), 0); return true; } void TestFilteredProblemStore::testNoGrouping() { // Add problems int c = 0; foreach (const IProblem::Ptr &p, m_problems) { m_store->addProblem(p); c++; QCOMPARE(m_store->count(), c); } for (int i = 0; i < c; i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(i)); QVERIFY(node != nullptr); QCOMPARE(node->problem()->description(), m_problems[i]->description()); } // Check if clear works m_store->clear(); QCOMPARE(m_store->count(), 0); // Set problems m_store->setProblems(m_problems); QCOMPARE(m_problems.count(), m_store->count()); for (int i = 0; i < c; i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(i)); QVERIFY(node != nullptr); QCOMPARE(node->problem()->description(), m_problems[i]->description()); } - // Check severity filtering + // Check old style severity filtering + // old-style setSeverity // Error filter m_store->setSeverity(IProblem::Error); QCOMPARE(m_store->count(), ErrorFilterProblemCount); - - { + for (int i = 0; i < ErrorFilterProblemCount; i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(0)); QVERIFY(node != nullptr); - QCOMPARE(node->problem()->description(), m_problems[0]->description()); + QCOMPARE(node->problem()->description(), m_problems[i]->description()); } // Warning filter m_store->setSeverity(IProblem::Warning); QCOMPARE(m_store->count(), WarningFilterProblemCount); for (int i = 0; i < WarningFilterProblemCount; i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(i)); QVERIFY(node != nullptr); QCOMPARE(node->problem()->description(), m_problems[i]->description()); } // Hint filter m_store->setSeverity(IProblem::Hint); QCOMPARE(m_store->count(), HintFilterProblemCount); for (int i = 0; i < HintFilterProblemCount; i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(i)); QVERIFY(node != nullptr); QCOMPARE(node->problem()->description(), m_problems[i]->description()); } + // Check new severity filtering + // Error filter + m_store->setSeverities(IProblem::Error); + QCOMPARE(m_store->count(), ErrorCount); + for (int i = 0; i < ErrorCount; i++) { + const ProblemNode *node = dynamic_cast(m_store->findNode(i)); + QVERIFY(node != nullptr); + QCOMPARE(node->problem()->description(), m_problems[i]->description()); + } + + // Warning filter + m_store->setSeverities(IProblem::Warning); + QCOMPARE(m_store->count(), WarningCount); + for (int i = 0; i < WarningCount; i++) { + const ProblemNode *node = dynamic_cast(m_store->findNode(i)); + QVERIFY(node != nullptr); + + QCOMPARE(node->problem()->description(), m_problems[i+ErrorCount]->description()); + } + + // Hint filter + m_store->setSeverities(IProblem::Hint); + QCOMPARE(m_store->count(), HintCount); + for (int i = 0; i < HintCount; i++) { + const ProblemNode *node = dynamic_cast(m_store->findNode(i)); + QVERIFY(node != nullptr); + + QCOMPARE(node->problem()->description(), m_problems[i+ErrorCount+WarningCount]->description()); + } + + //Error + Hint filter + m_store->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_store->count(), HintCount + ErrorCount); + for (int i = 0; i < ErrorCount; i++) { + const ProblemNode *node = dynamic_cast(m_store->findNode(i)); + QVERIFY(node != nullptr); + + QCOMPARE(node->problem()->description(), m_problems[i]->description()); + } + for (int i = ErrorCount; i < ErrorCount+HintCount; i++) { + const ProblemNode *node = dynamic_cast(m_store->findNode(i)); + QVERIFY(node != nullptr); + + QCOMPARE(node->problem()->description(), m_problems[i+WarningCount]->description()); + } + + m_store->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); m_store->clear(); // Check if diagnostics are added properly m_store->addProblem(m_diagnosticTestProblem); QCOMPARE(m_store->count(), 1); QVERIFY(checkDiagnodes(m_store->findNode(0), m_diagnosticTestProblem)); } bool checkNodeLabel(const ProblemStoreNode *node, const QString &label) { const LabelNode *parent = dynamic_cast(node); MYVERIFY(parent != nullptr); MYCOMPARE(parent->label(), label); return true; } bool checkNodeDescription(const ProblemStoreNode *node, const QString &descr) { const ProblemNode *n = dynamic_cast(node); MYVERIFY(n != nullptr); MYCOMPARE(n->problem()->description(), descr); return true; } void TestFilteredProblemStore::testPathGrouping() { m_store->clear(); // Rebuild the problem list with grouping m_store->setGrouping(PathGrouping); // Add problems foreach (const IProblem::Ptr &p, m_problems) { m_store->addProblem(p); } - QCOMPARE(m_store->count(), 3); + QCOMPARE(m_store->count(), ProblemsCount); for (int i = 0; i < 3; i++) { const ProblemStoreNode *node = m_store->findNode(i); checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); QCOMPARE(node->count(), 1); checkNodeDescription(node->child(0), m_problems[i]->description()); } // Now add a new problem IProblem::Ptr p(new DetectedProblem()); p->setDescription(QStringLiteral("PROBLEM4")); p->setFinalLocation(m_problems[2]->finalLocation()); m_store->addProblem(p); - QCOMPARE(m_store->count(), 3); + QCOMPARE(m_store->count(), ProblemsCount); // Check the first 2 top-nodes for (int i = 0; i < 2; i++) { const ProblemStoreNode *node = m_store->findNode(i); checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); QCOMPARE(node->count(), 1); checkNodeDescription(node->child(0), m_problems[i]->description()); } // check the last one, and check the added problem is at the right place { const ProblemStoreNode *node = m_store->findNode(2); checkNodeLabel(node, m_problems[2]->finalLocation().document.str()); QCOMPARE(node->count(), 2); checkNodeDescription(node->child(1), p->description()); } m_store->clear(); m_store->setProblems(m_problems); // Check filters + // old-style setSeverity // Error filter m_store->setSeverity(IProblem::Error); QCOMPARE(m_store->count(), ErrorFilterProblemCount); { const ProblemStoreNode *node = m_store->findNode(0); checkNodeLabel(node, m_problems[0]->finalLocation().document.str()); - QCOMPARE(node->count(), ErrorCount); + QCOMPARE(node->count(), 1); checkNodeDescription(node->child(0), m_problems[0]->description()); } // Warning filter m_store->setSeverity(IProblem::Warning); QCOMPARE(m_store->count(), WarningFilterProblemCount); for (int i = 0; i < WarningFilterProblemCount; i++) { const ProblemStoreNode *node = m_store->findNode(i); checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); - QCOMPARE(node->count(), WarningCount); + QCOMPARE(node->count(), 1); checkNodeDescription(node->child(0), m_problems[i]->description()); } // Hint filter m_store->setSeverity(IProblem::Hint); QCOMPARE(m_store->count(), HintFilterProblemCount); for (int i = 0; i < HintFilterProblemCount; i++) { const ProblemStoreNode *node = m_store->findNode(i); checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); - QCOMPARE(node->count(), HintCount); + QCOMPARE(node->count(), 1); + checkNodeDescription(node->child(0), m_problems[i]->description()); + } + + // Check new severity filtering + // Error filter + m_store->setSeverities(IProblem::Error); + for (int i = 0; i < ErrorCount; i++) { + const ProblemStoreNode *node = m_store->findNode(i); + checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); + QCOMPARE(node->count(), 1); + checkNodeDescription(node->child(0), m_problems[i]->description()); + } + + // Warning filter + m_store->setSeverities(IProblem::Warning); + for (int i = 0; i < WarningCount; i++) { + const ProblemStoreNode *node = m_store->findNode(i); + checkNodeLabel(node, m_problems[i+ErrorCount]->finalLocation().document.str()); + QCOMPARE(node->count(), 1); + checkNodeDescription(node->child(0), m_problems[i+ErrorCount]->description()); + } + + // Hint filter + m_store->setSeverities(IProblem::Hint); + for (int i = 0; i < HintCount; i++) { + const ProblemStoreNode *node = m_store->findNode(i); + checkNodeLabel(node, m_problems[i+ErrorCount+WarningCount]->finalLocation().document.str()); + QCOMPARE(node->count(), 1); + checkNodeDescription(node->child(0), m_problems[i+ErrorCount+WarningCount]->description()); + } + + //Error + Hint filter + m_store->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_store->count(), HintCount + ErrorCount); + for (int i = 0; i < ErrorCount; i++) { + const ProblemStoreNode *node = m_store->findNode(i); + checkNodeLabel(node, m_problems[i]->finalLocation().document.str()); + QCOMPARE(node->count(), 1); checkNodeDescription(node->child(0), m_problems[i]->description()); } + for (int i = ErrorCount; i < ErrorCount+HintCount; i++) { + const ProblemStoreNode *node = m_store->findNode(i); + checkNodeLabel(node, m_problems[i+WarningCount]->finalLocation().document.str()); + QCOMPARE(node->count(), 1); + checkNodeDescription(node->child(0), m_problems[i+WarningCount]->description()); + } + + m_store->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); m_store->clear(); // Check if the diagnostics are added properly m_store->addProblem(m_diagnosticTestProblem); QCOMPARE(m_store->count(), 1); const LabelNode *node = dynamic_cast(m_store->findNode(0)); QVERIFY(node != nullptr); QCOMPARE(node->label(), m_diagnosticTestProblem->finalLocation().document.str()); QVERIFY(checkDiagnodes(node->child(0), m_diagnosticTestProblem)); } void TestFilteredProblemStore::testSeverityGrouping() { m_store->clear(); m_store->setGrouping(SeverityGrouping); QCOMPARE(m_store->count(), 3); const ProblemStoreNode *errorNode = m_store->findNode(0); const ProblemStoreNode *warningNode = m_store->findNode(1); const ProblemStoreNode *hintNode = m_store->findNode(2); // Add problems - int c = 0; - foreach (const IProblem::Ptr &p, m_problems) { - m_store->addProblem(p); - - QCOMPARE(m_store->findNode(c)->count(), 1); - c++; + for (int i=0;iaddProblem(m_problems[i]); + int severityType = 0; //error + int addedCountOfCurrentSeverityType = i + 1; + if (i>=ErrorCount) + { + severityType = 1; //warning + addedCountOfCurrentSeverityType = i - ErrorCount + 1; + } + if (i>=ErrorCount+WarningCount) + { + severityType = 2; //hint + addedCountOfCurrentSeverityType = i - (ErrorCount + WarningCount) + 1; + } + QCOMPARE(m_store->findNode(severityType)->count(), addedCountOfCurrentSeverityType); } + QVERIFY(checkNodeLabels()); QVERIFY(checkCounts(ErrorCount, WarningCount, HintCount)); checkNodeDescription(errorNode->child(0), m_problems[0]->description()); checkNodeDescription(warningNode->child(0), m_problems[1]->description()); - checkNodeDescription(hintNode->child(0), m_problems[2]->description()); + checkNodeDescription(hintNode->child(0), m_problems[3]->description()); // Clear m_store->clear(); QCOMPARE(m_store->count(), 3); QVERIFY(checkCounts(0,0,0)); // Set problems m_store->setProblems(m_problems); QCOMPARE(m_store->count(), 3); QVERIFY(checkNodeLabels()); QVERIFY(checkCounts(ErrorCount, WarningCount, HintCount)); checkNodeDescription(errorNode->child(0), m_problems[0]->description()); checkNodeDescription(warningNode->child(0), m_problems[1]->description()); - checkNodeDescription(hintNode->child(0), m_problems[2]->description()); + checkNodeDescription(hintNode->child(0), m_problems[3]->description()); // Check severity filter + // old-style setSeverity // Error filter m_store->setSeverity(IProblem::Error); QCOMPARE(m_store->count(), 3); QVERIFY(checkNodeLabels()); QVERIFY(checkCounts(ErrorCount, 0, 0)); checkNodeDescription(errorNode->child(0), m_problems[0]->description()); // Warning filter m_store->setSeverity(IProblem::Warning); QCOMPARE(m_store->count(), 3); checkNodeLabels(); QVERIFY(checkCounts(ErrorCount, WarningCount, 0)); checkNodeDescription(errorNode->child(0), m_problems[0]->description()); checkNodeDescription(warningNode->child(0), m_problems[1]->description()); // Hint filter m_store->setSeverity(IProblem::Hint); QCOMPARE(m_store->count(), 3); QVERIFY(checkNodeLabels()); QVERIFY(checkCounts(ErrorCount, WarningCount, HintCount)); checkNodeDescription(errorNode->child(0), m_problems[0]->description()); checkNodeDescription(warningNode->child(0), m_problems[1]->description()); - checkNodeDescription(hintNode->child(0), m_problems[2]->description()); + checkNodeDescription(hintNode->child(0), m_problems[3]->description()); + + // Check severity filter + // Error filter + m_store->setSeverities(IProblem::Error); + QCOMPARE(m_store->count(), 3); + QVERIFY(checkNodeLabels()); + QVERIFY(checkCounts(ErrorCount, 0, 0)); + checkNodeDescription(errorNode->child(0), m_problems[0]->description()); + + // Warning filter + m_store->setSeverities(IProblem::Warning); + QCOMPARE(m_store->count(), 3); + QVERIFY(checkNodeLabels()); + QVERIFY(checkCounts(0, WarningCount, 0)); + checkNodeDescription(warningNode->child(0), m_problems[1]->description()); + + // Hint filter + m_store->setSeverities(IProblem::Hint); + QCOMPARE(m_store->count(), 3); + QVERIFY(checkNodeLabels()); + QVERIFY(checkCounts(0, 0, HintCount)); + checkNodeDescription(hintNode->child(0), m_problems[3]->description()); + + // Error + Hint filter + m_store->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_store->count(), 3); + QVERIFY(checkNodeLabels()); + QVERIFY(checkCounts(ErrorCount, 0, HintCount)); + checkNodeDescription(errorNode->child(0), m_problems[0]->description()); + checkNodeDescription(hintNode->child(0), m_problems[3]->description()); + + m_store->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); m_store->clear(); // Check if diagnostics are added properly m_store->addProblem(m_diagnosticTestProblem); QVERIFY(checkNodeLabels()); QVERIFY(checkCounts(1, 0, 0)); QVERIFY(checkDiagnodes(m_store->findNode(0)->child(0), m_diagnosticTestProblem)); } bool TestFilteredProblemStore::checkCounts(int error, int warning, int hint) { const ProblemStoreNode *errorNode = m_store->findNode(0); const ProblemStoreNode *warningNode = m_store->findNode(1); const ProblemStoreNode *hintNode = m_store->findNode(2); MYVERIFY(errorNode != nullptr); MYVERIFY(warningNode != nullptr); MYVERIFY(hintNode != nullptr); MYCOMPARE(errorNode->count(), error); MYCOMPARE(warningNode->count(), warning); MYCOMPARE(hintNode->count(), hint); return true; } bool TestFilteredProblemStore::checkNodeLabels() { const ProblemStoreNode *errorNode = m_store->findNode(0); const ProblemStoreNode *warningNode = m_store->findNode(1); const ProblemStoreNode *hintNode = m_store->findNode(2); MYCOMPARE(checkNodeLabel(errorNode, i18n("Error")), true); MYCOMPARE(checkNodeLabel(warningNode, i18n("Warning")), true); MYCOMPARE(checkNodeLabel(hintNode, i18n("Hint")), true); return true; } // Generate 3 problems, all with different paths, different severity // Also generates a problem with diagnostics void TestFilteredProblemStore::generateProblems() { IProblem::Ptr p1(new DetectedProblem()); IProblem::Ptr p2(new DetectedProblem()); IProblem::Ptr p3(new DetectedProblem()); + IProblem::Ptr p4(new DetectedProblem()); + IProblem::Ptr p5(new DetectedProblem()); + IProblem::Ptr p6(new DetectedProblem()); DocumentRange r1; r1.document = IndexedString("/just/a/random/path"); p1->setDescription(QStringLiteral("PROBLEM1")); p1->setSeverity(IProblem::Error); p1->setFinalLocation(r1); DocumentRange r2; r2.document = IndexedString("/just/another/path"); p2->setDescription(QStringLiteral("PROBLEM2")); p2->setSeverity(IProblem::Warning); p2->setFinalLocation(r2); DocumentRange r3; - r3.document = IndexedString("/yet/another/test/path"); + r3.document = IndexedString("/just/another/pathy/patha"); - p2->setDescription(QStringLiteral("PROBLEM3")); - p3->setSeverity(IProblem::Hint); + p3->setDescription(QStringLiteral("PROBLEM3")); + p3->setSeverity(IProblem::Warning); p3->setFinalLocation(r3); + DocumentRange r4; + r4.document = IndexedString("/yet/another/test/path"); + + p4->setDescription(QStringLiteral("PROBLEM4")); + p4->setSeverity(IProblem::Hint); + p4->setFinalLocation(r4); + + DocumentRange r5; + r5.document = IndexedString("/yet/another/pathy/test/path"); + + p5->setDescription(QStringLiteral("PROBLEM5")); + p5->setSeverity(IProblem::Hint); + p5->setFinalLocation(r5); + + DocumentRange r6; + r6.document = IndexedString("/yet/another/test/pathy/path"); + + p6->setDescription(QStringLiteral("PROBLEM6")); + p6->setSeverity(IProblem::Hint); + p6->setFinalLocation(r6); + + m_problems.push_back(p1); m_problems.push_back(p2); m_problems.push_back(p3); + m_problems.push_back(p4); + m_problems.push_back(p5); + m_problems.push_back(p6); // Problem for diagnostic testing IProblem::Ptr p(new DetectedProblem()); DocumentRange r; r.document = IndexedString("DIAGTEST"); p->setFinalLocation(r); p->setDescription(QStringLiteral("PROBLEM")); p->setSeverity(IProblem::Error); IProblem::Ptr d(new DetectedProblem()); d->setDescription(QStringLiteral("DIAG")); IProblem::Ptr dd(new DetectedProblem()); dd->setDescription(QStringLiteral("DIAGDIAG")); d->addDiagnostic(dd); p->addDiagnostic(d); m_diagnosticTestProblem = p; } QTEST_MAIN(TestFilteredProblemStore) #include "test_filteredproblemstore.moc" diff --git a/shell/tests/test_problemmodel.cpp b/shell/tests/test_problemmodel.cpp index e0bd4752fc..70dc1c1aeb 100644 --- a/shell/tests/test_problemmodel.cpp +++ b/shell/tests/test_problemmodel.cpp @@ -1,438 +1,515 @@ /* * 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. */ #include #include #include #include #include #include #include #include #define MYCOMPARE(actual, expected) \ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ return false #define MYVERIFY(statement) \ if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ return false using namespace KDevelop; class TestProblemModel : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testNoGrouping(); void testPathGrouping(); void testSeverityGrouping(); private: void generateProblems(); bool checkIsSame(int row, const QModelIndex &parent, const IProblem::Ptr &problem); bool checkDiagnostics(int row, const QModelIndex &parent); bool checkDisplay(int row, const QModelIndex &parent, const IProblem::Ptr &problem); bool checkLabel(int row, const QModelIndex &parent, const QString &label); bool checkPathGroup(int row, const IProblem::Ptr &problem); bool checkSeverityGroup(int row, const IProblem::Ptr &problem); QScopedPointer m_model; QVector m_problems; IProblem::Ptr m_diagnosticTestProblem; }; void TestProblemModel::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); m_model.reset(new ProblemModel(nullptr)); m_model->setScope(BypassScopeFilter); generateProblems(); } void TestProblemModel::cleanupTestCase() { TestCore::shutdown(); } void TestProblemModel::testNoGrouping() { m_model->setGrouping(NoGrouping); m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 0); // Check if setting the problems works m_model->setProblems(m_problems); QCOMPARE(m_model->rowCount(), 3); for (int i = 0; i < m_model->rowCount(); i++) { QVERIFY(checkIsSame(i, QModelIndex(), m_problems[i])); } // Check if displaying various data parts works QVERIFY(checkDisplay(0, QModelIndex(), m_problems[0])); // Check if clearing the problems works m_model->clearProblems(); QCOMPARE(m_model->rowCount(), 0); // Check if adding the problems works int c = 0; foreach (const IProblem::Ptr &p, m_problems) { m_model->addProblem(p); c++; QCOMPARE(m_model->rowCount(), c); } for (int i = 0; i < m_model->rowCount(); i++) { QVERIFY(checkIsSame(i, QModelIndex(), m_problems[i])); } // Check if filtering works + // old-style setSeverity // Error filter m_model->setSeverity(IProblem::Error); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); // Warning filter m_model->setSeverity(IProblem::Warning); QCOMPARE(m_model->rowCount(), 2); QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); QVERIFY(checkIsSame(1, QModelIndex(), m_problems[1])); // Hint filter m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 3); QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); QVERIFY(checkIsSame(1, QModelIndex(), m_problems[1])); QVERIFY(checkIsSame(2, QModelIndex(), m_problems[2])); + // Check if filtering works + // new style + // Error filter + m_model->setSeverities(IProblem::Error); + QCOMPARE(m_model->rowCount(), 1); + QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); + + // Warning filter + m_model->setSeverities(IProblem::Warning); + QCOMPARE(m_model->rowCount(), 1); + QVERIFY(checkIsSame(0, QModelIndex(), m_problems[1])); + + // Hint filter + m_model->setSeverities(IProblem::Hint); + QCOMPARE(m_model->rowCount(), 1); + QVERIFY(checkIsSame(0, QModelIndex(), m_problems[2])); + + // Error + Hint filter + m_model->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_model->rowCount(), 2); + QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); + QVERIFY(checkIsSame(1, QModelIndex(), m_problems[2])); + + m_model->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); + // Check if diagnostics are added properly m_model->clearProblems(); m_model->addProblem(m_diagnosticTestProblem); QVERIFY(checkDiagnostics(0, QModelIndex())); m_model->clearProblems(); } void TestProblemModel::testPathGrouping() { m_model->setGrouping(PathGrouping); m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 0); // Check if setting problems works m_model->setProblems(m_problems); QCOMPARE(m_model->rowCount(), 3); for (int i = 0; i < m_model->rowCount(); i++) { QVERIFY(checkLabel(i, QModelIndex(), m_problems[i]->finalLocation().document.str())); QModelIndex idx = m_model->index(i, 0); QVERIFY(idx.isValid()); QVERIFY(checkIsSame(0, idx, m_problems[i])); } // Check if displaying various data parts works { QModelIndex idx = m_model->index(0, 0); QVERIFY(idx.isValid()); QVERIFY(checkDisplay(0, idx, m_problems[0])); } // Check if clearing works m_model->clearProblems(); QCOMPARE(m_model->rowCount(), 0); // Check if add problems works int c = 0; foreach (const IProblem::Ptr &p, m_problems) { m_model->addProblem(p); c++; QCOMPARE(m_model->rowCount(), c); } for (int i = 0; i < m_model->rowCount(); i++) { QVERIFY(checkLabel(i, QModelIndex(), m_problems[i]->finalLocation().document.str())); QModelIndex idx = m_model->index(i, 0); QVERIFY(idx.isValid()); QVERIFY(checkIsSame(0, idx, m_problems[i])); } // Check if filtering works + // old-style setSeverity // Error filtering m_model->setSeverity(IProblem::Error); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkPathGroup(0, m_problems[0])); // Warning filtering m_model->setSeverity(IProblem::Warning); QCOMPARE(m_model->rowCount(), 2); QVERIFY(checkPathGroup(0, m_problems[0])); QVERIFY(checkPathGroup(1, m_problems[1])); // Hint filtering m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 3); QVERIFY(checkPathGroup(0, m_problems[0])); QVERIFY(checkPathGroup(1, m_problems[1])); QVERIFY(checkPathGroup(2, m_problems[2])); + // Check if filtering works + // new style + // Error filtering + m_model->setSeverities(IProblem::Error); + QCOMPARE(m_model->rowCount(), 1); + QVERIFY(checkPathGroup(0, m_problems[0])); + + // Warning filtering + m_model->setSeverities(IProblem::Warning); + QCOMPARE(m_model->rowCount(), 1); + QVERIFY(checkPathGroup(0, m_problems[1])); + + // Hint filtering + m_model->setSeverities(IProblem::Hint); + QCOMPARE(m_model->rowCount(), 1);; + QVERIFY(checkPathGroup(0, m_problems[2])); + + // Error + Hint filtering + m_model->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_model->rowCount(), 2); + QVERIFY(checkPathGroup(0, m_problems[0])); + QVERIFY(checkPathGroup(1, m_problems[2])); + + m_model->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); + // Check if diagnostics get to the right place m_model->clearProblems(); m_model->addProblem(m_diagnosticTestProblem); { QModelIndex parent = m_model->index(0, 0); QVERIFY(parent.isValid()); checkDiagnostics(0, parent); } m_model->clearProblems(); } void TestProblemModel::testSeverityGrouping() { m_model->setGrouping(SeverityGrouping); m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 3); // Check if setting problems works m_model->setProblems(m_problems); QCOMPARE(m_model->rowCount(), 3); for (int i = 0; i < m_model->rowCount(); i++) { QVERIFY(checkSeverityGroup(i, m_problems[i])); } // Check if displaying works for (int i = 0; i < m_model->rowCount(); i++) { QModelIndex parent = m_model->index(i, 0); QVERIFY(parent.isValid()); QVERIFY(checkDisplay(0, parent, m_problems[i])); } // Check if clearing works m_model->clearProblems(); QCOMPARE(m_model->rowCount(), 3); // Check if adding problems works int c = 0; foreach (const IProblem::Ptr &p, m_problems) { m_model->addProblem(p); QVERIFY(checkSeverityGroup(c, m_problems[c])); c++; } // Check if filtering works + // old-style setSeverity // Error filtering m_model->setSeverity(IProblem::Error); QCOMPARE(m_model->rowCount(), 3); checkSeverityGroup(0, m_problems[0]); // Warning filtering m_model->setSeverity(IProblem::Warning); QCOMPARE(m_model->rowCount(), 3); checkSeverityGroup(0, m_problems[0]); checkSeverityGroup(1, m_problems[1]); // Hint filtering m_model->setSeverity(IProblem::Hint); QCOMPARE(m_model->rowCount(), 3); checkSeverityGroup(0, m_problems[0]); checkSeverityGroup(1, m_problems[1]); checkSeverityGroup(2, m_problems[2]); + // Check if filtering works + // Error filtering + m_model->setSeverities(IProblem::Error); + QCOMPARE(m_model->rowCount(), 3); + checkSeverityGroup(0, m_problems[0]); + + // Warning filtering + m_model->setSeverities(IProblem::Warning); + QCOMPARE(m_model->rowCount(), 3); + checkSeverityGroup(1, m_problems[1]); + + // Hint filtering + m_model->setSeverities(IProblem::Hint); + QCOMPARE(m_model->rowCount(), 3); + checkSeverityGroup(2, m_problems[2]); + + // Error + Hint filtering + m_model->setSeverities(IProblem::Error | IProblem::Hint); + QCOMPARE(m_model->rowCount(), 3); + checkSeverityGroup(0, m_problems[0]); + checkSeverityGroup(2, m_problems[2]); + + m_model->setSeverities(IProblem::Error | IProblem::Warning | IProblem::Hint); + // Check if diagnostics get to the right place m_model->clearProblems(); m_model->addProblem(m_diagnosticTestProblem); { QModelIndex parent = m_model->index(0, 0); QVERIFY(parent.isValid()); checkDiagnostics(0, parent); } m_model->clearProblems(); } // Generate 3 problems, all with different paths, different severity // Also generates a problem with diagnostics void TestProblemModel::generateProblems() { IProblem::Ptr p1(new DetectedProblem()); IProblem::Ptr p2(new DetectedProblem()); IProblem::Ptr p3(new DetectedProblem()); DocumentRange r1; r1.document = IndexedString("/just/a/random/path"); p1->setDescription(QStringLiteral("PROBLEM1")); p1->setSeverity(IProblem::Error); p1->setFinalLocation(r1); DocumentRange r2; r2.document = IndexedString("/just/another/path"); p2->setDescription(QStringLiteral("PROBLEM2")); p2->setSeverity(IProblem::Warning); p2->setFinalLocation(r2); DocumentRange r3; r3.document = IndexedString("/yet/another/test/path"); p2->setDescription(QStringLiteral("PROBLEM3")); p3->setSeverity(IProblem::Hint); p3->setFinalLocation(r3); m_problems.push_back(p1); m_problems.push_back(p2); m_problems.push_back(p3); // Problem for diagnostic testing IProblem::Ptr p(new DetectedProblem()); DocumentRange r; r.document = IndexedString("DIAGTEST"); p->setFinalLocation(r); p->setDescription(QStringLiteral("PROBLEM")); p->setSeverity(IProblem::Error); IProblem::Ptr d(new DetectedProblem()); d->setDescription(QStringLiteral("DIAG")); IProblem::Ptr dd(new DetectedProblem()); dd->setDescription(QStringLiteral("DIAGDIAG")); d->addDiagnostic(dd); p->addDiagnostic(d); m_diagnosticTestProblem = p; } bool TestProblemModel::checkIsSame(int row, const QModelIndex &parent, const IProblem::Ptr &problem) { QModelIndex idx; idx = m_model->index(row, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->description()); return true; } bool TestProblemModel::checkDiagnostics(int row, const QModelIndex &parent) { MYCOMPARE(m_model->rowCount(parent), 1); QModelIndex idx; idx = m_model->index(row, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), m_diagnosticTestProblem->description()); QModelIndex diagidx; IProblem::Ptr diag = m_diagnosticTestProblem->diagnostics().at(0); diagidx = m_model->index(0, 0, idx); MYVERIFY(diagidx.isValid()); MYCOMPARE(m_model->data(diagidx).toString(), diag->description()); QModelIndex diagdiagidx; IProblem::Ptr diagdiag = diag->diagnostics().at(0); diagdiagidx = m_model->index(0, 0, diagidx); MYVERIFY(diagdiagidx.isValid()); MYCOMPARE(m_model->data(diagdiagidx).toString(), diagdiag->description()); return true; } bool TestProblemModel::checkDisplay(int row, const QModelIndex &parent, const IProblem::Ptr &problem) { QModelIndex idx; idx = m_model->index(row, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->description()); idx = m_model->index(row, 1, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->sourceString()); idx = m_model->index(row, 2, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->finalLocation().document.str()); idx = m_model->index(row, 3, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), QString::number(problem->finalLocation().start().line() + 1)); idx = m_model->index(row, 4, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), QString::number(problem->finalLocation().start().column() + 1)); return true; } bool TestProblemModel::checkLabel(int row, const QModelIndex &parent, const QString &label) { QModelIndex idx = m_model->index(row, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), label); return true; } bool TestProblemModel::checkPathGroup(int row, const IProblem::Ptr &problem) { QModelIndex parent = m_model->index(row, 0); MYVERIFY(parent.isValid()); MYCOMPARE(m_model->data(parent).toString(), problem->finalLocation().document.str()); QModelIndex idx = m_model->index(0, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->description()); return true; } bool TestProblemModel::checkSeverityGroup(int row, const IProblem::Ptr &problem) { QModelIndex parent = m_model->index(row, 0); MYVERIFY(parent.isValid()); MYCOMPARE(m_model->data(parent).toString(), problem->severityString()); QModelIndex idx = m_model->index(0, 0, parent); MYVERIFY(idx.isValid()); MYCOMPARE(m_model->data(idx).toString(), problem->description()); return true; } QTEST_MAIN(TestProblemModel) #include "test_problemmodel.moc" diff --git a/shell/tests/test_problemstore.cpp b/shell/tests/test_problemstore.cpp index 945ca61470..5832e45811 100644 --- a/shell/tests/test_problemstore.cpp +++ b/shell/tests/test_problemstore.cpp @@ -1,144 +1,158 @@ /* * 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. */ #include #include #include #include #include #include #include #include using namespace KDevelop; class TestProblemStore : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testAddProblems(); void testClearProblems(); void testSetProblems(); void testFindNode(); void testSeverity(); + void testSeverities(); void testScope(); private: void generateProblems(); QScopedPointer m_store; QVector m_problems; }; void TestProblemStore::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); m_store.reset(new ProblemStore()); m_store->setScope(CurrentDocument); QVERIFY(m_store->scope() == CurrentDocument); generateProblems(); } void TestProblemStore::cleanupTestCase() { TestCore::shutdown(); } void TestProblemStore::testAddProblems() { QCOMPARE(m_store->count(), 0); int c = 0; foreach (const IProblem::Ptr &problem, m_problems) { m_store->addProblem(problem); c++; QCOMPARE(m_store->count(), c); } } void TestProblemStore::testClearProblems() { m_store->clear(); QCOMPARE(m_store->count(), 0); } void TestProblemStore::testSetProblems() { m_store->setProblems(m_problems); QCOMPARE(m_store->count(), m_problems.count()); } void TestProblemStore::testFindNode() { for (int i = 0; i < m_problems.count(); i++) { const ProblemNode *node = dynamic_cast(m_store->findNode(i)); QVERIFY(node != nullptr); QVERIFY(node->problem().data() != nullptr); QCOMPARE(node->problem().data()->description(), m_problems[i]->description()); } } void TestProblemStore::testSeverity() { IProblem::Severity severity = IProblem::Error; QVERIFY(severity != m_store->severity()); QSignalSpy spy(m_store.data(), &ProblemStore::changed); m_store->setSeverity(severity); QVERIFY(m_store->severity() == severity); QCOMPARE(spy.count(), 1); } +void TestProblemStore::testSeverities() +{ + IProblem::Severities severities = IProblem::Error | IProblem::Hint; + + QVERIFY(severities != m_store->severities()); + + QSignalSpy spy(m_store.data(), &ProblemStore::changed); + m_store->setSeverities(severities); + + QVERIFY(m_store->severities() == severities); + QCOMPARE(spy.count(), 1); +} + void TestProblemStore::testScope() { QSignalSpy spy(m_store.data(), &ProblemStore::changed); ProblemScope scope = AllProjects; m_store->setScope(scope); QVERIFY(m_store->scope() == scope); QCOMPARE(spy.count(), 1); } void TestProblemStore::generateProblems() { for (int i = 0; i < 5; i++) { IProblem::Ptr problem(new DetectedProblem()); problem->setDescription(QStringLiteral("PROBLEM") + QString::number(i + 1)); m_problems.push_back(problem); } } QTEST_MAIN(TestProblemStore) #include "test_problemstore.moc"