diff --git a/app/plasma/dataengine/kdevelopsessionsservice.cpp b/app/plasma/dataengine/kdevelopsessionsservice.cpp index 7ccaa9f67c..c95b385f16 100644 --- a/app/plasma/dataengine/kdevelopsessionsservice.cpp +++ b/app/plasma/dataengine/kdevelopsessionsservice.cpp @@ -1,52 +1,54 @@ /***************************************************************************** * Copyright (C) 2012 by Eike Hein * * Copyright (C) 2011 by Shaun Reich * * Copyright (C) 2008 by Montel Laurent * * * * 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, see . * *****************************************************************************/ #include "kdevelopsessionsservice.h" #include KDevelopSessionsService::KDevelopSessionsService(QObject* parent, const QString& sessionName) : Plasma::Service(parent) { setName(QStringLiteral("org.kde.plasma.dataengine.kdevelopsessions")); setDestination(sessionName); } Plasma::ServiceJob* KDevelopSessionsService::createJob(const QString& operation, QMap& parameters) { return new SessionJob(this, operation, parameters); } SessionJob::SessionJob(KDevelopSessionsService *service, const QString &operation, const QMap ¶meters) : Plasma::ServiceJob(service->destination(), operation, parameters, service) { } void SessionJob::start() { if (operationName() == QLatin1String("open")) { - QStringList args; - args << QStringLiteral("--open-session") << destination(); + const QStringList args{ + QStringLiteral("--open-session"), + destination(), + }; KToolInvocation::kdeinitExec(QStringLiteral("kdevelop"), args); setResult(true); } } diff --git a/app/plasma/runner/kdevelopsessions.cpp b/app/plasma/runner/kdevelopsessions.cpp index 4c5dfc4832..6ce16e74f8 100644 --- a/app/plasma/runner/kdevelopsessions.cpp +++ b/app/plasma/runner/kdevelopsessions.cpp @@ -1,183 +1,184 @@ /* * Copyright 2008,2011 Sebastian Kügler * * 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, 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 "kdevelopsessions.h" #include #include #include #include #include #include #include #include #include #include #include K_EXPORT_PLASMA_RUNNER(kdevelopsessions, KDevelopSessions) bool kdevelopsessions_runner_compare_sessions(const Session &s1, const Session &s2) { QCollator c; return c.compare(s1.name, s2.name) < 0; } KDevelopSessions::KDevelopSessions(QObject *parent, const QVariantList& args) : Plasma::AbstractRunner(parent, args) { setObjectName(QStringLiteral("KDevelop Sessions")); setIgnoredTypes(Plasma::RunnerContext::File | Plasma::RunnerContext::Directory | Plasma::RunnerContext::NetworkLocation); m_icon = QIcon::fromTheme(QStringLiteral("kdevelop")); loadSessions(); // listen for changes to the list of kdevelop sessions KDirWatch *historyWatch = new KDirWatch(this); const QStringList sessiondirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/sessions"), QStandardPaths::LocateDirectory); foreach (const QString &dir, sessiondirs) { historyWatch->addDir(dir); } connect(historyWatch, &KDirWatch::dirty, this, &KDevelopSessions::loadSessions); connect(historyWatch, &KDirWatch::created, this, &KDevelopSessions::loadSessions); connect(historyWatch, &KDirWatch::deleted, this, &KDevelopSessions::loadSessions); Plasma::RunnerSyntax s(QStringLiteral(":q:"), i18n("Finds KDevelop sessions matching :q:.")); s.addExampleQuery(QStringLiteral("kdevelop :q:")); addSyntax(s); setDefaultSyntax(Plasma::RunnerSyntax(QStringLiteral("kdevelop"), i18n("Lists all the KDevelop editor sessions in your account."))); } KDevelopSessions::~KDevelopSessions() = default; QStringList findSessions() { QStringList sessionDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/sessions"), QStandardPaths::LocateDirectory); QStringList sessionrcs; Q_FOREACH(const QString& dir, sessionDirs) { QDir d(dir); Q_FOREACH(const QString& sessionDir, d.entryList(QDir::Dirs)) { QDir sd(d.absoluteFilePath(sessionDir)); QString path(sd.filePath(QStringLiteral("sessionrc"))); if(QFile::exists(path)) { sessionrcs += path; } } } return sessionrcs; } void KDevelopSessions::loadSessions() { m_sessions.clear(); // Switch kdevelop session: -u // Should we add a match for this option or would that clutter the matches too much? const QStringList list = findSessions(); + m_sessions.reserve(list.size()); foreach (const QString &sessionfile, list) { Session session; session.id = sessionfile.section('/', -2, -2); KConfig cfg(sessionfile, KConfig::SimpleConfig); KConfigGroup group = cfg.group(QString()); - session.name = group.readEntry("SessionPrettyContents");; + session.name = group.readEntry("SessionPrettyContents"); m_sessions << session; } std::sort(m_sessions.begin(), m_sessions.end(), kdevelopsessions_runner_compare_sessions); } void KDevelopSessions::match(Plasma::RunnerContext &context) { if (m_sessions.isEmpty()) { return; } QString term = context.query(); if (term.length() < 3) { return; } bool listAll = false; if (term.startsWith(QStringLiteral("kdevelop"), Qt::CaseInsensitive)) { if (term.trimmed().compare(QStringLiteral("kdevelop"), Qt::CaseInsensitive) == 0) { listAll = true; term.clear(); } else if (term.at(8) == QLatin1Char(' ') ) { term.remove(QStringLiteral("kdevelop"), Qt::CaseInsensitive); term = term.trimmed(); } else { term.clear(); } } if (term.isEmpty() && !listAll) { return; } foreach (const Session &session, m_sessions) { if (!context.isValid()) { return; } if (listAll || (!term.isEmpty() && session.name.contains(term, Qt::CaseInsensitive))) { Plasma::QueryMatch match(this); if (listAll) { // All sessions listed, but with a low priority match.setType(Plasma::QueryMatch::ExactMatch); match.setRelevance(0.8); } else { if (session.name.compare(term, Qt::CaseInsensitive) == 0) { // parameter to kdevelop matches session exactly, bump it up! match.setType(Plasma::QueryMatch::ExactMatch); match.setRelevance(1.0); } else { // fuzzy match of the session in "kdevelop $session" match.setType(Plasma::QueryMatch::PossibleMatch); match.setRelevance(0.8); } } match.setIcon(m_icon); match.setData(session.id); match.setText(session.name); match.setSubtext(i18n("Open KDevelop Session")); context.addMatch(match); } } } void KDevelopSessions::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) { Q_UNUSED(context) QString sessionId = match.data().toString(); if (sessionId.isEmpty()) { qWarning() << "No KDevelop session id in match!"; return; } qDebug() << "Open KDevelop session" << sessionId; const QStringList args = {QStringLiteral("--open-session"), sessionId}; KToolInvocation::kdeinitExec(QStringLiteral("kdevelop"), args); } #include "kdevelopsessions.moc" diff --git a/file_templates/common/class_method_declaration_apidox_cpp.txt b/file_templates/common/class_method_declaration_apidox_cpp.txt index c60d8156d7..4d4e8369ad 100644 --- a/file_templates/common/class_method_declaration_apidox_cpp.txt +++ b/file_templates/common/class_method_declaration_apidox_cpp.txt @@ -1,34 +1,36 @@ {# Template for api dox in front of class method declaration #} {% load kdev_filters %} {% with method.arguments as arguments %} {# standard four spaces indentation to match context #} /** {% if method.isConstructor %} {% if not arguments %} * Default constructor {% else %} {% with arguments|first as argFirst %} {% if arguments|length == 1 and argFirst.type == method.name|arg_type %} - * Copy Constructor + * Copy constructor {% else %} * Constructor {% endif %} {% endwith %} {% endif %} {% elif method.isDestructor %} * Destructor + {% elif "operator=" == method.name %} + * Assignment operator {% else %} * @todo write docs {% endif %} {# and > or, so we go here if arguments or (returntype and not constructor/destructor) #} {% if arguments or method.returnType and not method.isConstructor and not method.isDestructor %} * {% endif %} {% for argument in arguments %} * @param {{ argument.name }} TODO {% endfor %} {% if method.returnType and not method.isConstructor and not method.isDestructor %} * @return TODO {% endif %} */ {% endwith %} diff --git a/kdevplatform/debugger/variable/variablecollection.cpp b/kdevplatform/debugger/variable/variablecollection.cpp index 62f1d5bc5a..18d1501930 100644 --- a/kdevplatform/debugger/variable/variablecollection.cpp +++ b/kdevplatform/debugger/variable/variablecollection.cpp @@ -1,552 +1,553 @@ /* * 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 "../../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 #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) , m_expression(expression) , m_inScope(true) , m_topLevel(true) , m_changed(false) , m_showError(false) , m_format(Natural) { // FIXME: should not duplicate the data, instead overload 'data' // and return expression_ directly. if (display.isEmpty()) - setData(QVector() << expression << QString() << QString()); + setData(QVector{expression, QString(), QString()}); else - setData(QVector() << display << QString() << QString()); + setData(QVector{display, QString(), QString()}); } QString Variable::expression() const { return m_expression; } bool Variable::inScope() const { return m_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) { m_topLevel = v; } void Variable::setInScope(bool v) { m_inScope = v; for (int i=0; i < childCount(); ++i) { if (Variable *var = qobject_cast(child(i))) { var->setInScope(v); } } reportChange(); } void Variable::setShowError (bool v) { m_showError = v; reportChange(); } bool Variable::showError() { return m_showError; } Variable::~Variable() { } void Variable::die() { removeSelf(); deleteLater(); } void Variable::setChanged(bool c) { m_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"); } 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 (m_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 (!m_inScope) { return scheme.foreground(KColorScheme::InactiveText).color(); } else if (isPotentialProblematicValue()) { return scheme.foreground(KColorScheme::NegativeText).color(); } else if (m_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_(nullptr) { - setData(QVector() << i18n("Auto") << QString()); + setData(QVector{i18n("Auto"), QString()}); } Variable* Watches::add(const QString& expression) { if (!hasStartedSession()) return nullptr; 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_ = nullptr; } } 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()); + setData(QVector{name, QString()}); } QList Locals::updateLocals(QStringList locals) { QSet existing, current; for (int i = 0; i < childItems.size(); i++) { Q_ASSERT(qobject_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; + ret.reserve(childItems.size()); foreach (TreeItem *i, childItems) { Q_ASSERT(qobject_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) , m_watches(new Watches(model, this)) { appendChild(m_watches, true); } Locals* VariablesRoot::locals(const QString& name) { if (!m_locals.contains(name)) { m_locals[name] = new Locals(model(), this, name); appendChild(m_locals[name]); } return m_locals[name]; } QHash VariablesRoot::allLocals() const { return m_locals; } void VariablesRoot::resetChanged() { m_watches->resetChanged(); foreach (Locals *l, m_locals) { l->resetChanged(); } } VariableCollection::VariableCollection(IDebugController* controller) : TreeModel({i18n("Name"), i18n("Value"), i18n("Type")}, controller) , m_widgetVisible(false) , m_textHintProvider(this) { m_universe = new VariablesRoot(this); setRootItem(m_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(&m_textHintProvider); } Locals* VariableCollection::locals(const QString &name) const { return m_universe->locals(name.isEmpty() ? i18n("Locals") : name); } 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->m_activeTooltip && m_collection->m_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->m_activeTooltip = new VariableToolTip(w, global+QPoint(30,30), expression); m_collection->m_activeTooltip->setHandleRect(KTextEditorHelpers::getItemBoundingRect(view, expressionRange)); return QString(); } } diff --git a/kdevplatform/debugger/variable/variabletooltip.cpp b/kdevplatform/debugger/variable/variabletooltip.cpp index 7e0dda79ce..23901ba9ea 100644 --- a/kdevplatform/debugger/variable/variabletooltip.cpp +++ b/kdevplatform/debugger/variable/variabletooltip.cpp @@ -1,213 +1,212 @@ /* * KDevelop Debugger Support * * 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 "variabletooltip.h" #include #include #include #include #include #include #include #include #include #include #include #include "variablecollection.h" #include "../util/treeview.h" #include "../interfaces/ivariablecontroller.h" #include "../../util/activetooltip.h" #include "../../interfaces/icore.h" #include "../../interfaces/idebugcontroller.h" namespace KDevelop { class SizeGrip : public QWidget { Q_OBJECT public: explicit SizeGrip(QWidget* parent) : QWidget(parent) { m_parent = parent; } protected: void paintEvent(QPaintEvent *) override { QPainter painter(this); QStyleOptionSizeGrip opt; opt.init(this); opt.corner = Qt::BottomRightCorner; style()->drawControl(QStyle::CE_SizeGrip, &opt, &painter, this); } void mousePressEvent(QMouseEvent* e) override { if (e->button() == Qt::LeftButton) { m_pos = e->globalPos(); m_startSize = m_parent->size(); e->ignore(); } } void mouseReleaseEvent(QMouseEvent*) override { m_pos = QPoint(); } void mouseMoveEvent(QMouseEvent* e) override { if (!m_pos.isNull()) { m_parent->resize( m_startSize.width() + (e->globalPos().x() - m_pos.x()), m_startSize.height() + (e->globalPos().y() - m_pos.y()) ); } } private: QWidget *m_parent; QSize m_startSize; QPoint m_pos; }; VariableToolTip::VariableToolTip(QWidget* parent, const QPoint& position, const QString& identifier) : ActiveToolTip(parent, position) { setPalette( QApplication::palette() ); - m_model = new TreeModel(QVector() << i18n("Name") << i18n("Value") << i18n("Type"), - this); + m_model = new TreeModel(QVector{i18n("Name"), i18n("Value"), i18n("Type")}, this); TooltipRoot* tr = new TooltipRoot(m_model); m_model->setRootItem(tr); m_var = ICore::self()->debugController()->currentSession()-> variableController()->createVariable( m_model, tr, identifier); tr->init(m_var); m_var->attachMaybe(this, "variableCreated"); QVBoxLayout* l = new QVBoxLayout(this); l->setContentsMargins(0, 0, 0, 0); // setup proxy model m_proxy = new QSortFilterProxyModel; m_view = new AsyncTreeView(m_model, m_proxy, this); m_proxy->setSourceModel(m_model); m_view->setModel(m_proxy); m_view->header()->resizeSection(0, 150); m_view->header()->resizeSection(1, 90); m_view->setSelectionBehavior(QAbstractItemView::SelectRows); m_view->setSelectionMode(QAbstractItemView::SingleSelection); m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); l->addWidget(m_view); m_itemHeight = m_view->indexRowSizeHint(m_model->indexForItem(m_var, 0)); connect(m_view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &VariableToolTip::slotRangeChanged); m_selection = m_view->selectionModel(); m_selection->select(m_model->indexForItem(m_var, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); QHBoxLayout* buttonBox = new QHBoxLayout(); buttonBox->setContentsMargins(11, 0, 11, 6); QPushButton* watchThisButton = new QPushButton(i18n("Watch This")); buttonBox->addWidget(watchThisButton); QPushButton* stopOnChangeButton = new QPushButton(i18n("Stop on Change")); buttonBox->addWidget(stopOnChangeButton); connect(watchThisButton, &QPushButton::clicked, this, [this](){ slotLinkActivated(QStringLiteral("add_watch")); }); connect(stopOnChangeButton, &QPushButton::clicked, this, [this](){ slotLinkActivated(QStringLiteral("add_watchpoint")); }); QHBoxLayout* inner = new QHBoxLayout(); l->addLayout(inner); inner->setContentsMargins(0, 0, 0, 0); inner->addLayout(buttonBox); inner->addStretch(); SizeGrip* g = new SizeGrip(this); g->setFixedSize(16, 16); inner->addWidget(g, 0, (Qt::Alignment)(Qt::AlignRight | Qt::AlignBottom)); move(position); } void VariableToolTip::variableCreated(bool hasValue) { m_view->resizeColumns(); if (hasValue) { ActiveToolTip::showToolTip(this, 0.0); } else { close(); } } void VariableToolTip::slotLinkActivated(const QString& link) { Variable* v = m_var; QItemSelection s = m_selection->selection(); if (!s.empty()) { QModelIndex index = s.front().topLeft(); const auto sourceIndex = m_proxy->mapToSource(index); TreeItem *item = m_model->itemForIndex(sourceIndex); if (item) { Variable* v2 = qobject_cast(item); if (v2) v = v2; } } IDebugSession *session = ICore::self()->debugController()->currentSession(); if (session && session->state() != IDebugSession::NotStartedState && session->state() != IDebugSession::EndedState) { if (link == QLatin1String("add_watch")) { session->variableController()->addWatch(v); } else if (link == QLatin1String("add_watchpoint")) { session->variableController()->addWatchpoint(v); } } close(); } void VariableToolTip::slotRangeChanged(int min, int max) { Q_ASSERT(min == 0); Q_UNUSED(min); QRect rect = QApplication::desktop()->screenGeometry(this); if (pos().y() + height() + max*m_itemHeight < rect.bottom()) resize(width(), height() + max*m_itemHeight); else { // Oh, well, I'm sorry, but here's the scrollbar you was // longing to see m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); } } } #include "variabletooltip.moc" diff --git a/kdevplatform/language/classmodel/classmodel.cpp b/kdevplatform/language/classmodel/classmodel.cpp index ce4a325bc8..cff19b99be 100644 --- a/kdevplatform/language/classmodel/classmodel.cpp +++ b/kdevplatform/language/classmodel/classmodel.cpp @@ -1,282 +1,283 @@ /* * KDevelop Class Browser * * Copyright 2007-2008 Hamish Rodda * Copyright 2009 Lior Mualem * * 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 "classmodel.h" #include "classmodelnode.h" #include "allclassesfolder.h" #include "projectfolder.h" #include "../duchain/declaration.h" #include #include "../../interfaces/icore.h" #include "../../interfaces/iproject.h" #include "../../interfaces/iprojectcontroller.h" using namespace KDevelop; using namespace ClassModelNodes; ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// NodesModelInterface::~NodesModelInterface() { } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ClassModel::ClassModel() : m_features(NodesModelInterface::AllProjectsClasses | NodesModelInterface::BaseAndDerivedClasses | NodesModelInterface::ClassInternals) { m_topNode = new FolderNode(QStringLiteral("Top Node"), this); if ( features().testFlag(NodesModelInterface::AllProjectsClasses) ) { m_allClassesNode = new FilteredAllClassesFolder(this); m_topNode->addNode( m_allClassesNode ); } connect(ICore::self()->projectController(), &IProjectController::projectClosing, this, &ClassModel::removeProjectNode); connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, &ClassModel::addProjectNode); foreach ( IProject* project, ICore::self()->projectController()->projects() ) { addProjectNode(project); } } ClassModel::~ClassModel() { delete m_topNode; } void ClassModel::updateFilterString(const QString& a_newFilterString) { m_allClassesNode->updateFilterString(a_newFilterString); foreach ( ClassModelNodes::FilteredProjectFolder* folder, m_projectNodes ) { folder->updateFilterString(a_newFilterString); } } void ClassModel::collapsed(const QModelIndex& index) { Node* node = static_cast(index.internalPointer()); node->collapse(); } void ClassModel::expanded(const QModelIndex& index) { Node* node = static_cast(index.internalPointer()); node->expand(); } QFlags< Qt::ItemFlag > ClassModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } int ClassModel::rowCount(const QModelIndex& parent) const { Node* node = m_topNode; if ( parent.isValid() ) node = static_cast(parent.internalPointer()); return node->getChildren().size(); } QVariant ClassModel::data(const QModelIndex& index, int role) const { if ( !index.isValid() ) return QVariant(); Node* node = static_cast(index.internalPointer()); if ( role == Qt::DisplayRole ) return node->displayName(); if ( role == Qt::DecorationRole ) { QIcon icon = node->getCachedIcon(); return icon.isNull() ? QVariant() : icon; } return QVariant(); } QVariant ClassModel::headerData(int, Qt::Orientation, int role) const { if ( role == Qt::DisplayRole ) return QStringLiteral("Class"); return QVariant(); } int ClassModel::columnCount(const QModelIndex&) const { return 1; } bool ClassModel::hasChildren(const QModelIndex& parent) const { if ( !parent.isValid() ) return true; Node* node = static_cast(parent.internalPointer()); return node->hasChildren(); } QModelIndex ClassModel::index(int row, int column, const QModelIndex& parent) const { if (row < 0 || column != 0) return QModelIndex(); Node* node = m_topNode; if ( parent.isValid() ) node = static_cast(parent.internalPointer()); if ( row >= node->getChildren().size() ) return QModelIndex(); return index(node->getChildren()[row]); } QModelIndex ClassModel::parent(const QModelIndex& childIndex) const { if ( !childIndex.isValid() ) return QModelIndex(); Node* childNode = static_cast(childIndex.internalPointer()); if ( childNode->getParent() == m_topNode ) return QModelIndex(); return index( childNode->getParent() ); } QModelIndex ClassModel::index(ClassModelNodes::Node* a_node) const { if (!a_node) { return QModelIndex(); } // If no parent exists, we have an invalid index (root node or not part of a model). if ( a_node->getParent() == nullptr ) return QModelIndex(); return createIndex(a_node->row(), 0, a_node); } KDevelop::DUChainBase* ClassModel::duObjectForIndex(const QModelIndex& a_index) { if ( !a_index.isValid() ) return nullptr; Node* node = static_cast(a_index.internalPointer()); if ( IdentifierNode* identifierNode = dynamic_cast(node) ) return identifierNode->getDeclaration(); // Non was found. return nullptr; } QModelIndex ClassModel::getIndexForIdentifier(const KDevelop::IndexedQualifiedIdentifier& a_id) { ClassNode* node = m_allClassesNode->findClassNode(a_id); if ( node == nullptr ) return QModelIndex(); return index(node); } void ClassModel::nodesLayoutAboutToBeChanged(ClassModelNodes::Node*) { emit layoutAboutToBeChanged(); } void ClassModel::nodesLayoutChanged(ClassModelNodes::Node*) { QModelIndexList oldIndexList = persistentIndexList(); QModelIndexList newIndexList; + newIndexList.reserve(oldIndexList.size()); foreach(const QModelIndex& oldIndex, oldIndexList) { Node* node = static_cast(oldIndex.internalPointer()); if ( node ) { // Re-map the index. newIndexList << createIndex(node->row(), 0, node); } else newIndexList << oldIndex; } changePersistentIndexList(oldIndexList, newIndexList); emit layoutChanged(); } void ClassModel::nodesAboutToBeRemoved(ClassModelNodes::Node* a_parent, int a_first, int a_last) { beginRemoveRows(index(a_parent), a_first, a_last); } void ClassModel::nodesRemoved(ClassModelNodes::Node*) { endRemoveRows(); } void ClassModel::nodesAboutToBeAdded(ClassModelNodes::Node* a_parent, int a_pos, int a_size) { beginInsertRows(index(a_parent), a_pos, a_pos + a_size - 1); } void ClassModel::nodesAdded(ClassModelNodes::Node*) { endInsertRows(); } void ClassModel::addProjectNode( IProject* project ) { m_projectNodes[project] = new ClassModelNodes::FilteredProjectFolder(this, project); nodesLayoutAboutToBeChanged(m_projectNodes[project]); m_topNode->addNode(m_projectNodes[project]); nodesLayoutChanged(m_projectNodes[project]); } void ClassModel::removeProjectNode( IProject* project ) { m_topNode->removeNode(m_projectNodes[project]); m_projectNodes.remove(project); } // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on diff --git a/kdevplatform/language/codecompletion/codecompletionitemgrouper.h b/kdevplatform/language/codecompletion/codecompletionitemgrouper.h index 1ead5de5a9..1a0fd44b28 100644 --- a/kdevplatform/language/codecompletion/codecompletionitemgrouper.h +++ b/kdevplatform/language/codecompletion/codecompletionitemgrouper.h @@ -1,112 +1,114 @@ /* * KDevelop Generic Code Completion Support * * Copyright 2006-2008 Hamish Rodda * Copyright 2007-2008 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_CODECOMPLETIONITEMGROUPER_H #define KDEVPLATFORM_CODECOMPLETIONITEMGROUPER_H #include "codecompletionmodel.h" #include "codecompletionitem.h" #include #include namespace KDevelop { ///Always the last item of a grouping chain: Only inserts the items struct CodeCompletionItemLastGrouper { CodeCompletionItemLastGrouper(QList >& tree, CompletionTreeNode* parent, QList items) { + tree.reserve(tree.size() + items.size()); foreach( CompletionTreeItemPointer item, items ) { item->setParent(parent); tree << QExplicitlySharedDataPointer( item.data() ); } } }; ///Helper class that helps us grouping the completion-list. A chain of groupers can be built, by using NextGrouper. template struct CodeCompletionItemGrouper { typedef typename KeyExtractor::KeyType KeyType; CodeCompletionItemGrouper(QList >& tree, CompletionTreeNode* parent, QList items) { typedef QMap > GroupMap; GroupMap groups; foreach(const CompletionTreeItemPointer& item, items) { KeyType key = KeyExtractor::extract(item); typename GroupMap::iterator it = groups.find(key); if(it == groups.end()) it = groups.insert(key, QList()); (*it).append(item); } + tree.reserve(tree.size() + groups.size()); for( typename GroupMap::const_iterator it = groups.constBegin(); it != groups.constEnd(); ++it ) { QExplicitlySharedDataPointer node(new CompletionTreeNode()); node->setParent(parent); node->role = (KTextEditor::CodeCompletionModel::ExtraItemDataRoles)KeyExtractor::Role; node->roleValue = QVariant(it.key()); tree << QExplicitlySharedDataPointer( node.data() ); NextGrouper nextGrouper(node->children, node.data(), *it); } } }; ///Extracts the argument-hint depth from completion-items, to be used in ItemGrouper for grouping by argument-hint depth. struct ArgumentHintDepthExtractor { typedef int KeyType; enum { Role = KTextEditor::CodeCompletionModel::ArgumentHintDepth }; static KeyType extract( const CompletionTreeItemPointer& item ) { return item->argumentHintDepth(); } }; struct InheritanceDepthExtractor { typedef int KeyType; enum { Role = KTextEditor::CodeCompletionModel::InheritanceDepth }; static KeyType extract( const CompletionTreeItemPointer& item ) { return item->inheritanceDepth(); } }; struct SimplifiedAttributesExtractor { typedef int KeyType; enum { Role = KTextEditor::CodeCompletionModel::CompletionRole }; static const int groupingProperties; static KeyType extract( const CompletionTreeItemPointer& item ) { DUChainReadLocker lock(DUChain::lock()); return item->completionProperties() & groupingProperties; } }; } #endif // KDEVPLATFORM_CODECOMPLETIONITEMGROUPER_H diff --git a/kdevplatform/language/codecompletion/codecompletiontesthelper.h b/kdevplatform/language/codecompletion/codecompletiontesthelper.h index 154cda96be..e8f70e7cd3 100644 --- a/kdevplatform/language/codecompletion/codecompletiontesthelper.h +++ b/kdevplatform/language/codecompletion/codecompletiontesthelper.h @@ -1,234 +1,235 @@ /* This file is part of KDevelop Copyright 2006 Hamish Rodda Copyright 2007-2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H #define KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H #include #include #include "../duchain/declaration.h" #include "../duchain/duchain.h" #include "codecompletionitem.h" #include #include #include #include using namespace KTextEditor; using namespace KDevelop; /** * Helper-class for testing completion-items * Just initialize it with the context and the text, and then use the members, for simple cases only "names" * the template parameter is your language specific CodeCompletionContext */ template struct CodeCompletionItemTester { using Element = QExplicitlySharedDataPointer; using Item = QExplicitlySharedDataPointer; using Context = QExplicitlySharedDataPointer; //Standard constructor CodeCompletionItemTester(DUContext* context, const QString& text = "; ", const QString& followingText = QString(), const CursorInRevision& position = CursorInRevision::invalid()) : completionContext(new T(DUContextPointer(context), text, followingText, position.isValid() ? position : context->range().end)) { init(); } //Can be used if you already have the completion context CodeCompletionItemTester(const Context& context) : completionContext(context) { init(); } //Creates a CodeCompletionItemTester for the parent context CodeCompletionItemTester parent() const { Context parent = Context(dynamic_cast(completionContext->parentContext())); Q_ASSERT(parent); return CodeCompletionItemTester(parent); } void addElements(const QList& elements) { foreach(Element element, elements) { Item item(dynamic_cast(element.data())); if(item) items << item; CompletionTreeNode* node = dynamic_cast(element.data()); if(node) addElements(node->children); } } bool containsDeclaration(Declaration* dec) const { foreach(Item item, items) { if (item->declaration().data() == dec) { return true; } } return false; } QList items; // All items retrieved QStringList names; // Names of all completion-items Context completionContext; //Convenience-function to retrieve data from completion-items by name QVariant itemData(QString itemName, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { return itemData(names.indexOf(itemName), column, role); } QVariant itemData(int itemNumber, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { if(itemNumber < 0 || itemNumber >= items.size()) return QVariant(); return itemData(items[itemNumber], column, role); } QVariant itemData(Item item, int column = KTextEditor::CodeCompletionModel::Name, int role = Qt::DisplayRole) const { return item->data(fakeModel().index(0, column), role, nullptr); } Item findItem(const QString& itemName) const { const auto idx = names.indexOf(itemName); if (idx < 0) { return {}; } return items[idx]; } private: void init() { if ( !completionContext || !completionContext->isValid() ) { qWarning() << "invalid completion context"; return; } bool abort = false; items = completionContext->completionItems(abort); addElements(completionContext->ungroupedElements()); + names.reserve(items.size()); foreach(Item i, items) { names << i->data(fakeModel().index(0, KTextEditor::CodeCompletionModel::Name), Qt::DisplayRole, nullptr).toString(); } } static QStandardItemModel& fakeModel() { static QStandardItemModel model; model.setColumnCount(10); model.setRowCount(10); return model; } }; /** * Helper class that inserts the given text into the duchain under the specified name, * allows parsing it with a simple call to parse(), and automatically releases the top-context * * The duchain must not be locked when this object is destroyed */ struct InsertIntoDUChain { ///Artificially inserts a file called @p name with the text @p text InsertIntoDUChain(const QString& name, const QString& text) : m_insertedCode(IndexedString(name), text), m_topContext(nullptr) { } ~InsertIntoDUChain() { get(); release(); } ///The duchain must not be locked when this is called void release() { if(m_topContext) { DUChainWriteLocker lock; m_topContext = nullptr; QList< TopDUContext* > chains = DUChain::self()->chainsForDocument(m_insertedCode.file()); foreach(TopDUContext* top, chains) DUChain::self()->removeDocumentChain(top); } } TopDUContext* operator->() { get(); return m_topContext.data(); } TopDUContext* tryGet() { DUChainReadLocker lock; return DUChain::self()->chainForDocument(m_insertedCode.file(), false); } void get() { if(!m_topContext) m_topContext = tryGet(); } ///Helper function: get a declaration based on its qualified identifier Declaration* getDeclaration(const QString& id) { get(); if(!topContext()) return nullptr; return DeclarationId(IndexedQualifiedIdentifier(QualifiedIdentifier(id))).getDeclaration(topContext()); } TopDUContext* topContext() { return m_topContext.data(); } /** * Parses this inserted code as a stand-alone top-context * The duchain must not be locked when this is called * * @param features The features that should be requested for the top-context * @param update Whether the top-context should be updated if it already exists. Else it will be deleted. */ void parse(uint features = TopDUContext::AllDeclarationsContextsAndUses, bool update = false) { if(!update) release(); m_topContext = DUChain::self()->waitForUpdate(m_insertedCode.file(), (TopDUContext::Features)features, false); Q_ASSERT(m_topContext); DUChainReadLocker lock; Q_ASSERT(!m_topContext->parsingEnvironmentFile()->isProxyContext()); } InsertArtificialCodeRepresentation m_insertedCode; ReferencedTopDUContext m_topContext; }; #endif // KDEVPLATFORM_CODECOMPLETIONTESTHELPER_H diff --git a/kdevplatform/language/codegen/applychangeswidget.cpp b/kdevplatform/language/codegen/applychangeswidget.cpp index 35f924bd22..a842743108 100644 --- a/kdevplatform/language/codegen/applychangeswidget.cpp +++ b/kdevplatform/language/codegen/applychangeswidget.cpp @@ -1,203 +1,203 @@ /* Copyright 2008 Aleix Pol * Copyright 2009 Ramón Zarazúa * * 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 "applychangeswidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "coderepresentation.h" #include #include namespace KDevelop { class ApplyChangesWidgetPrivate { public: explicit ApplyChangesWidgetPrivate(ApplyChangesWidget * p) : parent(p), m_index(0) {} ~ApplyChangesWidgetPrivate() { qDeleteAll(m_temps); } void createEditPart(const KDevelop::IndexedString& url); ApplyChangesWidget * const parent; int m_index; QList m_editParts; QList m_temps; QList m_files; QTabWidget * m_documentTabs; QLabel* m_info; }; ApplyChangesWidget::ApplyChangesWidget(QWidget* parent) : QDialog(parent), d(new ApplyChangesWidgetPrivate(this)) { setSizeGripEnabled(true); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto mainLayout = new QVBoxLayout(this); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &ApplyChangesWidget::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ApplyChangesWidget::reject); QWidget* w=new QWidget(this); d->m_info=new QLabel(w); d->m_documentTabs = new QTabWidget(w); connect(d->m_documentTabs, &QTabWidget::currentChanged, this, &ApplyChangesWidget::indexChanged); QVBoxLayout* l = new QVBoxLayout(w); l->addWidget(d->m_info); l->addWidget(d->m_documentTabs); mainLayout->addWidget(w); mainLayout->addWidget(buttonBox); resize(QSize(800, 400)); } ApplyChangesWidget::~ApplyChangesWidget() = default; bool ApplyChangesWidget::hasDocuments() const { return d->m_editParts.size() > 0; } KTextEditor::Document* ApplyChangesWidget::document() const { return qobject_cast(d->m_editParts[d->m_index]); } void ApplyChangesWidget::setInformation(const QString & info) { d->m_info->setText(info); } void ApplyChangesWidget::addDocuments(const IndexedString & original) { int idx=d->m_files.indexOf(original); if(idx<0) { QWidget * w = new QWidget; d->m_documentTabs->addTab(w, original.str()); d->m_documentTabs->setCurrentWidget(w); d->m_files.insert(d->m_index, original); d->createEditPart(original); } else { d->m_index=idx; } } bool ApplyChangesWidget::applyAllChanges() { /// @todo implement safeguard in case a file saving fails bool ret = true; for(int i = 0; i < d->m_files.size(); ++i ) if(d->m_editParts[i]->saveAs(d->m_files[i].toUrl())) { IDocument* doc = ICore::self()->documentController()->documentForUrl(d->m_files[i].toUrl()); if(doc && doc->state()==IDocument::Dirty) doc->reload(); } else ret = false; return ret; } } namespace KDevelop { void ApplyChangesWidgetPrivate::createEditPart(const IndexedString & file) { QWidget * widget = m_documentTabs->currentWidget(); Q_ASSERT(widget); QVBoxLayout *m=new QVBoxLayout(widget); QSplitter *v=new QSplitter(widget); m->addWidget(v); QUrl url = file.toUrl(); QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(url); KParts::ReadWritePart* part=KMimeTypeTrader::self()->createPartInstanceFromQuery(mimetype.name(), widget, widget); KTextEditor::Document* document=qobject_cast(part); Q_ASSERT(document); Q_ASSERT(document->action("file_save")); document->action("file_save")->setEnabled(false); m_editParts.insert(m_index, part); //Open the best code representation, even if it is artificial CodeRepresentation::Ptr repr = createCodeRepresentation(file); if(!repr->fileExists()) { const QString templateName = QDir::tempPath() + QLatin1Char('/') + url.fileName().split(QLatin1Char('.')).last(); QTemporaryFile * temp(new QTemporaryFile(templateName)); temp->open(); temp->write(repr->text().toUtf8()); temp->close(); url = QUrl::fromLocalFile(temp->fileName()); m_temps << temp; } m_editParts[m_index]->openUrl(url); v->addWidget(m_editParts[m_index]->widget()); - v->setSizes(QList() << 400 << 100); + v->setSizes(QList{400, 100}); } void ApplyChangesWidget::indexChanged(int newIndex) { Q_ASSERT(newIndex != -1); d->m_index = newIndex; } void ApplyChangesWidget::updateDiffView(int index) { d->m_index = index == -1 ? d->m_index : index; } } diff --git a/kdevplatform/language/codegen/codedescription.cpp b/kdevplatform/language/codegen/codedescription.cpp index 401df9d28d..3a942b7cb7 100644 --- a/kdevplatform/language/codegen/codedescription.cpp +++ b/kdevplatform/language/codegen/codedescription.cpp @@ -1,185 +1,186 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "codedescription.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; /** * The access policy as a string, or an empty string * if the policy is set to default * * The DUChain must be locked when calling this function **/ QString accessPolicyName(const DeclarationPointer& declaration) { DUChainPointer member = declaration.dynamicCast(); if (member) { switch (member->accessPolicy()) { case Declaration::Private: return QStringLiteral("private"); case Declaration::Protected: return QStringLiteral("protected"); case Declaration::Public: return QStringLiteral("public"); default: break; } } return QString(); } VariableDescription::VariableDescription() { } VariableDescription::VariableDescription(const QString& type, const QString& name) : name(name) , type(type) { } VariableDescription::VariableDescription(const DeclarationPointer& declaration) { DUChainReadLocker lock; if (declaration) { name = declaration->identifier().toString(); if (auto abstractType = declaration->abstractType()) { type = abstractType->toString(); } } access = accessPolicyName(declaration); } FunctionDescription::FunctionDescription() : FunctionDescription::FunctionDescription({}, {}, {}) { } FunctionDescription::FunctionDescription(const QString& name, const VariableDescriptionList& arguments, const VariableDescriptionList& returnArguments) : name(name) , arguments(arguments) , returnArguments(returnArguments) , isConstructor(false) , isDestructor(false) , isVirtual(false) , isStatic(false) , isSlot(false) , isSignal(false) , isConst(false) { } FunctionDescription::FunctionDescription(const DeclarationPointer& declaration) : FunctionDescription::FunctionDescription({}, {}, {}) { DUChainReadLocker lock; if (declaration) { name = declaration->identifier().toString(); DUContext* context = declaration->internalContext(); DUChainPointer function = declaration.dynamicCast(); if (function) { context = DUChainUtils::getArgumentContext(declaration.data()); } DUChainPointer method = declaration.dynamicCast(); if (method) { isConstructor = method->isConstructor(); isDestructor = method->isDestructor(); isVirtual = method->isVirtual(); isAbstract = method->isAbstract(); isFinal = method->isFinal(); isOverriding = (DUChainUtils::getOverridden(method.data()) != nullptr); isStatic = method->isStatic(); isSlot = method->isSlot(); isSignal = method->isSignal(); } int i = 0; - foreach (Declaration* arg, context->localDeclarations()) - { + const auto localDeclarations = context->localDeclarations(); + arguments.reserve(localDeclarations.size()); + for (Declaration* arg : localDeclarations) { VariableDescription var = VariableDescription(DeclarationPointer(arg)); if (function) { var.value = function->defaultParameterForArgument(i).str(); qCDebug(LANGUAGE) << var.name << var.value; } arguments << var; ++i; } FunctionType::Ptr functionType = declaration->abstractType().cast(); if (functionType) { isConst = (functionType->modifiers() & AbstractType::ConstModifier); } if (functionType && functionType->returnType()) { returnArguments << VariableDescription(functionType->returnType()->toString(), QString()); } access = accessPolicyName(declaration); } } QString FunctionDescription::returnType() const { if (returnArguments.isEmpty()) { return QString(); } return returnArguments.first().type; } ClassDescription::ClassDescription() { } ClassDescription::ClassDescription(const QString& name) : name(name) { } diff --git a/kdevplatform/language/codegen/codedescription.h b/kdevplatform/language/codegen/codedescription.h index 319752f25f..fd31db0490 100644 --- a/kdevplatform/language/codegen/codedescription.h +++ b/kdevplatform/language/codegen/codedescription.h @@ -1,284 +1,285 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_CODEDESCRIPTION_H #define KDEVPLATFORM_CODEDESCRIPTION_H #include #include #include #include #include /** * NOTE: changes in this file will quite probably also require changes * in codedescriptionmetatype.h! */ namespace KDevelop { /** * @brief Represents a variable * * A variable has two main properties: its type and name. **/ struct KDEVPLATFORMLANGUAGE_EXPORT VariableDescription { /** * Creates a variable with no type and no name * **/ VariableDescription(); /** * Creates a variable with type @p type and name @p name * * @param type the type of this variable * @param name the name of this variable **/ VariableDescription(const QString& type, const QString& name); /** * Creates a variable and determines it type and name from the @p declaration * **/ explicit VariableDescription(const DeclarationPointer& declaration); /** * The name of this variable **/ QString name; /** * The type of this variable. * * In weakly typed languages, this field can be empty. **/ QString type; /** * Access specifier, only relevant for class members. * * Not all languages use these, so it can be left empty. **/ QString access; /** * The default value of this variable. */ QString value; }; /** * List of variable descriptions **/ typedef QVector VariableDescriptionList; /** * @brief Represents a function * * A function has a name and any number of arguments and return values **/ struct KDEVPLATFORMLANGUAGE_EXPORT FunctionDescription { /** * Creates a function with no name and no arguments * **/ FunctionDescription(); /** * Creates a function with name @p and specified @p arguments and @p returnArguments * * @param name the name of the new function * @param arguments a list of variables that are passed to this function as arguments * @param returnArguments a list of variables that this function returns **/ FunctionDescription(const QString& name, const VariableDescriptionList& arguments, const VariableDescriptionList& returnArguments); /** * Creates a function and determines its properties from the @p declaration * * @param declaration a function declaration **/ explicit FunctionDescription(const DeclarationPointer& declaration); /** * Convenience method, returns the type of the first variable in returnArguments * or an empty string if this function has no return arguments */ QString returnType() const; /** * The name of this function **/ QString name; /** * This function's input arguments **/ QVector arguments; /** * This function's return values **/ QVector returnArguments; /** * Access specifier, only relevant for class members. * * Not all languages use these, so it can be left empty. **/ QString access; /** * Specifies whether this function is a class constructor **/ bool isConstructor : 1; /** * Specifies whether this function is a class destructor */ bool isDestructor : 1; /** * Specifies whether this function is virtual and can be overridden by subclasses **/ bool isVirtual : 1; /** * Specifies whether this function is abstract and needs to be overridden by subclasses **/ bool isAbstract : 1; /** * Specifies whether this function overrides a virtual method of a base class **/ bool isOverriding : 1; /** * Specifies whether this function is final and cannot be overridden by subclasses **/ bool isFinal : 1; /** * Specifies whether this function is static and can be called without a class instance **/ bool isStatic : 1; /** * Specifies whether this function is a slot **/ bool isSlot : 1; /** * Specifies whether this function is a signal **/ bool isSignal : 1; /** * Specifies whether this function is constant **/ bool isConst : 1; }; /** * List of function descriptions **/ typedef QVector FunctionDescriptionList; /** * Description of an inheritance relation. **/ struct KDEVPLATFORMLANGUAGE_EXPORT InheritanceDescription { /** * @brief The mode of this inheritance. * * For C++ classes, mode string are the same as access specifiers (public, protected, private). * In other languages, the mode is used to differentiate between extends/implements * or other possible inheritance types. * * Some languages do not recognise distinct inheritance modes at all. **/ QString inheritanceMode; /** * The name of the base class **/ QString baseType; }; /** * List of inheritance descriptions **/ typedef QVector InheritanceDescriptionList; /** * @brief Represents a class * * A class descriptions stores its name, its member variables and functions, as well as its superclasses and inheritance types. **/ struct KDEVPLATFORMLANGUAGE_EXPORT ClassDescription { /** * Creates an empty class * **/ ClassDescription(); /** * Creates an empty class named @p name * * @param name the name of the new class **/ explicit ClassDescription(const QString& name); /** * The name of this class **/ QString name; /** * List of base classes (classes from which this one inherits) as well as inheritance types **/ InheritanceDescriptionList baseClasses; /** * List of all member variables in this class **/ VariableDescriptionList members; /** * List of all member functions (methods) in this class **/ FunctionDescriptionList methods; }; namespace CodeDescription { template QVariant toVariantList(const QVector& list) { QVariantList ret; + ret.reserve(list.size()); foreach (const T& t, list) { ret << QVariant::fromValue(t); } return QVariant::fromValue(ret); } } } Q_DECLARE_TYPEINFO(KDevelop::VariableDescription, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(KDevelop::FunctionDescription, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(KDevelop::InheritanceDescription, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(KDevelop::ClassDescription, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(KDevelop::VariableDescription) Q_DECLARE_METATYPE(KDevelop::VariableDescriptionList) Q_DECLARE_METATYPE(KDevelop::FunctionDescription) Q_DECLARE_METATYPE(KDevelop::FunctionDescriptionList) Q_DECLARE_METATYPE(KDevelop::InheritanceDescription) Q_DECLARE_METATYPE(KDevelop::InheritanceDescriptionList) Q_DECLARE_METATYPE(KDevelop::ClassDescription) #endif // KDEVPLATFORM_CODEDESCRIPTION_H diff --git a/kdevplatform/language/codegen/documentchangeset.cpp b/kdevplatform/language/codegen/documentchangeset.cpp index 425422c945..16c8c30c4b 100644 --- a/kdevplatform/language/codegen/documentchangeset.cpp +++ b/kdevplatform/language/codegen/documentchangeset.cpp @@ -1,591 +1,596 @@ /* 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 "documentchangeset.h" #include "coderepresentation.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KDevelop { typedef QList ChangesList; typedef QHash ChangesHash; class DocumentChangeSetPrivate { public: DocumentChangeSet::ReplacementPolicy replacePolicy; DocumentChangeSet::FormatPolicy formatPolicy; DocumentChangeSet::DUChainUpdateHandling updatePolicy; DocumentChangeSet::ActivationPolicy activationPolicy; ChangesHash changes; QHash documentsRename; DocumentChangeSet::ChangeResult addChange(const DocumentChangePointer& change); DocumentChangeSet::ChangeResult replaceOldText(CodeRepresentation* repr, const QString& newText, const ChangesList& sortedChangesList); DocumentChangeSet::ChangeResult generateNewText(const IndexedString& file, ChangesList& sortedChanges, const CodeRepresentation* repr, QString& output); DocumentChangeSet::ChangeResult removeDuplicates(const IndexedString& file, ChangesList& filteredChanges); void formatChanges(); void updateFiles(); }; // Simple helpers to clear up code clutter namespace { inline bool changeIsValid(const DocumentChange& change, const QStringList& textLines) { return change.m_range.start() <= change.m_range.end() && change.m_range.end().line() < textLines.size() && change.m_range.start().line() >= 0 && change.m_range.start().column() >= 0 && change.m_range.start().column() <= textLines[change.m_range.start().line()].length() && change.m_range.end().column() >= 0 && change.m_range.end().column() <= textLines[change.m_range.end().line()].length(); } inline bool duplicateChanges(const DocumentChangePointer& previous, const DocumentChangePointer& current) { // Given the option of considering a duplicate two changes in the same range // but with different old texts to be ignored return previous->m_range == current->m_range && previous->m_newText == current->m_newText && (previous->m_oldText == current->m_oldText || (previous->m_ignoreOldText && current->m_ignoreOldText)); } inline QString rangeText(const KTextEditor::Range& range, const QStringList& textLines) { QStringList ret; ret.reserve(range.end().line() - range.start().line() + 1); for(int line = range.start().line(); line <= range.end().line(); ++line) { const QString lineText = textLines.at(line); int startColumn = 0; int endColumn = lineText.length(); if (line == range.start().line()) { startColumn = range.start().column(); } if (line == range.end().line()) { endColumn = range.end().column(); } ret << lineText.mid(startColumn, endColumn - startColumn); } return ret.join(QLatin1Char('\n')); } // need to have it as otherwise the arguments can exceed the maximum of 10 static QString printRange(const KTextEditor::Range& r) { return i18nc("text range line:column->line:column", "%1:%2->%3:%4", r.start().line(), r.start().column(), r.end().line(), r.end().column()); } } DocumentChangeSet::DocumentChangeSet() : d(new DocumentChangeSetPrivate) { d->replacePolicy = StopOnFailedChange; d->formatPolicy = AutoFormatChanges; d->updatePolicy = SimpleUpdate; d->activationPolicy = DoNotActivate; } DocumentChangeSet::DocumentChangeSet(const DocumentChangeSet& rhs) : d(new DocumentChangeSetPrivate(*rhs.d)) { } DocumentChangeSet& DocumentChangeSet::operator=(const DocumentChangeSet& rhs) { *d = *rhs.d; return *this; } DocumentChangeSet::~DocumentChangeSet() = default; DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChange& change) { return d->addChange(DocumentChangePointer(new DocumentChange(change))); } DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChangePointer& change) { return d->addChange(change); } DocumentChangeSet::ChangeResult DocumentChangeSet::addDocumentRenameChange(const IndexedString& oldFile, const IndexedString& newname) { d->documentsRename.insert(oldFile, newname); return DocumentChangeSet::ChangeResult::successfulResult(); } DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::addChange(const DocumentChangePointer& change) { changes[change->m_document].append(change); return DocumentChangeSet::ChangeResult::successfulResult(); } void DocumentChangeSet::setReplacementPolicy(DocumentChangeSet::ReplacementPolicy policy) { d->replacePolicy = policy; } void DocumentChangeSet::setFormatPolicy(DocumentChangeSet::FormatPolicy policy) { d->formatPolicy = policy; } void DocumentChangeSet::setUpdateHandling(DocumentChangeSet::DUChainUpdateHandling policy) { d->updatePolicy = policy; } void DocumentChangeSet::setActivationPolicy(DocumentChangeSet::ActivationPolicy policy) { d->activationPolicy = policy; } DocumentChangeSet::ChangeResult DocumentChangeSet::applyAllChanges() { QUrl oldActiveDoc; if (IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) { oldActiveDoc = activeDoc->url(); } QList allFiles; - foreach (IndexedString file, d->documentsRename.keys().toSet() + d->changes.keys().toSet()) { + const auto changedFiles = d->documentsRename.keys().toSet() + d->changes.keys().toSet(); + allFiles.reserve(changedFiles.size()); + for (const IndexedString& file : changedFiles) { allFiles << file.toUrl(); } if (!KDevelop::ensureWritable(allFiles)) { return ChangeResult(QStringLiteral("some affected files are not writable")); } // rename files QHash::const_iterator it = d->documentsRename.constBegin(); for(; it != d->documentsRename.constEnd(); ++it) { QUrl url = it.key().toUrl(); IProject* p = ICore::self()->projectController()->findProjectForUrl(url); if(p) { QList files = p->filesForPath(it.key()); if(!files.isEmpty()) { ProjectBaseItem::RenameStatus renamed = files.first()->rename(it.value().str()); if(renamed == ProjectBaseItem::RenameOk) { const QUrl newUrl = Path(Path(url).parent(), it.value().str()).toUrl(); if (url == oldActiveDoc) { oldActiveDoc = newUrl; } IndexedString idxNewDoc(newUrl); // ensure changes operate on new file name ChangesHash::iterator iter = d->changes.find(it.key()); if (iter != d->changes.end()) { // copy changes ChangesList value = iter.value(); // remove old entry d->changes.erase(iter); // adapt to new url ChangesList::iterator itChange = value.begin(); ChangesList::iterator itEnd = value.end(); for(; itChange != itEnd; ++itChange) { (*itChange)->m_document = idxNewDoc; } d->changes[idxNewDoc] = value; } } else { ///FIXME: share code with project manager for the error code string representation return ChangeResult(i18n("Could not rename '%1' to '%2'", url.toDisplayString(QUrl::PreferLocalFile), it.value().str())); } } else { //TODO: do it outside the project management? qCWarning(LANGUAGE) << "tried to rename file not tracked by project - not implemented"; } } else { qCWarning(LANGUAGE) << "tried to rename a file outside of a project - not implemented"; } } QMap codeRepresentations; QMap newTexts; ChangesHash filteredSortedChanges; ChangeResult result = ChangeResult::successfulResult(); QList files(d->changes.keys()); foreach(const IndexedString &file, files) { CodeRepresentation::Ptr repr = createCodeRepresentation(file); if(!repr) { return ChangeResult(QStringLiteral("Could not create a Representation for %1").arg(file.str())); } codeRepresentations[file] = repr; QList& sortedChangesList(filteredSortedChanges[file]); { result = d->removeDuplicates(file, sortedChangesList); if(!result) return result; } { result = d->generateNewText(file, sortedChangesList, repr.data(), newTexts[file]); if(!result) return result; } } QMap oldTexts; //Apply the changes to the files foreach(const IndexedString &file, files) { oldTexts[file] = codeRepresentations[file]->text(); result = d->replaceOldText(codeRepresentations[file].data(), newTexts[file], filteredSortedChanges[file]); if(!result && d->replacePolicy == StopOnFailedChange) { //Revert all files foreach(const IndexedString &revertFile, oldTexts.keys()) { codeRepresentations[revertFile]->setText(oldTexts[revertFile]); } return result; } } d->updateFiles(); if(d->activationPolicy == Activate) { foreach(const IndexedString& file, files) { ICore::self()->documentController()->openDocument(file.toUrl()); } } // ensure the old document is still activated if (oldActiveDoc.isValid()) { ICore::self()->documentController()->openDocument(oldActiveDoc); } return result; } DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::replaceOldText(CodeRepresentation* repr, const QString& newText, const ChangesList& sortedChangesList) { DynamicCodeRepresentation* dynamic = dynamic_cast(repr); if(dynamic) { auto transaction = dynamic->makeEditTransaction(); //Replay the changes one by one for(int pos = sortedChangesList.size()-1; pos >= 0; --pos) { const DocumentChange& change(*sortedChangesList[pos]); if(!dynamic->replace(change.m_range, change.m_oldText, change.m_newText, change.m_ignoreOldText)) { QString warningString = i18nc("Inconsistent change in between , found (encountered ) -> ", "Inconsistent change in %1 between %2, found %3 (encountered \"%4\") -> \"%5\"" , change.m_document.str() , printRange(change.m_range) , change.m_oldText , dynamic->rangeText(change.m_range) , change.m_newText); if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) { qCWarning(LANGUAGE) << warningString; } else if(replacePolicy == DocumentChangeSet::StopOnFailedChange) { return DocumentChangeSet::ChangeResult(warningString); } //If set to ignore failed changes just continue with the others } } return DocumentChangeSet::ChangeResult::successfulResult(); } //For files on disk if (!repr->setText(newText)) { QString warningString = i18n("Could not replace text in the document: %1", sortedChangesList.begin()->data()->m_document.str()); if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) { qCWarning(LANGUAGE) << warningString; } return DocumentChangeSet::ChangeResult(warningString); } return DocumentChangeSet::ChangeResult::successfulResult(); } DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::generateNewText(const IndexedString & file, ChangesList& sortedChanges, const CodeRepresentation * repr, QString & output) { ISourceFormatter* formatter = nullptr; if(ICore::self()) { formatter = ICore::self()->sourceFormatterController()->formatterForUrl(file.toUrl()); } //Create the actual new modified file QStringList textLines = repr->text().split(QLatin1Char('\n')); QUrl url = file.toUrl(); QMimeType mime = QMimeDatabase().mimeTypeForUrl(url); QVector removedLines; for(int pos = sortedChanges.size()-1; pos >= 0; --pos) { DocumentChange& change(*sortedChanges[pos]); QString encountered; if(changeIsValid(change, textLines) && //We demand this, although it should be fixed ((encountered = rangeText(change.m_range, textLines)) == change.m_oldText || change.m_ignoreOldText)) { ///Problem: This does not work if the other changes significantly alter the context @todo Use the changed context QString leftContext = QStringList(textLines.mid(0, change.m_range.start().line()+1)).join(QLatin1Char('\n')); leftContext.chop(textLines[change.m_range.start().line()].length() - change.m_range.start().column()); QString rightContext = QStringList(textLines.mid(change.m_range.end().line())).join(QLatin1Char('\n')).mid(change.m_range.end().column()); if(formatter && (formatPolicy == DocumentChangeSet::AutoFormatChanges || formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation)) { QString oldNewText = change.m_newText; change.m_newText = formatter->formatSource(change.m_newText, url, mime, leftContext, rightContext); if(formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation) { // Reproduce the previous indentation QStringList oldLines = oldNewText.split(QLatin1Char('\n')); QStringList newLines = change.m_newText.split(QLatin1Char('\n')); if(oldLines.size() == newLines.size()) { for(int line = 0; line < newLines.size(); ++line) { // Keep the previous indentation QString oldIndentation; for (int a = 0; a < oldLines[line].size(); ++a) { if (oldLines[line][a].isSpace()) { oldIndentation.append(oldLines[line][a]); } else { break; } } int newIndentationLength = 0; for(int a = 0; a < newLines[line].size(); ++a) { if(newLines[line][a].isSpace()) { newIndentationLength = a; } else { break; } } newLines[line].replace(0, newIndentationLength, oldIndentation); } change.m_newText = newLines.join(QLatin1Char('\n')); } else { qCDebug(LANGUAGE) << "Cannot keep the indentation because the line count has changed" << oldNewText; } } } QString& line = textLines[change.m_range.start().line()]; if (change.m_range.start().line() == change.m_range.end().line()) { // simply replace existing line content line.replace(change.m_range.start().column(), change.m_range.end().column()-change.m_range.start().column(), change.m_newText); } else { // replace first line contents line.replace(change.m_range.start().column(), line.length() - change.m_range.start().column(), change.m_newText); // null other lines and remember for deletion - for(int i = change.m_range.start().line() + 1; i <= change.m_range.end().line(); ++i) { + const int firstLine = change.m_range.start().line() + 1; + const int lastLine = change.m_range.end().line(); + removedLines.reserve(removedLines.size() + lastLine - firstLine + 1); + for (int i = firstLine; i <= lastLine; ++i) { textLines[i].clear(); removedLines << i; } } }else{ QString warningString = i18nc("Inconsistent change in at " " = (encountered ) -> ", "Inconsistent change in %1 at %2" " = \"%3\"(encountered \"%4\") -> \"%5\"" , file.str() , printRange(change.m_range) , change.m_oldText , encountered , change.m_newText); if(replacePolicy == DocumentChangeSet::IgnoreFailedChange) { //Just don't do the replacement } else if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) { qCWarning(LANGUAGE) << warningString; } else { return DocumentChangeSet::ChangeResult(warningString, sortedChanges[pos]); } } } if (!removedLines.isEmpty()) { int offset = 0; std::sort(removedLines.begin(), removedLines.end()); foreach(int l, removedLines) { textLines.removeAt(l - offset); ++offset; } } output = textLines.join(QLatin1Char('\n')); return DocumentChangeSet::ChangeResult::successfulResult(); } //Removes all duplicate changes for a single file, and then returns (via filteredChanges) the filtered duplicates DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::removeDuplicates(const IndexedString& file, ChangesList& filteredChanges) { typedef QMultiMap ChangesMap; ChangesMap sortedChanges; foreach(const DocumentChangePointer &change, changes[file]) { sortedChanges.insert(change->m_range.end(), change); } //Remove duplicates ChangesMap::iterator previous = sortedChanges.begin(); for(ChangesMap::iterator it = ++sortedChanges.begin(); it != sortedChanges.end(); ) { if(( *previous ) && ( *previous )->m_range.end() > (*it)->m_range.start()) { //intersection if(duplicateChanges(( *previous ), *it)) { //duplicate, remove one it = sortedChanges.erase(it); continue; } //When two changes contain each other, and the container change is set to ignore old text, then it should be safe to //just ignore the contained change, and apply the bigger change else if((*it)->m_range.contains(( *previous )->m_range) && (*it)->m_ignoreOldText ) { qCDebug(LANGUAGE) << "Removing change: " << ( *previous )->m_oldText << "->" << ( *previous )->m_newText << ", because it is contained by change: " << (*it)->m_oldText << "->" << (*it)->m_newText; sortedChanges.erase(previous); } //This case is for when both have the same end, either of them could be the containing range else if((*previous)->m_range.contains((*it)->m_range) && (*previous)->m_ignoreOldText ) { qCDebug(LANGUAGE) << "Removing change: " << (*it)->m_oldText << "->" << (*it)->m_newText << ", because it is contained by change: " << ( *previous )->m_oldText << "->" << ( *previous )->m_newText; it = sortedChanges.erase(it); continue; } else { return DocumentChangeSet::ChangeResult( i18nc("Inconsistent change-request at :" "intersecting changes: " " -> @ & " " -> @", "Inconsistent change-request at %1; " "intersecting changes: " "\"%2\"->\"%3\"@%4 & \"%5\"->\"%6\"@%7 " , file.str() , ( *previous )->m_oldText , ( *previous )->m_newText , printRange(( *previous )->m_range) , (*it)->m_oldText , (*it)->m_newText , printRange((*it)->m_range))); } } previous = it; ++it; } filteredChanges = sortedChanges.values(); return DocumentChangeSet::ChangeResult::successfulResult(); } void DocumentChangeSetPrivate::updateFiles() { ModificationRevisionSet::clearCache(); foreach(const IndexedString& file, changes.keys()) { ModificationRevision::clearModificationCache(file); } if(updatePolicy != DocumentChangeSet::NoUpdate && ICore::self()) { // The active document should be updated first, so that the user sees the results instantly if(IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(activeDoc->url())); } // If there are currently open documents that now need an update, update them too foreach(const IndexedString& doc, ICore::self()->languageController()->backgroundParser()->managedDocuments()) { DUChainReadLocker lock(DUChain::lock()); TopDUContext* top = DUChainUtils::standardContextForUrl(doc.toUrl(), true); if((top && top->parsingEnvironmentFile() && top->parsingEnvironmentFile()->needsUpdate()) || !top) { lock.unlock(); ICore::self()->languageController()->backgroundParser()->addDocument(doc); } } // Eventually update _all_ affected files foreach(const IndexedString &file, changes.keys()) { if(!file.toUrl().isValid()) { qCWarning(LANGUAGE) << "Trying to apply changes to an invalid document"; continue; } ICore::self()->languageController()->backgroundParser()->addDocument(file); } } } } diff --git a/kdevplatform/language/codegen/sourcefiletemplate.cpp b/kdevplatform/language/codegen/sourcefiletemplate.cpp index 71453cdfd2..0ae1ab6045 100644 --- a/kdevplatform/language/codegen/sourcefiletemplate.cpp +++ b/kdevplatform/language/codegen/sourcefiletemplate.cpp @@ -1,348 +1,349 @@ /* * This file is part of KDevelop * Copyright 2012 Miha Čančula * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "sourcefiletemplate.h" #include "templaterenderer.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; typedef SourceFileTemplate::ConfigOption ConfigOption; class KDevelop::SourceFileTemplatePrivate { public: KArchive* archive; QString descriptionFileName; QStringList searchLocations; ConfigOption readEntry(const QDomElement& element, TemplateRenderer* renderer); }; ConfigOption SourceFileTemplatePrivate::readEntry(const QDomElement& element, TemplateRenderer* renderer) { ConfigOption entry; entry.name = element.attribute(QStringLiteral("name")); entry.type = element.attribute(QStringLiteral("type"), QStringLiteral("String")); bool isDefaultValueSet = false; for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { QString tag = e.tagName(); if (tag == QLatin1String("label")) { entry.label = e.text(); } else if (tag == QLatin1String("tooltip")) { entry.label = e.text(); } else if (tag == QLatin1String("whatsthis")) { entry.label = e.text(); } else if ( tag == QLatin1String("min") ) { entry.minValue = e.text(); } else if ( tag == QLatin1String("max") ) { entry.maxValue = e.text(); } else if ( tag == QLatin1String("default") ) { entry.value = renderer->render(e.text(), entry.name); isDefaultValueSet = true; } else if (tag == QLatin1String("choices")) { QStringList values; QDomNodeList choices = element.elementsByTagName(QStringLiteral("choice")); + values.reserve(choices.size()); for (int j = 0; j < choices.size(); ++j) { QDomElement choiceElement = choices.at(j).toElement(); values << choiceElement.attribute(QStringLiteral("name")); } Q_ASSERT(!values.isEmpty()); if (values.isEmpty()) { qCWarning(LANGUAGE) << "Entry " << entry.name << "has an enum without any choices"; } entry.values = values; } } qCDebug(LANGUAGE) << "Read entry" << entry.name << "with default value" << entry.value; // preset value for enum if needed if (!entry.values.isEmpty()) { if (isDefaultValueSet) { const bool isSaneDefaultValue = entry.values.contains(entry.value.toString()); Q_ASSERT(isSaneDefaultValue); if (!isSaneDefaultValue) { qCWarning(LANGUAGE) << "Default value" << entry.value << "not in enum" << entry.values; entry.value = entry.values.at(0); } } else { entry.value = entry.values.at(0); } } return entry; } SourceFileTemplate::SourceFileTemplate (const QString& templateDescription) : d(new KDevelop::SourceFileTemplatePrivate) { d->archive = nullptr; setTemplateDescription(templateDescription); } SourceFileTemplate::SourceFileTemplate() : d(new KDevelop::SourceFileTemplatePrivate) { d->archive = nullptr; } SourceFileTemplate::SourceFileTemplate (const SourceFileTemplate& other) : d(new KDevelop::SourceFileTemplatePrivate) { d->archive = nullptr; *this = other; } SourceFileTemplate::~SourceFileTemplate() { delete d->archive; } SourceFileTemplate& SourceFileTemplate::operator=(const SourceFileTemplate& other) { if (other.d == d) { return *this; } delete d->archive; if (other.d->archive) { if (other.d->archive->fileName().endsWith(QLatin1String(".zip"))) { d->archive = new KZip(other.d->archive->fileName()); } else { d->archive = new KTar(other.d->archive->fileName()); } d->archive->open(QIODevice::ReadOnly); } else { d->archive = nullptr; } d->descriptionFileName = other.d->descriptionFileName; return *this; } void SourceFileTemplate::setTemplateDescription(const QString& templateDescription) { delete d->archive; d->descriptionFileName = templateDescription; QString archiveFileName; const QString templateBaseName = QFileInfo(templateDescription).baseName(); d->searchLocations.append(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("/kdevfiletemplates/templates/"), QStandardPaths::LocateDirectory)); foreach(const QString& dir, d->searchLocations) { foreach(const auto& entry, QDir(dir).entryInfoList(QDir::Files)) { if (entry.baseName() == templateBaseName) { archiveFileName = entry.absoluteFilePath(); qCDebug(LANGUAGE) << "Found template archive" << archiveFileName; break; } } } if (archiveFileName.isEmpty() || !QFileInfo::exists(archiveFileName)) { qCWarning(LANGUAGE) << "Could not find a template archive for description" << templateDescription << ", archive file" << archiveFileName; d->archive = nullptr; } else { QFileInfo info(archiveFileName); if (info.suffix() == QLatin1String("zip")) { d->archive = new KZip(archiveFileName); } else { d->archive = new KTar(archiveFileName); } d->archive->open(QIODevice::ReadOnly); } } bool SourceFileTemplate::isValid() const { return d->archive; } QString SourceFileTemplate::name() const { KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); return cg.readEntry("Name"); } QString SourceFileTemplate::type() const { KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); return cg.readEntry("Type", QString()); } QString SourceFileTemplate::languageName() const { KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); return cg.readEntry("Language", QString()); } QStringList SourceFileTemplate::category() const { KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); return cg.readEntry("Category", QStringList()); } QStringList SourceFileTemplate::defaultBaseClasses() const { KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); return cg.readEntry("BaseClasses", QStringList()); } const KArchiveDirectory* SourceFileTemplate::directory() const { Q_ASSERT(isValid()); return d->archive->directory(); } QVector SourceFileTemplate::outputFiles() const { QVector outputFiles; KConfig templateConfig(d->descriptionFileName); KConfigGroup group(&templateConfig, "General"); QStringList files = group.readEntry("Files", QStringList()); qCDebug(LANGUAGE) << "Files in template" << files; outputFiles.reserve(files.size()); foreach (const QString& fileGroup, files) { KConfigGroup cg(&templateConfig, fileGroup); OutputFile f; f.identifier = cg.name(); f.label = cg.readEntry("Name"); f.fileName = cg.readEntry("File"); f.outputName = cg.readEntry("OutputFile"); outputFiles << f; } return outputFiles; } bool SourceFileTemplate::hasCustomOptions() const { Q_ASSERT(isValid()); KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); bool hasOptions = d->archive->directory()->entries().contains(cg.readEntry("OptionsFile", "options.kcfg")); qCDebug(LANGUAGE) << cg.readEntry("OptionsFile", "options.kcfg") << hasOptions; return hasOptions; } QVector SourceFileTemplate::customOptions(TemplateRenderer* renderer) const { Q_ASSERT(isValid()); KConfig templateConfig(d->descriptionFileName); KConfigGroup cg(&templateConfig, "General"); const KArchiveEntry* entry = d->archive->directory()->entry(cg.readEntry("OptionsFile", "options.kcfg")); QVector optionGroups; if (!entry->isFile()) { return optionGroups; } const KArchiveFile* file = static_cast(entry); /* * Copied from kconfig_compiler.kcfg */ QDomDocument doc; QString errorMsg; int errorRow; int errorCol; if ( !doc.setContent( file->data(), &errorMsg, &errorRow, &errorCol ) ) { qCDebug(LANGUAGE) << "Unable to load document."; qCDebug(LANGUAGE) << "Parse error in line " << errorRow << ", col " << errorCol << ": " << errorMsg; return optionGroups; } QDomElement cfgElement = doc.documentElement(); if ( cfgElement.isNull() ) { qCDebug(LANGUAGE) << "No document in kcfg file"; return optionGroups; } QDomNodeList groups = cfgElement.elementsByTagName(QStringLiteral("group")); optionGroups.reserve(groups.size()); for (int i = 0; i < groups.size(); ++i) { QDomElement group = groups.at(i).toElement(); ConfigOptionGroup optionGroup; optionGroup.name = group.attribute(QStringLiteral("name")); QDomNodeList entries = group.elementsByTagName(QStringLiteral("entry")); optionGroup.options.reserve(entries.size()); for (int j = 0; j < entries.size(); ++j) { QDomElement entry = entries.at(j).toElement(); optionGroup.options << d->readEntry(entry, renderer); } optionGroups << optionGroup; } return optionGroups; } void SourceFileTemplate::addAdditionalSearchLocation(const QString& location) { if(!d->searchLocations.contains(location)) d->searchLocations.append(location); } diff --git a/kdevplatform/language/codegen/templateclassgenerator.cpp b/kdevplatform/language/codegen/templateclassgenerator.cpp index 626ebb0521..d3f3027dbe 100644 --- a/kdevplatform/language/codegen/templateclassgenerator.cpp +++ b/kdevplatform/language/codegen/templateclassgenerator.cpp @@ -1,330 +1,335 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templateclassgenerator.h" #include "archivetemplateloader.h" #include #include #include "language/codegen/documentchangeset.h" #include "codedescription.h" #include "templaterenderer.h" #include "sourcefiletemplate.h" #include #include #include #include #include #include using namespace KDevelop; /// @param base String such as 'public QObject' or 'QObject' InheritanceDescription descriptionFromString(const QString& base) { QStringList splitBase = base.split(QLatin1Char(' ')); QString identifier = splitBase.takeLast(); QString inheritanceMode = splitBase.join(QLatin1Char(' ')); InheritanceDescription desc; desc.baseType = identifier; desc.inheritanceMode = inheritanceMode; return desc; } class KDevelop::TemplateClassGeneratorPrivate { public: SourceFileTemplate fileTemplate; QUrl baseUrl; TemplateRenderer renderer; QString name; QString identifier; QStringList namespaces; QString license; QHash fileUrls; QHash filePositions; ClassDescription description; QList directBaseClasses; QList allBaseClasses; void fetchSuperClasses(const DeclarationPointer& declaration); }; void TemplateClassGeneratorPrivate::fetchSuperClasses(const DeclarationPointer& declaration) { DUChainReadLocker lock; //Prevent duplicity if(allBaseClasses.contains(declaration)) { return; } allBaseClasses << declaration; DUContext* context = declaration->internalContext(); if (context) { foreach (const DUContext::Import& import, context->importedParentContexts()) { if (DUContext * parentContext = import.context(context->topContext())) { if (parentContext->type() == DUContext::Class) { fetchSuperClasses( DeclarationPointer(parentContext->owner()) ); } } } } } TemplateClassGenerator::TemplateClassGenerator(const QUrl& baseUrl) : d(new TemplateClassGeneratorPrivate) { Q_ASSERT(QFileInfo(baseUrl.toLocalFile()).isDir()); // assume folder d->baseUrl = baseUrl; d->renderer.setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); } TemplateClassGenerator::~TemplateClassGenerator() = default; void TemplateClassGenerator::setTemplateDescription(const SourceFileTemplate& fileTemplate) { d->fileTemplate = fileTemplate; Q_ASSERT(fileTemplate.isValid()); } DocumentChangeSet TemplateClassGenerator::generate() { return d->renderer.renderFileTemplate(d->fileTemplate, d->baseUrl, fileUrls()); } QHash TemplateClassGenerator::fileLabels() const { Q_ASSERT(d->fileTemplate.isValid()); QHash labels; foreach (const SourceFileTemplate::OutputFile& outputFile, d->fileTemplate.outputFiles()) { labels.insert(outputFile.identifier, outputFile.label); } return labels; } TemplateClassGenerator::UrlHash TemplateClassGenerator::fileUrls() const { if (d->fileUrls.isEmpty()) { foreach (const SourceFileTemplate::OutputFile& outputFile, d->fileTemplate.outputFiles()) { QString outputName = d->renderer.render(outputFile.outputName, outputFile.identifier); QUrl url = d->baseUrl.resolved(QUrl(outputName)); d->fileUrls.insert(outputFile.identifier, url); } } return d->fileUrls; } QUrl TemplateClassGenerator::baseUrl() const { return d->baseUrl; } QUrl TemplateClassGenerator::fileUrl(const QString& outputFile) const { return fileUrls().value(outputFile); } void TemplateClassGenerator::setFileUrl(const QString& outputFile, const QUrl& url) { d->fileUrls.insert(outputFile, url); d->renderer.addVariable(QLatin1String("output_file_") + outputFile.toLower(), QDir(d->baseUrl.path()).relativeFilePath(url.path())); d->renderer.addVariable(QLatin1String("output_file_") + outputFile.toLower() + QLatin1String("_absolute"), url.toLocalFile()); } KTextEditor::Cursor TemplateClassGenerator::filePosition(const QString& outputFile) const { return d->filePositions.value(outputFile); } void TemplateClassGenerator::setFilePosition(const QString& outputFile, const KTextEditor::Cursor& position) { d->filePositions.insert(outputFile, position); } void TemplateClassGenerator::addVariables(const QVariantHash& variables) { d->renderer.addVariables(variables); } QString TemplateClassGenerator::renderString(const QString& text) const { return d->renderer.render(text); } SourceFileTemplate TemplateClassGenerator::sourceFileTemplate() const { return d->fileTemplate; } TemplateRenderer* TemplateClassGenerator::renderer() const { return &(d->renderer); } QString TemplateClassGenerator::name() const { return d->name; } void TemplateClassGenerator::setName(const QString& newName) { d->name = newName; d->renderer.addVariable(QStringLiteral("name"), newName); } QString TemplateClassGenerator::identifier() const { return name(); } void TemplateClassGenerator::setIdentifier(const QString& identifier) { - d->renderer.addVariable(QStringLiteral("identifier"), identifier);; - QStringList separators; - separators << QStringLiteral("::") << QStringLiteral(".") << QStringLiteral(":") << QStringLiteral("\\") << QStringLiteral("/"); + d->renderer.addVariable(QStringLiteral("identifier"), identifier); + const QStringList separators{ + QStringLiteral("::"), + QStringLiteral("."), + QStringLiteral(":"), + QStringLiteral("\\"), + QStringLiteral("/"), + }; QStringList ns; foreach (const QString& separator, separators) { ns = identifier.split(separator); if (ns.size() > 1) { break; } } setName(ns.takeLast()); setNamespaces(ns); } QStringList TemplateClassGenerator::namespaces() const { return d->namespaces; } void TemplateClassGenerator::setNamespaces(const QStringList& namespaces) const { d->namespaces = namespaces; d->renderer.addVariable(QStringLiteral("namespaces"), namespaces); } /// Specify license for this class void TemplateClassGenerator::setLicense(const QString& license) { qCDebug(LANGUAGE) << "New Class: " << d->name << "Set license: " << d->license; d->license = license; d->renderer.addVariable(QStringLiteral("license"), license); } /// Get the license specified for this classes QString TemplateClassGenerator::license() const { return d->license; } void TemplateClassGenerator::setDescription(const ClassDescription& description) { d->description = description; QVariantHash variables; variables[QStringLiteral("description")] = QVariant::fromValue(description); variables[QStringLiteral("members")] = CodeDescription::toVariantList(description.members); variables[QStringLiteral("functions")] = CodeDescription::toVariantList(description.methods); variables[QStringLiteral("base_classes")] = CodeDescription::toVariantList(description.baseClasses); d->renderer.addVariables(variables); } ClassDescription TemplateClassGenerator::description() const { return d->description; } void TemplateClassGenerator::addBaseClass(const QString& base) { const InheritanceDescription desc = descriptionFromString(base); ClassDescription cd = description(); cd.baseClasses << desc; setDescription(cd); DUChainReadLocker lock; PersistentSymbolTable::Declarations decl = PersistentSymbolTable::self().getDeclarations(IndexedQualifiedIdentifier(QualifiedIdentifier(desc.baseType))); //Search for all super classes for(PersistentSymbolTable::Declarations::Iterator it = decl.iterator(); it; ++it) { DeclarationPointer declaration = DeclarationPointer(it->declaration()); if(declaration->isForwardDeclaration()) { continue; } // Check if it's a class/struct/etc if(declaration->type()) { d->fetchSuperClasses(declaration); d->directBaseClasses << declaration; break; } } } void TemplateClassGenerator::setBaseClasses(const QList& bases) { // clear ClassDescription cd = description(); cd.baseClasses.clear(); setDescription(cd); d->directBaseClasses.clear(); d->allBaseClasses.clear(); // add all bases foreach (const QString& base, bases) { addBaseClass(base); } } QList< DeclarationPointer > TemplateClassGenerator::directBaseClasses() const { return d->directBaseClasses; } QList< DeclarationPointer > TemplateClassGenerator::allBaseClasses() const { return d->allBaseClasses; } diff --git a/kdevplatform/language/codegen/templaterenderer.cpp b/kdevplatform/language/codegen/templaterenderer.cpp index 9d0148c456..cf6c38c82f 100644 --- a/kdevplatform/language/codegen/templaterenderer.cpp +++ b/kdevplatform/language/codegen/templaterenderer.cpp @@ -1,305 +1,306 @@ /* * This file is part of KDevelop * Copyright 2012 Miha Čančula * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "templaterenderer.h" #include "documentchangeset.h" #include "sourcefiletemplate.h" #include "templateengine.h" #include "templateengine_p.h" #include "archivetemplateloader.h" #include #include #include #include #include #include #include using namespace Grantlee; class NoEscapeStream : public OutputStream { public: NoEscapeStream(); explicit NoEscapeStream (QTextStream* stream); QString escape (const QString& input) const override; QSharedPointer< OutputStream > clone (QTextStream* stream) const override; }; NoEscapeStream::NoEscapeStream() : OutputStream() { } NoEscapeStream::NoEscapeStream(QTextStream* stream) : OutputStream (stream) { } QString NoEscapeStream::escape(const QString& input) const { return input; } QSharedPointer NoEscapeStream::clone(QTextStream* stream) const { QSharedPointer clonedStream = QSharedPointer( new NoEscapeStream( stream ) ); return clonedStream; } using namespace KDevelop; namespace KDevelop { class TemplateRendererPrivate { public: Engine* engine; Grantlee::Context context; TemplateRenderer::EmptyLinesPolicy emptyLinesPolicy; QString errorString; }; } TemplateRenderer::TemplateRenderer() : d(new TemplateRendererPrivate) { d->engine = &TemplateEngine::self()->d->engine; d->emptyLinesPolicy = KeepEmptyLines; } TemplateRenderer::~TemplateRenderer() = default; void TemplateRenderer::addVariables(const QVariantHash& variables) { QVariantHash::const_iterator it = variables.constBegin(); QVariantHash::const_iterator end = variables.constEnd(); for (; it != end; ++it) { d->context.insert(it.key(), it.value()); } } void TemplateRenderer::addVariable(const QString& name, const QVariant& value) { d->context.insert(name, value); } QVariantHash TemplateRenderer::variables() const { return d->context.stackHash(0); } QString TemplateRenderer::render(const QString& content, const QString& name) const { Template t = d->engine->newTemplate(content, name); QString output; QTextStream textStream(&output); NoEscapeStream stream(&textStream); t->render(&stream, &d->context); if (t->error() != Grantlee::NoError) { d->errorString = t->errorString(); } else { d->errorString.clear(); } if (d->emptyLinesPolicy == TrimEmptyLines && output.contains(QLatin1Char('\n'))) { QStringList lines = output.split(QLatin1Char('\n'), QString::KeepEmptyParts); QMutableStringListIterator it(lines); // Remove empty lines from the start of the document while (it.hasNext()) { if (it.next().trimmed().isEmpty()) { it.remove(); } else { break; } } // Remove single empty lines it.toFront(); bool prePreviousEmpty = false; bool previousEmpty = false; while (it.hasNext()) { bool currentEmpty = it.peekNext().trimmed().isEmpty(); if (!prePreviousEmpty && previousEmpty && !currentEmpty) { it.remove(); } prePreviousEmpty = previousEmpty; previousEmpty = currentEmpty; it.next(); } // Compress multiple empty lines it.toFront(); previousEmpty = false; while (it.hasNext()) { bool currentEmpty = it.next().trimmed().isEmpty(); if (currentEmpty && previousEmpty) { it.remove(); } previousEmpty = currentEmpty; } // Remove empty lines from the end it.toBack(); while (it.hasPrevious()) { if (it.previous().trimmed().isEmpty()) { it.remove(); } else { break; } } // Add a newline to the end of file it.toBack(); it.insert(QString()); output = lines.join(QLatin1Char('\n')); } else if (d->emptyLinesPolicy == RemoveEmptyLines) { QStringList lines = output.split(QLatin1Char('\n'), QString::SkipEmptyParts); QMutableStringListIterator it(lines); while (it.hasNext()) { if (it.next().trimmed().isEmpty()) { it.remove(); } } it.toBack(); if (lines.size() > 1) { it.insert(QString()); } output = lines.join(QLatin1Char('\n')); } return output; } QString TemplateRenderer::renderFile(const QUrl& url, const QString& name) const { QFile file(url.toLocalFile()); file.open(QIODevice::ReadOnly); const QString content = QString::fromUtf8(file.readAll()); qCDebug(LANGUAGE) << content; return render(content, name); } QStringList TemplateRenderer::render(const QStringList& contents) const { qCDebug(LANGUAGE) << d->context.stackHash(0); QStringList ret; + ret.reserve(contents.size()); foreach (const QString& content, contents) { ret << render(content); } return ret; } void TemplateRenderer::setEmptyLinesPolicy(TemplateRenderer::EmptyLinesPolicy policy) { d->emptyLinesPolicy = policy; } TemplateRenderer::EmptyLinesPolicy TemplateRenderer::emptyLinesPolicy() const { return d->emptyLinesPolicy; } DocumentChangeSet TemplateRenderer::renderFileTemplate(const SourceFileTemplate& fileTemplate, const QUrl& baseUrl, const QHash& fileUrls) { DocumentChangeSet changes; const QDir baseDir(baseUrl.path()); QRegExp nonAlphaNumeric(QStringLiteral("\\W")); for (QHash::const_iterator it = fileUrls.constBegin(); it != fileUrls.constEnd(); ++it) { QString cleanName = it.key().toLower(); cleanName.replace(nonAlphaNumeric, QStringLiteral("_")); const QString path = it.value().toLocalFile(); addVariable(QLatin1String("output_file_") + cleanName, baseDir.relativeFilePath(path)); addVariable(QLatin1String("output_file_") + cleanName + QLatin1String("_absolute"), path); } const KArchiveDirectory* directory = fileTemplate.directory(); ArchiveTemplateLocation location(directory); foreach (const SourceFileTemplate::OutputFile& outputFile, fileTemplate.outputFiles()) { const KArchiveEntry* entry = directory->entry(outputFile.fileName); if (!entry) { qCWarning(LANGUAGE) << "Entry" << outputFile.fileName << "is mentioned in group" << outputFile.identifier << "but is not present in the archive"; continue; } const KArchiveFile* file = dynamic_cast(entry); if (!file) { qCWarning(LANGUAGE) << "Entry" << entry->name() << "is not a file"; continue; } QUrl url = fileUrls[outputFile.identifier]; IndexedString document(url); KTextEditor::Range range(KTextEditor::Cursor(0, 0), 0); DocumentChange change(document, range, QString(), render(QString::fromUtf8(file->data()), outputFile.identifier)); changes.addChange(change); qCDebug(LANGUAGE) << "Added change for file" << document.str(); } return changes; } QString TemplateRenderer::errorString() const { return d->errorString; } diff --git a/kdevplatform/language/codegen/templatesmodel.cpp b/kdevplatform/language/codegen/templatesmodel.cpp index c1c1b08cbb..085b742db3 100644 --- a/kdevplatform/language/codegen/templatesmodel.cpp +++ b/kdevplatform/language/codegen/templatesmodel.cpp @@ -1,409 +1,411 @@ /* This file is part of KDevelop Copyright 2007 Alexander Dymo Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templatesmodel.h" #include "templatepreviewicon.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; class KDevelop::TemplatesModelPrivate { public: explicit TemplatesModelPrivate(const QString& typePrefix); QString typePrefix; QStringList searchPaths; QMap templateItems; /** * Extracts description files from all available template archives and saves them to a location * determined by descriptionResourceSuffix(). **/ void extractTemplateDescriptions(); /** * Creates a model item for the template @p name in category @p category * * @param name the name of the new template * @param category the category of the new template * @param parent the parent item * @return the created item **/ QStandardItem *createItem(const QString& name, const QString& category, QStandardItem* parent); enum ResourceType { Description, Template, Preview }; QString resourceFilter(ResourceType type, const QString &suffix = QString()) { QString filter = typePrefix; switch(type) { case Description: filter += QLatin1String("template_descriptions/"); break; case Template: filter += QLatin1String("templates/"); break; case Preview: filter += QLatin1String("template_previews/"); break; } return filter + suffix; } }; TemplatesModelPrivate::TemplatesModelPrivate(const QString& _typePrefix) : typePrefix(_typePrefix) { if (!typePrefix.endsWith(QLatin1Char('/'))) { typePrefix.append(QLatin1Char('/')); } } TemplatesModel::TemplatesModel(const QString& typePrefix, QObject* parent) : QStandardItemModel(parent) , d(new TemplatesModelPrivate(typePrefix)) { } TemplatesModel::~TemplatesModel() = default; void TemplatesModel::refresh() { clear(); d->templateItems.clear(); d->templateItems[QString()] = invisibleRootItem(); d->extractTemplateDescriptions(); QStringList templateArchives; foreach(const QString& archivePath, d->searchPaths) { const QStringList files = QDir(archivePath).entryList(QDir::Files); foreach(const QString& file, files) { templateArchives.append(archivePath + file); } } QStringList templateDescriptions; const QStringList templatePaths = {QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->resourceFilter(TemplatesModelPrivate::Description)}; foreach(const QString& templateDescription, templatePaths) { const QStringList files = QDir(templateDescription).entryList(QDir::Files); foreach(const QString& file, files) { templateDescriptions.append(templateDescription + file); } } foreach (const QString &templateDescription, templateDescriptions) { QFileInfo fi(templateDescription); bool archiveFound = false; foreach( const QString& templateArchive, templateArchives ) { if( QFileInfo(templateArchive).baseName() == fi.baseName() ) { archiveFound = true; KConfig templateConfig(templateDescription); KConfigGroup general(&templateConfig, "General"); QString name = general.readEntry("Name"); QString category = general.readEntry("Category"); QString comment = general.readEntry("Comment"); TemplatePreviewIcon icon(general.readEntry("Icon"), templateArchive, d->resourceFilter(TemplatesModelPrivate::Preview)); QStandardItem *templateItem = d->createItem(name, category, invisibleRootItem()); templateItem->setData(templateDescription, DescriptionFileRole); templateItem->setData(templateArchive, ArchiveFileRole); templateItem->setData(comment, CommentRole); templateItem->setData(QVariant::fromValue(icon), PreviewIconRole); } } if (!archiveFound) { // Template file doesn't exist anymore, so remove the description // saves us the extra lookups for templateExists on the next run QFile(templateDescription).remove(); } } } QStandardItem *TemplatesModelPrivate::createItem(const QString& name, const QString& category, QStandardItem* parent) { QStringList path = category.split(QLatin1Char('/')); QStringList currentPath; + currentPath.reserve(path.size()); foreach (const QString& entry, path) { currentPath << entry; if (!templateItems.contains(currentPath.join(QLatin1Char('/')))) { QStandardItem *item = new QStandardItem(entry); item->setEditable(false); parent->appendRow(item); templateItems[currentPath.join(QLatin1Char('/'))] = item; parent = item; } else { parent = templateItems[currentPath.join(QLatin1Char('/'))]; } } QStandardItem *templateItem = new QStandardItem(name); templateItem->setEditable(false); parent->appendRow(templateItem); return templateItem; } void TemplatesModelPrivate::extractTemplateDescriptions() { QStringList templateArchives; searchPaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, resourceFilter(Template), QStandardPaths::LocateDirectory); searchPaths.removeDuplicates(); foreach(const QString &archivePath, searchPaths) { const QStringList files = QDir(archivePath).entryList(QDir::Files); foreach(const QString& file, files) { if(file.endsWith(QLatin1String(".zip")) || file.endsWith(QLatin1String(".tar.bz2"))) { QString archfile = archivePath + file; templateArchives.append(archfile); } } } QString localDescriptionsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + resourceFilter(Description); QDir dir(localDescriptionsDir); if(!dir.exists()) dir.mkpath(QStringLiteral(".")); foreach (const QString &archName, templateArchives) { qCDebug(LANGUAGE) << "processing template" << archName; QScopedPointer templateArchive; if (QFileInfo(archName).completeSuffix() == QLatin1String("zip")) { templateArchive.reset(new KZip(archName)); } else { templateArchive.reset(new KTar(archName)); } if (templateArchive->open(QIODevice::ReadOnly)) { /* * This class looks for template description files in the following order * * - "basename.kdevtemplate" * - "*.kdevtemplate" * - "basename.desktop" * - "*.desktop" * * This is done because application templates can contain .desktop files used by the application * so the kdevtemplate suffix must have priority. */ QFileInfo templateInfo(archName); QString suffix = QStringLiteral(".kdevtemplate"); const KArchiveEntry *templateEntry = templateArchive->directory()->entry(templateInfo.baseName() + suffix); if (!templateEntry || !templateEntry->isFile()) { /* * First, if the .kdevtemplate file is not found by name, * we check all the files in the archive for any .kdevtemplate file * * This is needed because kde-files.org renames downloaded files */ foreach (const QString& entryName, templateArchive->directory()->entries()) { if (entryName.endsWith(suffix)) { templateEntry = templateArchive->directory()->entry(entryName); break; } } } if (!templateEntry || !templateEntry->isFile()) { suffix = QStringLiteral(".desktop"); templateEntry = templateArchive->directory()->entry(templateInfo.baseName() + suffix); } if (!templateEntry || !templateEntry->isFile()) { foreach (const QString& entryName, templateArchive->directory()->entries()) { if (entryName.endsWith(suffix)) { templateEntry = templateArchive->directory()->entry(entryName); break; } } } if (!templateEntry || !templateEntry->isFile()) { qCDebug(LANGUAGE) << "template" << archName << "does not contain .kdevtemplate or .desktop file"; continue; } const KArchiveFile *templateFile = static_cast(templateEntry); qCDebug(LANGUAGE) << "copy template description to" << localDescriptionsDir; const QString descriptionFileName = templateInfo.baseName() + suffix; if (templateFile->name() == descriptionFileName) { templateFile->copyTo(localDescriptionsDir); } else { // Rename the extracted description // so that its basename matches the basename of the template archive // Use temporary dir to not overwrite other files with same name QTemporaryDir dir; templateFile->copyTo(dir.path()); const QString destinationPath = localDescriptionsDir + descriptionFileName; QFile::remove(destinationPath); QFile::rename(dir.path() + QLatin1Char('/') + templateFile->name(), destinationPath); } } else { qCWarning(LANGUAGE) << "could not open template" << archName; } } } QModelIndexList TemplatesModel::templateIndexes(const QString& fileName) const { QFileInfo info(fileName); QString description = QStandardPaths::locate(QStandardPaths::GenericDataLocation, d->resourceFilter(TemplatesModelPrivate::Description, info.baseName() + QLatin1String(".kdevtemplate"))); if (description.isEmpty()) { description = QStandardPaths::locate(QStandardPaths::GenericDataLocation, d->resourceFilter(TemplatesModelPrivate::Description, info.baseName() + QLatin1String(".desktop"))); } QModelIndexList indexes; if (!description.isEmpty()) { KConfig templateConfig(description); KConfigGroup general(&templateConfig, "General"); QStringList categories = general.readEntry("Category").split(QLatin1Char('/')); QStringList levels; + levels.reserve(categories.size()); foreach (const QString& category, categories) { levels << category; indexes << d->templateItems[levels.join(QLatin1Char('/'))]->index(); } if (!indexes.isEmpty()) { QString name = general.readEntry("Name"); QStandardItem* categoryItem = d->templateItems[levels.join(QLatin1Char('/'))]; for (int i = 0; i < categoryItem->rowCount(); ++i) { QStandardItem* templateItem = categoryItem->child(i); if (templateItem->text() == name) { indexes << templateItem->index(); break; } } } } return indexes; } QString TemplatesModel::typePrefix() const { return d->typePrefix; } void TemplatesModel::addDataPath(const QString& path) { QString realpath = path + d->resourceFilter(TemplatesModelPrivate::Template); d->searchPaths.append(realpath); } QString TemplatesModel::loadTemplateFile(const QString& fileName) { QString saveLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->resourceFilter(TemplatesModelPrivate::Template); QDir dir(saveLocation); if(!dir.exists()) dir.mkpath(QStringLiteral(".")); QFileInfo info(fileName); QString destination = saveLocation + info.baseName(); QMimeType mimeType = QMimeDatabase().mimeTypeForFile(fileName); qCDebug(LANGUAGE) << "Loaded file" << fileName << "with type" << mimeType.name(); if (mimeType.name() == QLatin1String("application/x-desktop")) { qCDebug(LANGUAGE) << "Loaded desktop file" << info.absoluteFilePath() << ", compressing"; #ifdef Q_WS_WIN destination += ".zip"; KZip archive(destination); #else destination += QLatin1String(".tar.bz2"); KTar archive(destination, QStringLiteral("application/x-bzip")); #endif //Q_WS_WIN archive.open(QIODevice::WriteOnly); QDir dir(info.absoluteDir()); QDir::Filters filter = QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot; foreach (const QFileInfo& entry, dir.entryInfoList(filter)) { if (entry.isFile()) { archive.addLocalFile(entry.absoluteFilePath(), entry.fileName()); } else if (entry.isDir()) { archive.addLocalDirectory(entry.absoluteFilePath(), entry.fileName()); } } archive.close(); } else { qCDebug(LANGUAGE) << "Copying" << fileName << "to" << saveLocation; QFile::copy(fileName, saveLocation + info.fileName()); } refresh(); return destination; } diff --git a/kdevplatform/language/duchain/ducontext.cpp b/kdevplatform/language/duchain/ducontext.cpp index f43c4ce09e..77b96f3e91 100644 --- a/kdevplatform/language/duchain/ducontext.cpp +++ b/kdevplatform/language/duchain/ducontext.cpp @@ -1,1706 +1,1708 @@ /* This is part of KDevelop Copyright 2006 Hamish Rodda Copyright 2007-2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ducontext.h" #include #include #include #include "ducontextdata.h" #include "declaration.h" #include "duchain.h" #include "duchainlock.h" #include "use.h" #include "identifier.h" #include "topducontext.h" #include "persistentsymboltable.h" #include "aliasdeclaration.h" #include "namespacealiasdeclaration.h" #include "abstractfunctiondeclaration.h" #include "duchainregister.h" #include "topducontextdynamicdata.h" #include "importers.h" #include "uses.h" #include "navigation/abstractdeclarationnavigationcontext.h" #include "navigation/abstractnavigationwidget.h" #include "ducontextdynamicdata.h" #include // maximum depth for DUContext::findDeclarationsInternal searches const uint maxParentDepth = 20; using namespace KTextEditor; #ifndef NDEBUG #define ENSURE_CAN_WRITE_(x) {if(x->inDUChain()) { ENSURE_CHAIN_WRITE_LOCKED }} #define ENSURE_CAN_READ_(x) {if(x->inDUChain()) { ENSURE_CHAIN_READ_LOCKED }} #else #define ENSURE_CAN_WRITE_(x) #define ENSURE_CAN_READ_(x) #endif QDebug operator<<(QDebug dbg, const KDevelop::DUContext::Import& import) { QDebugStateSaver saver(dbg); dbg.nospace() << "Import(" << import.indexedContext().data() << ')'; return dbg; } namespace KDevelop { DEFINE_LIST_MEMBER_HASH(DUContextData, m_childContexts, LocalIndexedDUContext) DEFINE_LIST_MEMBER_HASH(DUContextData, m_importers, IndexedDUContext) DEFINE_LIST_MEMBER_HASH(DUContextData, m_importedContexts, DUContext::Import) DEFINE_LIST_MEMBER_HASH(DUContextData, m_localDeclarations, LocalIndexedDeclaration) DEFINE_LIST_MEMBER_HASH(DUContextData, m_uses, Use) REGISTER_DUCHAIN_ITEM(DUContext); DUChainVisitor::~DUChainVisitor() { } /** * We leak here, to prevent a possible crash during destruction, as the destructor * of Identifier is not safe to be called after the duchain has been destroyed */ const Identifier& globalImportIdentifier() { static const Identifier globalImportIdentifierObject(QStringLiteral("{...import...}")); return globalImportIdentifierObject; } const Identifier& globalAliasIdentifier() { static const Identifier globalAliasIdentifierObject(QStringLiteral("{...alias...}")); return globalAliasIdentifierObject; } const IndexedIdentifier& globalIndexedImportIdentifier() { static const IndexedIdentifier id(globalImportIdentifier()); return id; } const IndexedIdentifier& globalIndexedAliasIdentifier() { static const IndexedIdentifier id(globalAliasIdentifier()); return id; } void DUContext::rebuildDynamicData(DUContext* parent, uint ownIndex) { Q_ASSERT(!parent || ownIndex); m_dynamicData->m_topContext = parent ? parent->topContext() : static_cast(this); m_dynamicData->m_indexInTopContext = ownIndex; m_dynamicData->m_parentContext = DUContextPointer(parent); m_dynamicData->m_context = this; m_dynamicData->m_childContexts.clear(); m_dynamicData->m_childContexts.reserve(d_func()->m_childContextsSize()); FOREACH_FUNCTION(const LocalIndexedDUContext& ctx, d_func()->m_childContexts) { m_dynamicData->m_childContexts << ctx.data(m_dynamicData->m_topContext); } m_dynamicData->m_localDeclarations.clear(); m_dynamicData->m_localDeclarations.reserve(d_func()->m_localDeclarationsSize()); FOREACH_FUNCTION(const LocalIndexedDeclaration& idx, d_func()->m_localDeclarations) { auto declaration = idx.data(m_dynamicData->m_topContext); if (!declaration) { qCWarning(LANGUAGE) << "child declaration number" << idx.localIndex() << "of" << d_func_dynamic()->m_localDeclarationsSize() << "is invalid"; continue; } m_dynamicData->m_localDeclarations << declaration; } DUChainBase::rebuildDynamicData(parent, ownIndex); } DUContextData::DUContextData() : m_inSymbolTable(false) , m_anonymousInParent(false) , m_propagateDeclarations(false) { initializeAppendedLists(); } DUContextData::~DUContextData() { freeAppendedLists(); } DUContextData::DUContextData(const DUContextData& rhs) : DUChainBaseData(rhs) , m_inSymbolTable(rhs.m_inSymbolTable) , m_anonymousInParent(rhs.m_anonymousInParent) , m_propagateDeclarations(rhs.m_propagateDeclarations) { initializeAppendedLists(); copyListsFrom(rhs); m_scopeIdentifier = rhs.m_scopeIdentifier; m_contextType = rhs.m_contextType; m_owner = rhs.m_owner; } DUContextDynamicData::DUContextDynamicData(DUContext* d) : m_topContext(nullptr) , m_indexInTopContext(0) , m_context(d) { } void DUContextDynamicData::scopeIdentifier(bool includeClasses, QualifiedIdentifier& target) const { if (m_parentContext) m_parentContext->m_dynamicData->scopeIdentifier(includeClasses, target); if (includeClasses || d_func()->m_contextType != DUContext::Class) target += d_func()->m_scopeIdentifier; } bool DUContextDynamicData::imports(const DUContext* context, const TopDUContext* source, QSet* recursionGuard) const { if( this == context->m_dynamicData ) return true; if (recursionGuard->contains(this)) { return false; } recursionGuard->insert(this); FOREACH_FUNCTION( const DUContext::Import& ctx, d_func()->m_importedContexts ) { DUContext* import = ctx.context(source); if(import == context || (import && import->m_dynamicData->imports(context, source, recursionGuard))) return true; } return false; } inline bool isContextTemporary(uint index) { return index > (0xffffffff/2); } void DUContextDynamicData::addDeclaration( Declaration * newDeclaration ) { // The definition may not have its identifier set when it's assigned... // allow dupes here, TODO catch the error elsewhere //If this context is temporary, added declarations should be as well, and viceversa Q_ASSERT(isContextTemporary(m_indexInTopContext) == isContextTemporary(newDeclaration->ownIndex())); CursorInRevision start = newDeclaration->range().start; bool inserted = false; ///@todo Do binary search to find the position for (int i = m_localDeclarations.size() - 1; i >= 0; --i) { Declaration* child = m_localDeclarations[i]; Q_ASSERT(d_func()->m_localDeclarations()[i].data(m_topContext) == child); if(child == newDeclaration) return; //TODO: All declarations in a macro will have the same empty range, and just get appended //that may not be Good Enough in complex cases. if (start >= child->range().start) { m_localDeclarations.insert(i + 1, newDeclaration); d_func_dynamic()->m_localDeclarationsList().insert(i+1, newDeclaration); Q_ASSERT(d_func()->m_localDeclarations()[i+1].data(m_topContext) == newDeclaration); inserted = true; break; } } if (!inserted) { // We haven't found any child that is before this one, so prepend it m_localDeclarations.insert(0, newDeclaration); d_func_dynamic()->m_localDeclarationsList().insert(0, newDeclaration); Q_ASSERT(d_func()->m_localDeclarations()[0].data(m_topContext) == newDeclaration); } } bool DUContextDynamicData::removeDeclaration(Declaration* declaration) { const int idx = m_localDeclarations.indexOf(declaration); if (idx != -1) { Q_ASSERT(d_func()->m_localDeclarations()[idx].data(m_topContext) == declaration); m_localDeclarations.remove(idx); d_func_dynamic()->m_localDeclarationsList().remove(idx); return true; } else { Q_ASSERT(d_func_dynamic()->m_localDeclarationsList().indexOf(LocalIndexedDeclaration(declaration)) == -1); return false; } } void DUContextDynamicData::addChildContext( DUContext * context ) { // Internal, don't need to assert a lock Q_ASSERT(!context->m_dynamicData->m_parentContext || context->m_dynamicData->m_parentContext.data()->m_dynamicData == this ); LocalIndexedDUContext indexed(context->m_dynamicData->m_indexInTopContext); //If this context is temporary, added declarations should be as well, and viceversa Q_ASSERT(isContextTemporary(m_indexInTopContext) == isContextTemporary(indexed.localIndex())); bool inserted = false; int childCount = m_childContexts.size(); for (int i = childCount-1; i >= 0; --i) {///@todo Do binary search to find the position DUContext* child = m_childContexts[i]; Q_ASSERT(d_func_dynamic()->m_childContexts()[i] == LocalIndexedDUContext(child)); if (context == child) return; if (context->range().start >= child->range().start) { m_childContexts.insert(i+1, context); d_func_dynamic()->m_childContextsList().insert(i+1, indexed); context->m_dynamicData->m_parentContext = m_context; inserted = true; break; } } if( !inserted ) { m_childContexts.insert(0, context); d_func_dynamic()->m_childContextsList().insert(0, indexed); context->m_dynamicData->m_parentContext = m_context; } } bool DUContextDynamicData::removeChildContext( DUContext* context ) { // ENSURE_CAN_WRITE const int idx = m_childContexts.indexOf(context); if (idx != -1) { m_childContexts.remove(idx); Q_ASSERT(d_func()->m_childContexts()[idx] == LocalIndexedDUContext(context)); d_func_dynamic()->m_childContextsList().remove(idx); return true; } else { Q_ASSERT(d_func_dynamic()->m_childContextsList().indexOf(LocalIndexedDUContext(context)) == -1); return false; } } void DUContextDynamicData::addImportedChildContext( DUContext * context ) { // ENSURE_CAN_WRITE DUContext::Import import(m_context, context); if(import.isDirect()) { //Direct importers are registered directly within the data if(d_func_dynamic()->m_importersList().contains(IndexedDUContext(context))) { qCDebug(LANGUAGE) << m_context->scopeIdentifier(true).toString() << "importer added multiple times:" << context->scopeIdentifier(true).toString(); return; } d_func_dynamic()->m_importersList().append(context); }else{ //Indirect importers are registered separately Importers::self().addImporter(import.indirectDeclarationId(), IndexedDUContext(context)); } } //Can also be called with a context that is not in the list void DUContextDynamicData::removeImportedChildContext( DUContext * context ) { // ENSURE_CAN_WRITE DUContext::Import import(m_context, context); if(import.isDirect()) { d_func_dynamic()->m_importersList().removeOne(IndexedDUContext(context)); }else{ //Indirect importers are registered separately Importers::self().removeImporter(import.indirectDeclarationId(), IndexedDUContext(context)); } } int DUContext::depth() const { { if (!parentContext()) return 0; return parentContext()->depth() + 1; } } DUContext::DUContext(DUContextData& data) : DUChainBase(data) , m_dynamicData(new DUContextDynamicData(this)) { } DUContext::DUContext(const RangeInRevision& range, DUContext* parent, bool anonymous) : DUChainBase(*new DUContextData(), range) , m_dynamicData(new DUContextDynamicData(this)) { d_func_dynamic()->setClassId(this); if(parent) m_dynamicData->m_topContext = parent->topContext(); else m_dynamicData->m_topContext = static_cast(this); d_func_dynamic()->setClassId(this); DUCHAIN_D_DYNAMIC(DUContext); d->m_contextType = Other; m_dynamicData->m_parentContext = nullptr; d->m_anonymousInParent = anonymous; d->m_inSymbolTable = false; if (parent) { m_dynamicData->m_indexInTopContext = parent->topContext()->m_dynamicData->allocateContextIndex(this, parent->isAnonymous() || anonymous); Q_ASSERT(m_dynamicData->m_indexInTopContext); if( !anonymous ) parent->m_dynamicData->addChildContext(this); else m_dynamicData->m_parentContext = parent; } if(parent && !anonymous && parent->inSymbolTable()) setInSymbolTable(true); } bool DUContext::isAnonymous() const { return d_func()->m_anonymousInParent || (m_dynamicData->m_parentContext && m_dynamicData->m_parentContext->isAnonymous()); } DUContext::DUContext( DUContextData& dd, const RangeInRevision& range, DUContext * parent, bool anonymous ) : DUChainBase(dd, range) , m_dynamicData(new DUContextDynamicData(this)) { if(parent) m_dynamicData->m_topContext = parent->topContext(); else m_dynamicData->m_topContext = static_cast(this); DUCHAIN_D_DYNAMIC(DUContext); d->m_contextType = Other; m_dynamicData->m_parentContext = nullptr; d->m_inSymbolTable = false; d->m_anonymousInParent = anonymous; if (parent) { m_dynamicData->m_indexInTopContext = parent->topContext()->m_dynamicData->allocateContextIndex(this, parent->isAnonymous() || anonymous); if( !anonymous ) parent->m_dynamicData->addChildContext(this); else m_dynamicData->m_parentContext = parent; } } DUContext::DUContext(DUContext& useDataFrom) : DUChainBase(useDataFrom) , m_dynamicData(useDataFrom.m_dynamicData) { } DUContext::~DUContext( ) { TopDUContext* top = topContext(); if(!top->deleting() || !top->isOnDisk()) { DUCHAIN_D_DYNAMIC(DUContext); if(d->m_owner.declaration()) d->m_owner.declaration()->setInternalContext(nullptr); while( d->m_importersSize() != 0 ) { if(d->m_importers()[0].data()) d->m_importers()[0].data()->removeImportedParentContext(this); else { qCDebug(LANGUAGE) << "importer disappeared"; d->m_importersList().removeOne(d->m_importers()[0]); } } clearImportedParentContexts(); } deleteChildContextsRecursively(); if(!topContext()->deleting() || !topContext()->isOnDisk()) deleteUses(); deleteLocalDeclarations(); //If the top-context is being delete, we don't need to spend time rebuilding the inner structure. //That's expensive, especially when the data is not dynamic. if(!top->deleting() || !top->isOnDisk()) { if (m_dynamicData->m_parentContext) m_dynamicData->m_parentContext->m_dynamicData->removeChildContext(this); } top->m_dynamicData->clearContextIndex(this); Q_ASSERT(d_func()->isDynamic() == (!top->deleting() || !top->isOnDisk() || top->m_dynamicData->isTemporaryContextIndex(m_dynamicData->m_indexInTopContext))); delete m_dynamicData; } QVector< DUContext * > DUContext::childContexts( ) const { ENSURE_CAN_READ return m_dynamicData->m_childContexts; } Declaration* DUContext::owner() const { ENSURE_CAN_READ return d_func()->m_owner.declaration(); } void DUContext::setOwner(Declaration* owner) { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); if( owner == d->m_owner.declaration() ) return; Declaration* oldOwner = d->m_owner.declaration(); d->m_owner = owner; //Q_ASSERT(!oldOwner || oldOwner->internalContext() == this); if( oldOwner && oldOwner->internalContext() == this ) oldOwner->setInternalContext(nullptr); //The context set as internal context should always be the last opened context if( owner ) owner->setInternalContext(this); } DUContext* DUContext::parentContext( ) const { //ENSURE_CAN_READ Commented out for performance reasons return m_dynamicData->m_parentContext.data(); } void DUContext::setPropagateDeclarations(bool propagate) { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); if(propagate == d->m_propagateDeclarations) return; d->m_propagateDeclarations = propagate; } bool DUContext::isPropagateDeclarations() const { return d_func()->m_propagateDeclarations; } QList DUContext::findLocalDeclarations( const IndexedIdentifier& identifier, const CursorInRevision& position, const TopDUContext* topContext, const AbstractType::Ptr& dataType, SearchFlags flags ) const { ENSURE_CAN_READ DeclarationList ret; findLocalDeclarationsInternal(identifier, position.isValid() ? position : range().end, dataType, ret, topContext ? topContext : this->topContext(), flags); return ret; } QList DUContext::findLocalDeclarations( const Identifier& identifier, const CursorInRevision& position, const TopDUContext* topContext, const AbstractType::Ptr& dataType, SearchFlags flags ) const { return findLocalDeclarations(IndexedIdentifier(identifier), position, topContext, dataType, flags); } namespace { bool contextIsChildOrEqual(const DUContext* childContext, const DUContext* context) { if(childContext == context) return true; if(childContext->parentContext()) return contextIsChildOrEqual(childContext->parentContext(), context); else return false; } struct Checker { Checker(DUContext::SearchFlags flags, const AbstractType::Ptr& dataType, const CursorInRevision & position, DUContext::ContextType ownType) : m_flags(flags) , m_dataType(dataType) , m_position(position) , m_ownType(ownType) { } Declaration* check(Declaration* declaration) const { ///@todo This is C++-specific if (m_ownType != DUContext::Class && m_ownType != DUContext::Template && m_position.isValid() && m_position <= declaration->range().start) { return nullptr; } if (declaration->kind() == Declaration::Alias && !(m_flags & DUContext::DontResolveAliases)) { //Apply alias declarations AliasDeclaration* alias = static_cast(declaration); if (alias->aliasedDeclaration().isValid()) { declaration = alias->aliasedDeclaration().declaration(); } else { qCDebug(LANGUAGE) << "lost aliased declaration"; } } if (declaration->kind() == Declaration::NamespaceAlias && !(m_flags & DUContext::NoFiltering)) { return nullptr; } if ((m_flags & DUContext::OnlyFunctions) && !declaration->isFunctionDeclaration()) { return nullptr; } if (m_dataType && m_dataType->indexed() != declaration->indexedType()) { return nullptr; } return declaration; } DUContext::SearchFlags m_flags; const AbstractType::Ptr m_dataType; const CursorInRevision m_position; DUContext::ContextType m_ownType; }; } void DUContext::findLocalDeclarationsInternal(const Identifier& identifier, const CursorInRevision& position, const AbstractType::Ptr& dataType, DeclarationList& ret, const TopDUContext* source, SearchFlags flags) const { return findLocalDeclarationsInternal(IndexedIdentifier(identifier), position, dataType, ret, source, flags); } void DUContext::findLocalDeclarationsInternal( const IndexedIdentifier& identifier, const CursorInRevision & position, const AbstractType::Ptr& dataType, DeclarationList& ret, const TopDUContext* /*source*/, SearchFlags flags ) const { Checker checker(flags, dataType, position, type()); DUCHAIN_D(DUContext); if (d->m_inSymbolTable && !d->m_scopeIdentifier.isEmpty() && !identifier.isEmpty()) { //This context is in the symbol table, use the symbol-table to speed up the search QualifiedIdentifier id(scopeIdentifier(true) + identifier); TopDUContext* top = topContext(); uint count; const IndexedDeclaration* declarations; PersistentSymbolTable::self().declarations(id, count, declarations); for (uint a = 0; a < count; ++a) { ///@todo Eventually do efficient iteration-free filtering if (declarations[a].topContextIndex() == top->ownIndex()) { Declaration* decl = declarations[a].declaration(); if (decl && contextIsChildOrEqual(decl->context(), this)) { Declaration* checked = checker.check(decl); if (checked) { ret.append(checked); } } } } } else { //Iterate through all declarations DUContextDynamicData::VisibleDeclarationIterator it(m_dynamicData); while (it) { Declaration* declaration = *it; if (declaration && declaration->indexedIdentifier() == identifier) { Declaration* checked = checker.check(declaration); if (checked) ret.append(checked); } ++it; } } } bool DUContext::foundEnough( const DeclarationList& ret, SearchFlags flags ) const { if( !ret.isEmpty() && !(flags & DUContext::NoFiltering)) return true; else return false; } bool DUContext::findDeclarationsInternal( const SearchItem::PtrList & baseIdentifiers, const CursorInRevision & position, const AbstractType::Ptr& dataType, DeclarationList& ret, const TopDUContext* source, SearchFlags flags, uint depth ) const { if (depth > maxParentDepth) { qCDebug(LANGUAGE) << "maximum depth reached in" << scopeIdentifier(true); return false; } DUCHAIN_D(DUContext); if (d->m_contextType != Namespace) { // If we're in a namespace, delay all the searching into the top-context, because only that has the overview to pick the correct declarations. for (int a = 0; a < baseIdentifiers.size(); ++a) { if (!baseIdentifiers[a]->isExplicitlyGlobal && baseIdentifiers[a]->next.isEmpty()) { // It makes no sense searching locally for qualified identifiers findLocalDeclarationsInternal(baseIdentifiers[a]->identifier, position, dataType, ret, source, flags); } } if (foundEnough(ret, flags)) { return true; } } ///Step 1: Apply namespace-aliases and -imports SearchItem::PtrList aliasedIdentifiers; //Because of namespace-imports and aliases, this identifier may need to be searched under multiple names applyAliases(baseIdentifiers, aliasedIdentifiers, position, false, type() != DUContext::Namespace && type() != DUContext::Global); if (d->m_importedContextsSize() != 0) { ///Step 2: Give identifiers that are not marked as explicitly-global to imported contexts(explicitly global ones are treatead in TopDUContext) SearchItem::PtrList nonGlobalIdentifiers; foreach (const SearchItem::Ptr& identifier, aliasedIdentifiers) { if (!identifier->isExplicitlyGlobal) { nonGlobalIdentifiers << identifier; } } if (!nonGlobalIdentifiers.isEmpty()) { const auto& url = this->url(); for(int import = d->m_importedContextsSize()-1; import >= 0; --import ) { if (position.isValid() && d->m_importedContexts()[import].position.isValid() && position < d->m_importedContexts()[import].position) { continue; } DUContext* context = d->m_importedContexts()[import].context(source); if (!context) { continue; } else if (context == this) { qCDebug(LANGUAGE) << "resolved self as import:" << scopeIdentifier(true); continue; } if (!context->findDeclarationsInternal(nonGlobalIdentifiers, url == context->url() ? position : context->range().end, dataType, ret, source, flags | InImportedParentContext, depth+1)) { return false; } } } } if (foundEnough(ret, flags)) { return true; } ///Step 3: Continue search in parent-context if (!(flags & DontSearchInParent) && shouldSearchInParent(flags) && m_dynamicData->m_parentContext) { applyUpwardsAliases(aliasedIdentifiers, source); return m_dynamicData->m_parentContext->findDeclarationsInternal(aliasedIdentifiers, url() == m_dynamicData->m_parentContext->url() ? position : m_dynamicData->m_parentContext->range().end, dataType, ret, source, flags, depth); } return true; } QVector DUContext::fullyApplyAliases(const QualifiedIdentifier& id, const TopDUContext* source) const { ENSURE_CAN_READ if(!source) source = topContext(); SearchItem::PtrList identifiers; identifiers << SearchItem::Ptr(new SearchItem(id)); const DUContext* current = this; while(current) { SearchItem::PtrList aliasedIdentifiers; current->applyAliases(identifiers, aliasedIdentifiers, CursorInRevision::invalid(), true, false); current->applyUpwardsAliases(identifiers, source); current = current->parentContext(); } QVector ret; foreach (const SearchItem::Ptr& item, identifiers) ret += item->toList(); return ret; } QList DUContext::findDeclarations( const QualifiedIdentifier & identifier, const CursorInRevision & position, const AbstractType::Ptr& dataType, const TopDUContext* topContext, SearchFlags flags) const { ENSURE_CAN_READ DeclarationList ret; // optimize: we don't want to allocate the top node always // so create it on stack but ref it so its not deleted by the smart pointer SearchItem item(identifier); item.ref.ref(); SearchItem::PtrList identifiers{SearchItem::Ptr(&item)}; findDeclarationsInternal(identifiers, position.isValid() ? position : range().end, dataType, ret, topContext ? topContext : this->topContext(), flags, 0); return ret; } bool DUContext::imports(const DUContext* origin, const CursorInRevision& /*position*/ ) const { ENSURE_CAN_READ QSet recursionGuard; recursionGuard.reserve(8); return m_dynamicData->imports(origin, topContext(), &recursionGuard); } bool DUContext::addIndirectImport(const DUContext::Import& import) { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); for(unsigned int a = 0; a < d->m_importedContextsSize(); ++a) { if(d->m_importedContexts()[a] == import) { d->m_importedContextsList()[a].position = import.position; return true; } } ///Do not sort the imported contexts by their own line-number, it makes no sense. ///Contexts added first, aka template-contexts, should stay in first place, so they are searched first. d->m_importedContextsList().append(import); return false; } void DUContext::addImportedParentContext( DUContext * context, const CursorInRevision& position, bool anonymous, bool /*temporary*/ ) { ENSURE_CAN_WRITE if(context == this) { qCDebug(LANGUAGE) << "Tried to import self"; return; } if(!context) { qCDebug(LANGUAGE) << "Tried to import invalid context"; return; } Import import(context, this, position); if(addIndirectImport(import)) return; if( !anonymous ) { ENSURE_CAN_WRITE_(context) context->m_dynamicData->addImportedChildContext(this); } } void DUContext::removeImportedParentContext( DUContext * context ) { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); Import import(context, this, CursorInRevision::invalid()); for(unsigned int a = 0; a < d->m_importedContextsSize(); ++a) { if(d->m_importedContexts()[a] == import) { d->m_importedContextsList().remove(a); break; } } if( !context ) return; context->m_dynamicData->removeImportedChildContext(this); } KDevVarLengthArray DUContext::indexedImporters() const { KDevVarLengthArray ret; if(owner()) ret = Importers::self().importers(owner()->id()); //Add indirect importers to the list FOREACH_FUNCTION(const IndexedDUContext& ctx, d_func()->m_importers) ret.append(ctx); return ret; } QVector DUContext::importers() const { ENSURE_CAN_READ QVector ret; + ret.reserve(d_func()->m_importersSize()); FOREACH_FUNCTION(const IndexedDUContext& ctx, d_func()->m_importers) ret << ctx.context(); if(owner()) { //Add indirect importers to the list KDevVarLengthArray indirect = Importers::self().importers(owner()->id()); + ret.reserve(ret.size() + indirect.size()); foreach (const IndexedDUContext ctx, indirect) { ret << ctx.context(); } } return ret; } DUContext * DUContext::findContext( const CursorInRevision& position, DUContext* parent) const { ENSURE_CAN_READ if (!parent) parent = const_cast(this); foreach (DUContext* context, parent->m_dynamicData->m_childContexts) { if (context->range().contains(position)) { DUContext* ret = findContext(position, context); if (!ret) { ret = context; } return ret; } } return nullptr; } bool DUContext::parentContextOf(DUContext* context) const { if (this == context) return true; foreach (DUContext* child, m_dynamicData->m_childContexts) { if (child->parentContextOf(context)) { return true; } } return false; } QVector> DUContext::allDeclarations(const CursorInRevision& position, const TopDUContext* topContext, bool searchInParents) const { ENSURE_CAN_READ QVector> ret; QHash hadContexts; // Iterate back up the chain mergeDeclarationsInternal(ret, position, hadContexts, topContext ? topContext : this->topContext(), searchInParents); return ret; } QVector DUContext::localDeclarations(const TopDUContext* source) const { ENSURE_CAN_READ // TODO: remove this parameter once we kill old-cpp Q_UNUSED(source); return m_dynamicData->m_localDeclarations; } void DUContext::mergeDeclarationsInternal(QVector>& definitions, const CursorInRevision& position, QHash& hadContexts, const TopDUContext* source, bool searchInParents, int currentDepth) const { ENSURE_CAN_READ if((currentDepth > 300 && currentDepth < 1000) || currentDepth > 1300) { qCDebug(LANGUAGE) << "too much depth"; return; } DUCHAIN_D(DUContext); if(hadContexts.contains(this) && !searchInParents) return; if(!hadContexts.contains(this)) { hadContexts[this] = true; if( (type() == DUContext::Namespace || type() == DUContext::Global) && currentDepth < 1000 ) currentDepth += 1000; { DUContextDynamicData::VisibleDeclarationIterator it(m_dynamicData); while(it) { Declaration* decl = *it; if ( decl && (!position.isValid() || decl->range().start <= position) ) definitions << qMakePair(decl, currentDepth); ++it; } } for(int a = d->m_importedContextsSize()-1; a >= 0; --a) { const Import* import(&d->m_importedContexts()[a]); DUContext* context = import->context(source); while( !context && a > 0 ) { --a; import = &d->m_importedContexts()[a]; context = import->context(source); } if( !context ) break; if(context == this) { qCDebug(LANGUAGE) << "resolved self as import:" << scopeIdentifier(true); continue; } if( position.isValid() && import->position.isValid() && position < import->position ) continue; context->mergeDeclarationsInternal(definitions, CursorInRevision::invalid(), hadContexts, source, searchInParents && context->shouldSearchInParent(InImportedParentContext) && context->parentContext()->type() == DUContext::Helper, currentDepth+1); } } ///Only respect the position if the parent-context is not a class(@todo this is language-dependent) if (parentContext() && searchInParents ) parentContext()->mergeDeclarationsInternal(definitions, parentContext()->type() == DUContext::Class ? parentContext()->range().end : position, hadContexts, source, searchInParents, currentDepth+1); } void DUContext::deleteLocalDeclarations() { ENSURE_CAN_WRITE // It may happen that the deletion of one declaration triggers the deletion of another one // Therefore we copy the list of indexed declarations and work on those. Indexed declarations // will return zero for already deleted declarations. KDevVarLengthArray indexedLocal; if (d_func()->m_localDeclarations()) { indexedLocal.append(d_func()->m_localDeclarations(), d_func()->m_localDeclarationsSize()); } foreach (const LocalIndexedDeclaration& indexed, m_dynamicData->m_localDeclarations) { delete indexed.data(topContext()); } m_dynamicData->m_localDeclarations.clear(); } void DUContext::deleteChildContextsRecursively() { ENSURE_CAN_WRITE // note: don't use qDeleteAll here because child ctx deletion changes m_dynamicData->m_childContexts // also note: foreach iterates on a copy, so this is safe foreach (DUContext* ctx, m_dynamicData->m_childContexts) { delete ctx; } m_dynamicData->m_childContexts.clear(); } QVector DUContext::clearLocalDeclarations( ) { auto copy = m_dynamicData->m_localDeclarations; foreach (Declaration* dec, copy) { dec->setContext(nullptr); } return copy; } QualifiedIdentifier DUContext::scopeIdentifier(bool includeClasses) const { ENSURE_CAN_READ QualifiedIdentifier ret; m_dynamicData->scopeIdentifier(includeClasses, ret); return ret; } bool DUContext::equalScopeIdentifier(const DUContext* rhs) const { ENSURE_CAN_READ const DUContext* left = this; const DUContext* right = rhs; while(left || right) { if(!left || !right) return false; if(!(left->d_func()->m_scopeIdentifier == right->d_func()->m_scopeIdentifier)) return false; left = left->parentContext(); right = right->parentContext(); } return true; } void DUContext::setLocalScopeIdentifier(const QualifiedIdentifier & identifier) { ENSURE_CAN_WRITE bool wasInSymbolTable = inSymbolTable(); setInSymbolTable(false); d_func_dynamic()->m_scopeIdentifier = identifier; setInSymbolTable(wasInSymbolTable); } QualifiedIdentifier DUContext::localScopeIdentifier() const { //ENSURE_CAN_READ Commented out for performance reasons return d_func()->m_scopeIdentifier; } IndexedQualifiedIdentifier DUContext::indexedLocalScopeIdentifier() const { return d_func()->m_scopeIdentifier; } DUContext::ContextType DUContext::type() const { //ENSURE_CAN_READ This is disabled, because type() is called very often while searching, and it costs us performance return d_func()->m_contextType; } void DUContext::setType(ContextType type) { ENSURE_CAN_WRITE d_func_dynamic()->m_contextType = type; } QList DUContext::findDeclarations(const Identifier& identifier, const CursorInRevision& position, const TopDUContext* topContext, SearchFlags flags) const { return findDeclarations(IndexedIdentifier(identifier), position, topContext, flags); } QList DUContext::findDeclarations(const IndexedIdentifier& identifier, const CursorInRevision& position, const TopDUContext* topContext, SearchFlags flags) const { ENSURE_CAN_READ DeclarationList ret; SearchItem::PtrList identifiers; identifiers << SearchItem::Ptr(new SearchItem(false, identifier, SearchItem::PtrList())); findDeclarationsInternal(identifiers, position.isValid() ? position : range().end, AbstractType::Ptr(), ret, topContext ? topContext : this->topContext(), flags, 0); return ret; } void DUContext::deleteUse(int index) { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); d->m_usesList().remove(index); } void DUContext::deleteUses() { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); d->m_usesList().clear(); } void DUContext::deleteUsesRecursively() { deleteUses(); foreach (DUContext* childContext, m_dynamicData->m_childContexts) { childContext->deleteUsesRecursively(); } } bool DUContext::inDUChain() const { if( d_func()->m_anonymousInParent || !m_dynamicData->m_parentContext) return false; TopDUContext* top = topContext(); return top && top->inDUChain(); } DUContext* DUContext::specialize(const IndexedInstantiationInformation& /*specialization*/, const TopDUContext* topContext, int /*upDistance*/) { if(!topContext) return nullptr; return this; } CursorInRevision DUContext::importPosition(const DUContext* target) const { ENSURE_CAN_READ DUCHAIN_D(DUContext); Import import(const_cast(target), this, CursorInRevision::invalid()); for(unsigned int a = 0; a < d->m_importedContextsSize(); ++a) if(d->m_importedContexts()[a] == import) return d->m_importedContexts()[a].position; return CursorInRevision::invalid(); } QVector DUContext::importedParentContexts() const { ENSURE_CAN_READ QVector ret; ret.reserve(d_func()->m_importedContextsSize()); FOREACH_FUNCTION(const DUContext::Import& import, d_func()->m_importedContexts) ret << import; return ret; } void DUContext::applyAliases(const SearchItem::PtrList& baseIdentifiers, SearchItem::PtrList& identifiers, const CursorInRevision& position, bool canBeNamespace, bool onlyImports) const { DeclarationList imports; findLocalDeclarationsInternal(globalIndexedImportIdentifier(), position, AbstractType::Ptr(), imports, topContext(), DUContext::NoFiltering); if(imports.isEmpty() && onlyImports) { identifiers = baseIdentifiers; return; } for ( const SearchItem::Ptr& identifier : baseIdentifiers ) { bool addUnmodified = true; if( !identifier->isExplicitlyGlobal ) { if( !imports.isEmpty() ) { //We have namespace-imports. foreach ( Declaration* importDecl, imports ) { //Search for the identifier with the import-identifier prepended if(dynamic_cast(importDecl)) { NamespaceAliasDeclaration* alias = static_cast(importDecl); identifiers.append( SearchItem::Ptr( new SearchItem( alias->importIdentifier(), identifier ) ) ) ; }else{ qCDebug(LANGUAGE) << "Declaration with namespace alias identifier has the wrong type" << importDecl->url().str() << importDecl->range().castToSimpleRange(); } } } if( !identifier->isEmpty() && (identifier->hasNext() || canBeNamespace) ) { DeclarationList aliases; findLocalDeclarationsInternal(identifier->identifier, position, AbstractType::Ptr(), imports, nullptr, DUContext::NoFiltering); if(!aliases.isEmpty()) { //The first part of the identifier has been found as a namespace-alias. //In c++, we only need the first alias. However, just to be correct, follow them all for now. foreach ( Declaration* aliasDecl, aliases ) { if(!dynamic_cast(aliasDecl)) continue; addUnmodified = false; //The un-modified identifier can be ignored, because it will be replaced with the resolved alias NamespaceAliasDeclaration* alias = static_cast(aliasDecl); //Create an identifier where namespace-alias part is replaced with the alias target identifiers.append( SearchItem::Ptr( new SearchItem( alias->importIdentifier(), identifier->next ) ) ) ; } } } } if( addUnmodified ) identifiers.append(identifier); } } void DUContext::applyUpwardsAliases(SearchItem::PtrList& identifiers, const TopDUContext* /*source*/) const { if(type() == Namespace) { if(d_func()->m_scopeIdentifier.isEmpty()) return; //Make sure we search for the items in all namespaces of the same name, by duplicating each one with the namespace-identifier prepended. //We do this by prepending items to the current identifiers that equal the local scope identifier. SearchItem::Ptr newItem( new SearchItem(d_func()->m_scopeIdentifier.identifier()) ); //This will exclude explictly global identifiers newItem->addToEachNode( identifiers ); if(!newItem->next.isEmpty()) { //Prepend the full scope before newItem DUContext* parent = m_dynamicData->m_parentContext.data(); while(parent) { newItem = SearchItem::Ptr( new SearchItem(parent->d_func()->m_scopeIdentifier, newItem) ); parent = parent->m_dynamicData->m_parentContext.data(); } newItem->isExplicitlyGlobal = true; identifiers.insert(0, newItem); } } } bool DUContext::shouldSearchInParent(SearchFlags flags) const { return (parentContext() && parentContext()->type() == DUContext::Helper && (flags & InImportedParentContext)) || !(flags & InImportedParentContext); } const Use* DUContext::uses() const { ENSURE_CAN_READ return d_func()->m_uses(); } bool DUContext::declarationHasUses(Declaration* decl) { return DUChain::uses()->hasUses(decl->id()); } int DUContext::usesCount() const { return d_func()->m_usesSize(); } bool usesRangeLessThan(const Use& left, const Use& right) { return left.m_range.start < right.m_range.start; } int DUContext::createUse(int declarationIndex, const RangeInRevision& range, int insertBefore) { DUCHAIN_D_DYNAMIC(DUContext); ENSURE_CAN_WRITE Use use(range, declarationIndex); if(insertBefore == -1) { //Find position where to insert const unsigned int size = d->m_usesSize(); const Use* uses = d->m_uses(); const Use* lowerBound = std::lower_bound(uses, uses + size, use, usesRangeLessThan); insertBefore = lowerBound - uses; // comment out to test this: /* unsigned int a = 0; for(; a < size && range.start > uses[a].m_range.start; ++a) { } Q_ASSERT(a == insertBefore); */ } d->m_usesList().insert(insertBefore, use); return insertBefore; } void DUContext::changeUseRange(int useIndex, const RangeInRevision& range) { ENSURE_CAN_WRITE d_func_dynamic()->m_usesList()[useIndex].m_range = range; } void DUContext::setUseDeclaration(int useNumber, int declarationIndex) { ENSURE_CAN_WRITE d_func_dynamic()->m_usesList()[useNumber].m_declarationIndex = declarationIndex; } DUContext * DUContext::findContextAt(const CursorInRevision & position, bool includeRightBorder) const { ENSURE_CAN_READ // qCDebug(LANGUAGE) << "searchign" << position << "in:" << scopeIdentifier(true).toString() << range() << includeRightBorder; if (!range().contains(position) && (!includeRightBorder || range().end != position)) { // qCDebug(LANGUAGE) << "mismatch"; return nullptr; } const auto childContexts = m_dynamicData->m_childContexts; for(int a = childContexts.size() - 1; a >= 0; --a) { if (DUContext* specific = childContexts[a]->findContextAt(position, includeRightBorder)) { return specific; } } return const_cast(this); } Declaration * DUContext::findDeclarationAt(const CursorInRevision & position) const { ENSURE_CAN_READ if (!range().contains(position)) return nullptr; foreach (Declaration* child, m_dynamicData->m_localDeclarations) { if (child->range().contains(position)) { return child; } } return nullptr; } DUContext* DUContext::findContextIncluding(const RangeInRevision& range) const { ENSURE_CAN_READ if (!this->range().contains(range)) return nullptr; foreach (DUContext* child, m_dynamicData->m_childContexts) { if (DUContext* specific = child->findContextIncluding(range)) { return specific; } } return const_cast(this); } int DUContext::findUseAt(const CursorInRevision & position) const { ENSURE_CAN_READ if (!range().contains(position)) return -1; for(unsigned int a = 0; a < d_func()->m_usesSize(); ++a) if (d_func()->m_uses()[a].m_range.contains(position)) return a; return -1; } bool DUContext::inSymbolTable() const { return d_func()->m_inSymbolTable; } void DUContext::setInSymbolTable(bool inSymbolTable) { d_func_dynamic()->m_inSymbolTable = inSymbolTable; } void DUContext::clearImportedParentContexts() { ENSURE_CAN_WRITE DUCHAIN_D_DYNAMIC(DUContext); while( d->m_importedContextsSize() != 0 ) { DUContext* ctx = d->m_importedContexts()[0].context(nullptr, false); if(ctx) ctx->m_dynamicData->removeImportedChildContext(this); d->m_importedContextsList().removeOne(d->m_importedContexts()[0]); } } void DUContext::cleanIfNotEncountered(const QSet& encountered) { ENSURE_CAN_WRITE // It may happen that the deletion of one declaration triggers the deletion of another one // Therefore we copy the list of indexed declarations and work on those. Indexed declarations // will return zero for already deleted declarations. KDevVarLengthArray indexedLocal; if (d_func()->m_localDeclarations()) { indexedLocal.append(d_func()->m_localDeclarations(), d_func()->m_localDeclarationsSize()); } foreach (const LocalIndexedDeclaration& indexed, m_dynamicData->m_localDeclarations) { auto dec = indexed.data(topContext()); if (dec && !encountered.contains(dec) && (!dec->isAutoDeclaration() || !dec->hasUses())) { delete dec; } } foreach (DUContext* childContext, m_dynamicData->m_childContexts) { if (!encountered.contains(childContext)) { delete childContext; } } } TopDUContext* DUContext::topContext() const { return m_dynamicData->m_topContext; } QWidget* DUContext::createNavigationWidget(Declaration* decl, TopDUContext* topContext, const QString& htmlPrefix, const QString& htmlSuffix, AbstractNavigationWidget::DisplayHints hints) const { if (decl) { AbstractNavigationWidget* widget = new AbstractNavigationWidget; widget->setDisplayHints(hints); AbstractDeclarationNavigationContext* context = new AbstractDeclarationNavigationContext(DeclarationPointer(decl), TopDUContextPointer(topContext)); context->setPrefixSuffix(htmlPrefix, htmlSuffix); widget->setContext(NavigationContextPointer(context)); return widget; } else { return nullptr; } } QVector allUses(DUContext* context, int declarationIndex, bool noEmptyUses) { QVector ret; for(int a = 0; a < context->usesCount(); ++a) if(context->uses()[a].m_declarationIndex == declarationIndex) if(!noEmptyUses || !context->uses()[a].m_range.isEmpty()) ret << context->uses()[a].m_range; foreach(DUContext* child, context->childContexts()) ret += allUses(child, declarationIndex, noEmptyUses); return ret; } DUContext::SearchItem::SearchItem(const QualifiedIdentifier& id, const Ptr& nextItem, int start) : isExplicitlyGlobal(start == 0 ? id.explicitlyGlobal() : false) { if(!id.isEmpty()) { if(id.count() > start) identifier = id.indexedAt(start); if(id.count() > start+1) addNext(Ptr( new SearchItem(id, nextItem, start+1) )); else if(nextItem) next.append(nextItem); }else if(nextItem) { ///If there is no prefix, just copy nextItem isExplicitlyGlobal = nextItem->isExplicitlyGlobal; identifier = nextItem->identifier; next = nextItem->next; } } DUContext::SearchItem::SearchItem(const QualifiedIdentifier& id, const PtrList& nextItems, int start) : isExplicitlyGlobal(start == 0 ? id.explicitlyGlobal() : false) { if(id.count() > start) identifier = id.indexedAt(start); if(id.count() > start+1) addNext(Ptr( new SearchItem(id, nextItems, start+1) )); else next = nextItems; } DUContext::SearchItem::SearchItem(bool explicitlyGlobal, const IndexedIdentifier& id, const PtrList& nextItems) : isExplicitlyGlobal(explicitlyGlobal) , identifier(id) , next(nextItems) { } DUContext::SearchItem::SearchItem(bool explicitlyGlobal, const IndexedIdentifier& id, const Ptr& nextItem) : isExplicitlyGlobal(explicitlyGlobal) , identifier(id) { next.append(nextItem); } bool DUContext::SearchItem::match(const QualifiedIdentifier& id, int offset) const { if(id.isEmpty()) { if(identifier.isEmpty() && next.isEmpty()) return true; else return false; } if(id.at(offset) != identifier) //The identifier is different return false; if(offset == id.count()-1) { if(next.isEmpty()) return true; //match else return false; //id is too short } for(int a = 0; a < next.size(); ++a) if(next[a]->match(id, offset+1)) return true; return false; } bool DUContext::SearchItem::isEmpty() const { return identifier.isEmpty(); } bool DUContext::SearchItem::hasNext() const { return !next.isEmpty(); } QVector DUContext::SearchItem::toList(const QualifiedIdentifier& prefix) const { QVector ret; QualifiedIdentifier id = prefix; if(id.isEmpty()) id.setExplicitlyGlobal(isExplicitlyGlobal); if(!identifier.isEmpty()) id.push(identifier); if(next.isEmpty()) { ret << id; } else { for(int a = 0; a < next.size(); ++a) ret += next[a]->toList(id); } return ret; } void DUContext::SearchItem::addNext(const SearchItem::Ptr& other) { next.append(other); } void DUContext::SearchItem::addToEachNode(const SearchItem::Ptr& other) { if(other->isExplicitlyGlobal) return; next.append(other); for(int a = 0; a < next.size()-1; ++a) next[a]->addToEachNode(other); } void DUContext::SearchItem::addToEachNode(const SearchItem::PtrList& other) { int added = 0; for (const SearchItem::Ptr& o : other) { if(!o->isExplicitlyGlobal) { next.append(o); ++added; } } for(int a = 0; a < next.size()-added; ++a) next[a]->addToEachNode(other); } DUContext::Import::Import(DUContext* _context, const DUContext* importer, const CursorInRevision& _position) : position(_position) { if(_context && _context->owner() && (_context->owner()->specialization().index() || (importer && importer->topContext() != _context->topContext()))) { m_declaration = _context->owner()->id(); }else{ m_context = _context; } } DUContext::Import::Import(const DeclarationId& id, const CursorInRevision& _position) : position(_position) , m_declaration(id) { } DUContext* DUContext::Import::context(const TopDUContext* topContext, bool instantiateIfRequired) const { if(m_declaration.isValid()) { Declaration* decl = m_declaration.getDeclaration(topContext, instantiateIfRequired); //This first case rests on the assumption that no context will ever import a function's expression context //More accurately, that no specialized or cross-topContext imports will, but if the former assumption fails the latter will too if (AbstractFunctionDeclaration *functionDecl = dynamic_cast(decl)) { if (functionDecl->internalFunctionContext()) { return functionDecl->internalFunctionContext(); } else { qCWarning(LANGUAGE) << "Import of function declaration without internal function context encountered!"; } } if(decl) return decl->logicalInternalContext(topContext); else return nullptr; }else{ return m_context.data(); } } bool DUContext::Import::isDirect() const { return m_context.isValid(); } void DUContext::visit(DUChainVisitor& visitor) { ENSURE_CAN_READ visitor.visit(this); foreach (Declaration* decl, m_dynamicData->m_localDeclarations) { visitor.visit(decl); } foreach (DUContext* childContext, m_dynamicData->m_childContexts) { childContext->visit(visitor); } } static bool sortByRange(const DUChainBase* lhs, const DUChainBase* rhs) { return lhs->range() < rhs->range(); } void DUContext::resortLocalDeclarations() { ENSURE_CAN_WRITE std::sort(m_dynamicData->m_localDeclarations.begin(), m_dynamicData->m_localDeclarations.end(), sortByRange); auto top = topContext(); auto& declarations = d_func_dynamic()->m_localDeclarationsList(); std::sort(declarations.begin(), declarations.end(), [top] (const LocalIndexedDeclaration& lhs, const LocalIndexedDeclaration& rhs) { return lhs.data(top)->range() < rhs.data(top)->range(); }); } void DUContext::resortChildContexts() { ENSURE_CAN_WRITE std::sort(m_dynamicData->m_childContexts.begin(), m_dynamicData->m_childContexts.end(), sortByRange); auto top = topContext(); auto& contexts = d_func_dynamic()->m_childContextsList(); std::sort(contexts.begin(), contexts.end(), [top] (const LocalIndexedDUContext& lhs, const LocalIndexedDUContext& rhs) { return lhs.data(top)->range() < rhs.data(top)->range(); }); } } diff --git a/kdevplatform/language/duchain/identifier.cpp b/kdevplatform/language/duchain/identifier.cpp index 8f4a77d1bc..eea3848715 100644 --- a/kdevplatform/language/duchain/identifier.cpp +++ b/kdevplatform/language/duchain/identifier.cpp @@ -1,1600 +1,1593 @@ /* This file is part of KDevelop Copyright 2006 Hamish Rodda Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "identifier.h" #include #include "stringhelpers.h" #include "appendedlist_static.h" #include "serialization/itemrepository.h" #include "util/kdevhash.h" #include #include #include #define ifDebug(x) namespace KDevelop { template class IdentifierPrivate { public: IdentifierPrivate() { } template explicit IdentifierPrivate(const IdentifierPrivate& rhs) : m_unique(rhs.m_unique) , m_identifier(rhs.m_identifier) , m_refCount(0) , m_hash(rhs.m_hash) { copyListsFrom(rhs); } ~IdentifierPrivate() { templateIdentifiersList.free(const_cast(templateIdentifiers())); } //Flags the stored hash-value invalid void clearHash() { //This is always called on an object private to an Identifier, so there is no threading-problem. Q_ASSERT(dynamic); m_hash = 0; } uint hash() const { // Since this only needs reading and the data needs not to be private, this may be called by // multiple threads simultaneously, so computeHash() must be thread-safe. if( !m_hash && dynamic ) computeHash(); return m_hash; } int m_unique = 0; IndexedString m_identifier; uint m_refCount = 0; START_APPENDED_LISTS_STATIC(IdentifierPrivate) APPENDED_LIST_FIRST_STATIC(IndexedTypeIdentifier, templateIdentifiers) END_APPENDED_LISTS_STATIC(templateIdentifiers) uint itemSize() const { return sizeof(IdentifierPrivate) + lastOffsetBehind(); } void computeHash() const { Q_ASSERT(dynamic); //this must stay thread-safe(may be called by multiple threads at a time) //The thread-safety is given because all threads will have the same result, and it will only be written once at the end. KDevHash kdevhash; kdevhash << m_identifier.hash() << m_unique; FOREACH_FUNCTION_STATIC(const IndexedTypeIdentifier& templateIdentifier, templateIdentifiers) kdevhash << templateIdentifier.hash(); m_hash = kdevhash; } mutable uint m_hash = 0; }; typedef IdentifierPrivate DynamicIdentifierPrivate; typedef IdentifierPrivate ConstantIdentifierPrivate; struct IdentifierItemRequest { IdentifierItemRequest(const DynamicIdentifierPrivate& identifier) : m_identifier(identifier) { identifier.hash(); //Make sure the hash is valid by calling this } enum { AverageSize = sizeof(IdentifierPrivate)+4 }; //Should return the hash-value associated with this request(For example the hash of a string) uint hash() const { return m_identifier.hash(); } //Should return the size of an item created with createItem uint itemSize() const { return m_identifier.itemSize(); } //Should create an item where the information of the requested item is permanently stored. The pointer //@param item equals an allocated range with the size of itemSize(). void createItem(ConstantIdentifierPrivate* item) const { new (item) ConstantIdentifierPrivate(m_identifier); } static bool persistent(const ConstantIdentifierPrivate* item) { return (bool)item->m_refCount; } static void destroy(ConstantIdentifierPrivate* item, AbstractItemRepository&) { item->~ConstantIdentifierPrivate(); } //Should return whether the here requested item equals the given item bool equals(const ConstantIdentifierPrivate* item) const { return item->m_hash == m_identifier.m_hash && item->m_unique == m_identifier.m_unique && item->m_identifier == m_identifier.m_identifier && m_identifier.listsEqual(*item); } const DynamicIdentifierPrivate& m_identifier; }; using IdentifierRepository = RepositoryManager< ItemRepository, false>; static IdentifierRepository& identifierRepository() { static IdentifierRepository identifierRepositoryObject(QStringLiteral("Identifier Repository")); return identifierRepositoryObject; } static uint emptyConstantIdentifierPrivateIndex() { static const uint index = identifierRepository()->index(DynamicIdentifierPrivate()); return index; } static const ConstantIdentifierPrivate* emptyConstantIdentifierPrivate() { static const ConstantIdentifierPrivate item; return &item; } bool IndexedIdentifier::isEmpty() const { return index == emptyConstantIdentifierPrivateIndex(); } /** * Before something is modified in QualifiedIdentifierPrivate, it must be made sure that * it is private to the QualifiedIdentifier it is used in(@see QualifiedIdentifier::prepareWrite) */ template class QualifiedIdentifierPrivate { public: QualifiedIdentifierPrivate() : m_explicitlyGlobal(false) , m_isExpression(false) { } template explicit QualifiedIdentifierPrivate(const QualifiedIdentifierPrivate& rhs) : m_explicitlyGlobal(rhs.m_explicitlyGlobal) , m_isExpression(rhs.m_isExpression) , m_hash(rhs.m_hash) , m_refCount(0) { copyListsFrom(rhs); } ~QualifiedIdentifierPrivate() { identifiersList.free(const_cast(identifiers())); } bool m_explicitlyGlobal:1; bool m_isExpression:1; mutable uint m_hash = 0; uint m_refCount = 0; START_APPENDED_LISTS_STATIC(QualifiedIdentifierPrivate) APPENDED_LIST_FIRST_STATIC(IndexedIdentifier, identifiers) END_APPENDED_LISTS_STATIC(identifiers) uint itemSize() const { return sizeof(QualifiedIdentifierPrivate) + lastOffsetBehind(); } //Constructs m_identifiers void splitIdentifiers( const QString& str, int start ) { Q_ASSERT(dynamic); uint currentStart = start; while( currentStart < (uint)str.length() ) { identifiersList.append(IndexedIdentifier(Identifier( str, currentStart, ¤tStart ))); while (currentStart < (uint)str.length() && (str[currentStart] == QLatin1Char(' '))) ++currentStart; currentStart += 2; //Skip "::" } } inline void clearHash() const { m_hash = 0; } uint hash() const { if( m_hash == 0 ) { KDevHash hash; quint32 bitfields = static_cast(m_explicitlyGlobal) | (m_isExpression << 1); hash << bitfields << identifiersSize(); FOREACH_FUNCTION_STATIC( const IndexedIdentifier& identifier, identifiers ) { hash << identifier.getIndex(); } m_hash = hash; } return m_hash; } }; typedef QualifiedIdentifierPrivate DynamicQualifiedIdentifierPrivate; typedef QualifiedIdentifierPrivate ConstantQualifiedIdentifierPrivate; struct QualifiedIdentifierItemRequest { QualifiedIdentifierItemRequest(const DynamicQualifiedIdentifierPrivate& identifier) : m_identifier(identifier) { identifier.hash(); //Make sure the hash is valid by calling this } enum { AverageSize = sizeof(QualifiedIdentifierPrivate)+8 }; //Should return the hash-value associated with this request(For example the hash of a string) uint hash() const { return m_identifier.hash(); } //Should return the size of an item created with createItem uint itemSize() const { return m_identifier.itemSize(); } /** * Should create an item where the information of the requested item is permanently stored. The pointer * @param item equals an allocated range with the size of itemSize(). */ void createItem(ConstantQualifiedIdentifierPrivate* item) const { Q_ASSERT(shouldDoDUChainReferenceCounting(item)); Q_ASSERT(shouldDoDUChainReferenceCounting(((char*)item) + (itemSize()-1))); new (item) ConstantQualifiedIdentifierPrivate(m_identifier); } static bool persistent(const ConstantQualifiedIdentifierPrivate* item) { return (bool)item->m_refCount; } static void destroy(ConstantQualifiedIdentifierPrivate* item, AbstractItemRepository&) { Q_ASSERT(shouldDoDUChainReferenceCounting(item)); item->~ConstantQualifiedIdentifierPrivate(); } //Should return whether the here requested item equals the given item bool equals(const ConstantQualifiedIdentifierPrivate* item) const { return item->m_explicitlyGlobal == m_identifier.m_explicitlyGlobal && item->m_isExpression == m_identifier.m_isExpression && item->m_hash == m_identifier.m_hash && m_identifier.listsEqual(*item); } const DynamicQualifiedIdentifierPrivate& m_identifier; }; using QualifiedIdentifierRepository = RepositoryManager< ItemRepository, false>; static QualifiedIdentifierRepository& qualifiedidentifierRepository() { static QualifiedIdentifierRepository repo(QStringLiteral("Qualified Identifier Repository"), 1, [] () -> AbstractRepositoryManager* { return &identifierRepository(); }); return repo; } static uint emptyConstantQualifiedIdentifierPrivateIndex() { static const uint index = qualifiedidentifierRepository()->index(DynamicQualifiedIdentifierPrivate()); return index; } static const ConstantQualifiedIdentifierPrivate* emptyConstantQualifiedIdentifierPrivate() { static const ConstantQualifiedIdentifierPrivate item; return &item; } Identifier::Identifier(const Identifier& rhs) { rhs.makeConstant(); cd = rhs.cd; m_index = rhs.m_index; } Identifier::Identifier(uint index) : m_index(index) { Q_ASSERT(m_index); cd = identifierRepository()->itemFromIndex(index); } Identifier::Identifier(const IndexedString& str) { if (str.isEmpty()) { m_index = emptyConstantIdentifierPrivateIndex(); cd = emptyConstantIdentifierPrivate(); } else { m_index = 0; dd = new IdentifierPrivate; dd->m_identifier = str; } } Identifier::Identifier(const QString& id, uint start, uint* takenRange) { if (id.isEmpty()) { m_index = emptyConstantIdentifierPrivateIndex(); cd = emptyConstantIdentifierPrivate(); return; } m_index = 0; dd = new IdentifierPrivate; ///Extract template-parameters ParamIterator paramIt(QStringLiteral("<>:"), id, start); dd->m_identifier = IndexedString(paramIt.prefix().trimmed()); while( paramIt ) { appendTemplateIdentifier( IndexedTypeIdentifier(IndexedQualifiedIdentifier(QualifiedIdentifier(*paramIt))) ); ++paramIt; } if( takenRange ) *takenRange = paramIt.position(); } Identifier::Identifier() : m_index(emptyConstantIdentifierPrivateIndex()) , cd(emptyConstantIdentifierPrivate()) { } Identifier& Identifier::operator=(const Identifier& rhs) { if(dd == rhs.dd && cd == rhs.cd) return *this; if(!m_index) delete dd; dd = nullptr; rhs.makeConstant(); cd = rhs.cd; m_index = rhs.m_index; Q_ASSERT(cd); return *this; } Identifier::Identifier(Identifier&& rhs) Q_DECL_NOEXCEPT : m_index(rhs.m_index) { if (m_index) { cd = rhs.cd; } else { dd = rhs.dd; } rhs.cd = emptyConstantIdentifierPrivate(); rhs.m_index = emptyConstantIdentifierPrivateIndex(); } Identifier& Identifier::operator=(Identifier&& rhs) Q_DECL_NOEXCEPT { if(dd == rhs.dd && cd == rhs.cd) return *this; if (!m_index) { delete dd; dd = nullptr; } m_index = rhs.m_index; if (m_index) { cd = rhs.cd; } else { dd = rhs.dd; } rhs.cd = emptyConstantIdentifierPrivate(); rhs.m_index = emptyConstantIdentifierPrivateIndex(); return *this; } Identifier::~Identifier() { if(!m_index) delete dd; } bool Identifier::nameEquals(const Identifier& rhs) const { return identifier() == rhs.identifier(); } uint Identifier::hash() const { if(!m_index) return dd->hash(); else return cd->hash(); } bool Identifier::isEmpty() const { if(!m_index) return dd->m_identifier.isEmpty() && dd->m_unique == 0 && dd->templateIdentifiersSize() == 0; else return cd->m_identifier.isEmpty() && cd->m_unique == 0 && cd->templateIdentifiersSize() == 0; } Identifier Identifier::unique(int token) { Identifier ret; ret.setUnique(token); return ret; } bool Identifier::isUnique() const { if(!m_index) return dd->m_unique; else return cd->m_unique; } int Identifier::uniqueToken() const { if(!m_index) return dd->m_unique; else return cd->m_unique; } void Identifier::setUnique(int token) { if (token != uniqueToken()) { prepareWrite(); dd->m_unique = token; } } const IndexedString Identifier::identifier() const { if(!m_index) return dd->m_identifier; else return cd->m_identifier; } void Identifier::setIdentifier(const QString& identifier) { IndexedString id(identifier); if (id != this->identifier()) { prepareWrite(); dd->m_identifier = std::move(id); } } void Identifier::setIdentifier(const IndexedString& identifier) { if (identifier != this->identifier()) { prepareWrite(); dd->m_identifier = identifier; } } IndexedTypeIdentifier Identifier::templateIdentifier(int num) const { if(!m_index) return dd->templateIdentifiers()[num]; else return cd->templateIdentifiers()[num]; } uint Identifier::templateIdentifiersCount() const { if(!m_index) return dd->templateIdentifiersSize(); else return cd->templateIdentifiersSize(); } void Identifier::appendTemplateIdentifier(const IndexedTypeIdentifier& identifier) { prepareWrite(); dd->templateIdentifiersList.append(identifier); } void Identifier::clearTemplateIdentifiers() { prepareWrite(); dd->templateIdentifiersList.clear(); } uint Identifier::index() const { makeConstant(); Q_ASSERT(m_index); return m_index; } bool Identifier::inRepository() const { return m_index; } void Identifier::setTemplateIdentifiers(const QList& templateIdentifiers) { prepareWrite(); dd->templateIdentifiersList.clear(); foreach(const IndexedTypeIdentifier& id, templateIdentifiers) dd->templateIdentifiersList.append(id); } QString Identifier::toString(IdentifierStringFormattingOptions options) const { QString ret = identifier().str(); if (!options.testFlag(RemoveTemplateInformation) && templateIdentifiersCount()) { - ret.append(QStringLiteral("< ")); + QStringList templateIds; + templateIds.reserve(templateIdentifiersCount()); for (uint i = 0; i < templateIdentifiersCount(); ++i) { - ret.append(templateIdentifier(i).toString(options)); - if (i != templateIdentifiersCount() - 1) - ret.append(QStringLiteral(", ")); + templateIds.append(templateIdentifier(i).toString(options)); } - ret.append(QStringLiteral(" >")); + ret += QStringLiteral("< ") + templateIds.join(QStringLiteral(", ")) + QStringLiteral(" >"); } return ret; } bool Identifier::operator==(const Identifier& rhs) const { return index() == rhs.index(); } bool Identifier::operator!=(const Identifier& rhs) const { return !operator==(rhs); } uint QualifiedIdentifier::index() const { makeConstant(); Q_ASSERT(m_index); return m_index; } void Identifier::makeConstant() const { if(m_index) return; m_index = identifierRepository()->index( IdentifierItemRequest(*dd) ); delete dd; cd = identifierRepository()->itemFromIndex( m_index ); } void Identifier::prepareWrite() { if(m_index) { const IdentifierPrivate* oldCc = cd; dd = new IdentifierPrivate; dd->m_hash = oldCc->m_hash; dd->m_unique = oldCc->m_unique; dd->m_identifier = oldCc->m_identifier; dd->copyListsFrom(*oldCc); m_index = 0; } dd->clearHash(); } bool QualifiedIdentifier::inRepository() const { if(m_index) return true; else return (bool)qualifiedidentifierRepository()->findIndex( QualifiedIdentifierItemRequest(*dd) ); } QualifiedIdentifier::QualifiedIdentifier(uint index) : m_index(index) , cd( qualifiedidentifierRepository()->itemFromIndex(index) ) { } QualifiedIdentifier::QualifiedIdentifier(const QString& id, bool isExpression) { if (id.isEmpty()) { m_index = emptyConstantQualifiedIdentifierPrivateIndex(); cd = emptyConstantQualifiedIdentifierPrivate(); return; } m_index = 0; dd = new DynamicQualifiedIdentifierPrivate; if(isExpression) { setIsExpression(true); if(!id.isEmpty()) { //Prevent tokenization, since we may lose information there Identifier finishedId; finishedId.setIdentifier(id); push(finishedId); } }else{ if (id.startsWith(QStringLiteral("::"))) { dd->m_explicitlyGlobal = true; dd->splitIdentifiers(id, 2); } else { dd->m_explicitlyGlobal = false; dd->splitIdentifiers(id, 0); } } } QualifiedIdentifier::QualifiedIdentifier(const Identifier& id) { if (id.isEmpty()) { m_index = emptyConstantQualifiedIdentifierPrivateIndex(); cd = emptyConstantQualifiedIdentifierPrivate(); return; } m_index = 0; dd = new DynamicQualifiedIdentifierPrivate; if (id.dd->m_identifier.str().isEmpty()) { dd->m_explicitlyGlobal = true; } else { dd->m_explicitlyGlobal = false; dd->identifiersList.append(IndexedIdentifier(id)); } } QualifiedIdentifier::QualifiedIdentifier() : m_index(emptyConstantQualifiedIdentifierPrivateIndex()) , cd(emptyConstantQualifiedIdentifierPrivate()) { } QualifiedIdentifier::QualifiedIdentifier(const QualifiedIdentifier& id) { if(id.m_index) { m_index = id.m_index; cd = id.cd; }else{ m_index = 0; dd = new QualifiedIdentifierPrivate(*id.dd); } } QualifiedIdentifier::QualifiedIdentifier(QualifiedIdentifier&& rhs) Q_DECL_NOEXCEPT : m_index(rhs.m_index) { if (m_index) { cd = rhs.cd; } else { dd = rhs.dd; } rhs.m_index = emptyConstantQualifiedIdentifierPrivateIndex(); rhs.cd = emptyConstantQualifiedIdentifierPrivate(); } QualifiedIdentifier& QualifiedIdentifier::operator=(const QualifiedIdentifier& rhs) { if(dd == rhs.dd && cd == rhs.cd) return *this; if(!m_index) delete dd; rhs.makeConstant(); cd = rhs.cd; m_index = rhs.m_index; return *this; } QualifiedIdentifier& QualifiedIdentifier::operator=(QualifiedIdentifier&& rhs) Q_DECL_NOEXCEPT { if(!m_index) delete dd; m_index = rhs.m_index; if (m_index) { cd = rhs.cd; } else { dd = rhs.dd; } rhs.cd = emptyConstantQualifiedIdentifierPrivate(); rhs.m_index = emptyConstantQualifiedIdentifierPrivateIndex(); return *this; } QualifiedIdentifier::~QualifiedIdentifier() { if(!m_index) delete dd; } QStringList QualifiedIdentifier::toStringList(IdentifierStringFormattingOptions options) const { QStringList ret; ret.reserve(explicitlyGlobal() + count()); if (explicitlyGlobal()) ret.append(QString()); if(m_index) { + ret.reserve(ret.size() + cd->identifiersSize()); FOREACH_FUNCTION_STATIC(const IndexedIdentifier& index, cd->identifiers) ret << index.identifier().toString(options); }else{ + ret.reserve(ret.size() + dd->identifiersSize()); FOREACH_FUNCTION_STATIC(const IndexedIdentifier& index, dd->identifiers) ret << index.identifier().toString(options); } return ret; } QString QualifiedIdentifier::toString(IdentifierStringFormattingOptions options) const { const QString doubleColon = QStringLiteral("::"); QString ret; if( !options.testFlag(RemoveExplicitlyGlobalPrefix) && explicitlyGlobal() ) ret = doubleColon; - bool first = true; + QStringList identifiers; if(m_index) { + identifiers.reserve(cd->identifiersSize()); FOREACH_FUNCTION_STATIC(const IndexedIdentifier& index, cd->identifiers) { - if( !first ) - ret += doubleColon; - else - first = false; - - ret += index.identifier().toString(options); + identifiers += index.identifier().toString(options); } }else{ + identifiers.reserve(dd->identifiersSize()); FOREACH_FUNCTION_STATIC(const IndexedIdentifier& index, dd->identifiers) { - if( !first ) - ret += doubleColon; - else - first = false; - - ret += index.identifier().toString(options); + identifiers += index.identifier().toString(options); } } - return ret; + return ret + identifiers.join(doubleColon); } QualifiedIdentifier QualifiedIdentifier::merge(const QualifiedIdentifier& base) const { QualifiedIdentifier ret(base); ret.push(*this); return ret; } QualifiedIdentifier QualifiedIdentifier::operator+(const QualifiedIdentifier& rhs) const { return rhs.merge(*this); } QualifiedIdentifier& QualifiedIdentifier::operator+=(const QualifiedIdentifier& rhs) { push(rhs); return *this; } QualifiedIdentifier QualifiedIdentifier::operator+(const Identifier& rhs) const { QualifiedIdentifier ret(*this); ret.push(rhs); return ret; } QualifiedIdentifier& QualifiedIdentifier::operator+=(const Identifier& rhs) { push(rhs); return *this; } QualifiedIdentifier QualifiedIdentifier::operator+(const IndexedIdentifier& rhs) const { QualifiedIdentifier ret(*this); ret.push(rhs); return ret; } QualifiedIdentifier& QualifiedIdentifier::operator+=(const IndexedIdentifier& rhs) { push(rhs); return *this; } bool QualifiedIdentifier::isExpression() const { if(m_index) return cd->m_isExpression; else return dd->m_isExpression; } void QualifiedIdentifier::setIsExpression(bool is) { if (is != isExpression()) { prepareWrite(); dd->m_isExpression = is; } } bool QualifiedIdentifier::explicitlyGlobal() const { // True if started with "::" if(m_index) return cd->m_explicitlyGlobal; else return dd->m_explicitlyGlobal; } void QualifiedIdentifier::setExplicitlyGlobal(bool eg) { if (eg != explicitlyGlobal()) { prepareWrite(); dd->m_explicitlyGlobal = eg; } } bool QualifiedIdentifier::sameIdentifiers(const QualifiedIdentifier& rhs) const { if(m_index && rhs.m_index) return cd->listsEqual(*rhs.cd); else if(m_index && !rhs.m_index) return cd->listsEqual(*rhs.dd); else if(!m_index && !rhs.m_index) return dd->listsEqual(*rhs.dd); else return dd->listsEqual(*rhs.cd); } bool QualifiedIdentifier::operator==(const QualifiedIdentifier& rhs) const { if( cd == rhs.cd ) return true; return hash() == rhs.hash() && sameIdentifiers(rhs); } bool QualifiedIdentifier::operator!=(const QualifiedIdentifier& rhs) const { return !operator==(rhs); } bool QualifiedIdentifier::beginsWith(const QualifiedIdentifier& other) const { uint c = count(); uint oc = other.count(); for (uint i = 0; i < c && i < oc; ++i) if (at(i) == other.at(i)) { continue; } else { return false; } return true; } struct Visitor { Visitor(KDevVarLengthArray& target, uint hash) : target(target) , hash(hash) { } bool operator()(const ConstantQualifiedIdentifierPrivate* item, uint index) const { if(item->m_hash == hash) target.append(QualifiedIdentifier(index)); return true; } KDevVarLengthArray& target; const uint hash; }; uint QualifiedIdentifier::hash() const { if(m_index) return cd->hash(); else return dd->hash(); } uint qHash(const IndexedTypeIdentifier& id) { return id.hash(); } uint qHash(const QualifiedIdentifier& id) { return id.hash(); } uint qHash(const Identifier& id) { return id.hash(); } bool QualifiedIdentifier::isQualified() const { return count() > 1 || explicitlyGlobal(); } void QualifiedIdentifier::push(const Identifier& id) { if(id.isEmpty()) return; push(IndexedIdentifier(id)); } void QualifiedIdentifier::push(const IndexedIdentifier& id) { if (id.isEmpty()) { return; } prepareWrite(); dd->identifiersList.append(id); } void QualifiedIdentifier::push(const QualifiedIdentifier& id) { if (id.isEmpty()) { return; } prepareWrite(); if (id.m_index) { dd->identifiersList.append(id.cd->identifiers(), id.cd->identifiersSize()); } else { dd->identifiersList.append(id.dd->identifiers(), id.dd->identifiersSize()); } if (id.explicitlyGlobal()) { setExplicitlyGlobal(true); } } void QualifiedIdentifier::pop() { prepareWrite(); if(!dd->identifiersSize()) return; dd->identifiersList.resize(dd->identifiersList.size()-1); } void QualifiedIdentifier::clear() { prepareWrite(); dd->identifiersList.clear(); dd->m_explicitlyGlobal = false; dd->m_isExpression = false; } bool QualifiedIdentifier::isEmpty() const { if(m_index) return cd->identifiersSize() == 0; else return dd->identifiersSize() == 0; } int QualifiedIdentifier::count() const { if(m_index) return cd->identifiersSize(); else return dd->identifiersSize(); } Identifier QualifiedIdentifier::first() const { return indexedFirst().identifier(); } IndexedIdentifier QualifiedIdentifier::indexedFirst() const { if( (m_index && cd->identifiersSize() == 0) || (!m_index && dd->identifiersSize() == 0) ) return IndexedIdentifier(); else return indexedAt(0); } Identifier QualifiedIdentifier::last() const { return indexedLast().identifier(); } IndexedIdentifier QualifiedIdentifier::indexedLast() const { uint c = count(); if(c) return indexedAt(c-1); else return IndexedIdentifier(); } Identifier QualifiedIdentifier::top() const { return last(); } QualifiedIdentifier QualifiedIdentifier::mid(int pos, int len) const { QualifiedIdentifier ret; if( pos == 0 ) ret.setExplicitlyGlobal(explicitlyGlobal()); int cnt = (int)count(); if( len == -1 ) len = cnt - pos; if( pos+len > cnt ) len -= cnt - (pos+len); for( int a = pos; a < pos+len; a++ ) ret.push(at(a)); return ret; } Identifier QualifiedIdentifier::at(int i) const { return indexedAt(i).identifier(); } IndexedIdentifier QualifiedIdentifier::indexedAt(int i) const { if (m_index) { Q_ASSERT(i >= 0 && i < (int)cd->identifiersSize()); return cd->identifiers()[i]; } else { Q_ASSERT(i >= 0 && i < (int)dd->identifiersSize()); return dd->identifiers()[i]; } } void QualifiedIdentifier::makeConstant() const { if(m_index) return; m_index = qualifiedidentifierRepository()->index( QualifiedIdentifierItemRequest(*dd) ); delete dd; cd = qualifiedidentifierRepository()->itemFromIndex( m_index ); } void QualifiedIdentifier::prepareWrite() { if(m_index) { const QualifiedIdentifierPrivate* oldCc = cd; dd = new QualifiedIdentifierPrivate; dd->m_explicitlyGlobal = oldCc->m_explicitlyGlobal; dd->m_isExpression = oldCc->m_isExpression; dd->m_hash = oldCc->m_hash; dd->copyListsFrom(*oldCc); m_index = 0; } dd->clearHash(); } uint IndexedTypeIdentifier::hash() const { quint32 bitfields = static_cast(m_isConstant) | (m_isReference << 1) | (m_isRValue << 2) | (m_isVolatile << 3) | (m_pointerDepth << 4) | (m_pointerConstMask << 9); return KDevHash() << m_identifier.getIndex() << bitfields; } bool IndexedTypeIdentifier::operator==(const IndexedTypeIdentifier& rhs) const { return m_identifier == rhs.m_identifier && m_isConstant == rhs.m_isConstant && m_isReference == rhs.m_isReference && m_isRValue == rhs.m_isRValue && m_isVolatile == rhs.m_isVolatile && m_pointerConstMask == rhs.m_pointerConstMask && m_pointerDepth == rhs.m_pointerDepth; } bool IndexedTypeIdentifier::operator!=(const IndexedTypeIdentifier& rhs) const { return !operator==(rhs); } bool IndexedTypeIdentifier::isReference() const { return m_isReference; } void IndexedTypeIdentifier::setIsReference(bool isRef) { m_isReference = isRef; } bool IndexedTypeIdentifier::isRValue() const { return m_isRValue; } void IndexedTypeIdentifier::setIsRValue(bool isRVal) { m_isRValue = isRVal; } bool IndexedTypeIdentifier::isConstant() const { return m_isConstant; } void IndexedTypeIdentifier::setIsConstant(bool isConst) { m_isConstant = isConst; } bool IndexedTypeIdentifier::isVolatile() const { return m_isVolatile; } void IndexedTypeIdentifier::setIsVolatile(bool isVolatile) { m_isVolatile = isVolatile; } int IndexedTypeIdentifier::pointerDepth() const { return m_pointerDepth; } void IndexedTypeIdentifier::setPointerDepth(int depth) { Q_ASSERT(depth <= 23 && depth >= 0); ///Clear the mask in removed fields for(int s = depth; s < (int)m_pointerDepth; ++s) setIsConstPointer(s, false); m_pointerDepth = depth; } bool IndexedTypeIdentifier::isConstPointer(int depthNumber) const { return m_pointerConstMask & (1 << depthNumber); } void IndexedTypeIdentifier::setIsConstPointer(int depthNumber, bool constant) { if(constant) m_pointerConstMask |= (1 << depthNumber); else m_pointerConstMask &= (~(1 << depthNumber)); } QString IndexedTypeIdentifier::toString(IdentifierStringFormattingOptions options) const { QString ret; if(isConstant()) ret += QLatin1String("const "); if(isVolatile()) ret += QLatin1String("volatile "); ret += m_identifier.identifier().toString(options); for(int a = 0; a < pointerDepth(); ++a) { ret += QLatin1Char('*'); if( isConstPointer(a) ) ret += QLatin1String("const"); } if(isRValue()) ret += QLatin1String("&&"); else if(isReference()) ret += QLatin1Char('&'); return ret; } IndexedTypeIdentifier::IndexedTypeIdentifier(const IndexedQualifiedIdentifier& identifier) : m_identifier(identifier) , m_isConstant(false) , m_isReference(false) , m_isRValue(false) , m_isVolatile(false) , m_pointerDepth(0) , m_pointerConstMask(0) { } IndexedTypeIdentifier::IndexedTypeIdentifier(const QString& identifier, bool isExpression) : m_identifier(QualifiedIdentifier(identifier, isExpression)) , m_isConstant(false) , m_isReference(false) , m_isRValue(false) , m_isVolatile(false) , m_pointerDepth(0) , m_pointerConstMask(0) { } IndexedIdentifier::IndexedIdentifier() : index(emptyConstantIdentifierPrivateIndex()) { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedIdentifier::IndexedIdentifier(const Identifier& id) : index(id.index()) { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedIdentifier::IndexedIdentifier(const IndexedIdentifier& rhs) : index(rhs.index) { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedIdentifier::IndexedIdentifier(IndexedIdentifier&& rhs) Q_DECL_NOEXCEPT : index(rhs.index) { rhs.index = emptyConstantIdentifierPrivateIndex(); } IndexedIdentifier::~IndexedIdentifier() { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); decrease(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedIdentifier& IndexedIdentifier::operator=(const Identifier& id) { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); decrease(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } index = id.index(); if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } return *this; } IndexedIdentifier& IndexedIdentifier::operator=(IndexedIdentifier&& rhs) Q_DECL_NOEXCEPT { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } else if (shouldDoDUChainReferenceCounting(&rhs)) { QMutexLocker lock(identifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(identifierRepository()->dynamicItemFromIndexSimple(rhs.index)->m_refCount, rhs.index); } index = rhs.index; rhs.index = emptyConstantIdentifierPrivateIndex(); if(shouldDoDUChainReferenceCounting(this) && !(shouldDoDUChainReferenceCounting(&rhs))) { QMutexLocker lock(identifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "increasing"; ) increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } return *this; } IndexedIdentifier& IndexedIdentifier::operator=(const IndexedIdentifier& id) { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); decrease(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } index = id.index; if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(identifierRepository()->mutex()); increase(identifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } return *this; } bool IndexedIdentifier::operator==(const IndexedIdentifier& rhs) const { return index == rhs.index; } bool IndexedIdentifier::operator!=(const IndexedIdentifier& rhs) const { return index != rhs.index; } bool IndexedIdentifier::operator==(const Identifier& id) const { return index == id.index(); } Identifier IndexedIdentifier::identifier() const { return Identifier(index); } IndexedIdentifier::operator Identifier() const { return Identifier(index); } bool IndexedQualifiedIdentifier::isValid() const { return index != emptyConstantQualifiedIdentifierPrivateIndex(); } bool IndexedQualifiedIdentifier::isEmpty() const { return index == emptyConstantQualifiedIdentifierPrivateIndex(); } int cnt = 0; IndexedQualifiedIdentifier IndexedTypeIdentifier::identifier() const { return m_identifier; } void IndexedTypeIdentifier::setIdentifier(const IndexedQualifiedIdentifier& id) { m_identifier = id; } IndexedQualifiedIdentifier::IndexedQualifiedIdentifier() : index(emptyConstantQualifiedIdentifierPrivateIndex()) { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { ifDebug( qCDebug(LANGUAGE) << "increasing"; ) //qCDebug(LANGUAGE) << "(" << ++cnt << ")" << this << identifier().toString() << "inc" << index; QMutexLocker lock(qualifiedidentifierRepository()->mutex()); increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedQualifiedIdentifier::IndexedQualifiedIdentifier(const QualifiedIdentifier& id) : index(id.index()) { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { ifDebug( qCDebug(LANGUAGE) << "increasing"; ) QMutexLocker lock(qualifiedidentifierRepository()->mutex()); increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedQualifiedIdentifier::IndexedQualifiedIdentifier(const IndexedQualifiedIdentifier& id) : index(id.index) { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { ifDebug( qCDebug(LANGUAGE) << "increasing"; ) QMutexLocker lock(qualifiedidentifierRepository()->mutex()); increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } IndexedQualifiedIdentifier::IndexedQualifiedIdentifier(IndexedQualifiedIdentifier&& rhs) Q_DECL_NOEXCEPT : index(rhs.index) { rhs.index = emptyConstantQualifiedIdentifierPrivateIndex(); } IndexedQualifiedIdentifier& IndexedQualifiedIdentifier::operator=(const QualifiedIdentifier& id) { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(qualifiedidentifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); index = id.index(); ifDebug( qCDebug(LANGUAGE) << index << "increasing"; ) increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } else { index = id.index(); } return *this; } IndexedQualifiedIdentifier& IndexedQualifiedIdentifier::operator=(const IndexedQualifiedIdentifier& rhs) { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(qualifiedidentifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); index = rhs.index; ifDebug( qCDebug(LANGUAGE) << index << "increasing"; ) increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } else { index = rhs.index; } return *this; } IndexedQualifiedIdentifier& IndexedQualifiedIdentifier::operator=(IndexedQualifiedIdentifier&& rhs) Q_DECL_NOEXCEPT { if(shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(qualifiedidentifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } else if (shouldDoDUChainReferenceCounting(&rhs)) { QMutexLocker lock(qualifiedidentifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "decreasing"; ) decrease(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(rhs.index)->m_refCount, rhs.index); } index = rhs.index; rhs.index = emptyConstantQualifiedIdentifierPrivateIndex(); if(shouldDoDUChainReferenceCounting(this) && !(shouldDoDUChainReferenceCounting(&rhs))) { QMutexLocker lock(qualifiedidentifierRepository()->mutex()); ifDebug( qCDebug(LANGUAGE) << "increasing"; ) increase(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } return *this; } IndexedQualifiedIdentifier::~IndexedQualifiedIdentifier() { ifDebug( qCDebug(LANGUAGE) << "(" << ++cnt << ")" << identifier().toString() << index; ) if(shouldDoDUChainReferenceCounting(this)) { ifDebug( qCDebug(LANGUAGE) << index << "decreasing"; ) QMutexLocker lock(qualifiedidentifierRepository()->mutex()); decrease(qualifiedidentifierRepository()->dynamicItemFromIndexSimple(index)->m_refCount, index); } } bool IndexedQualifiedIdentifier::operator==(const IndexedQualifiedIdentifier& rhs) const { return index == rhs.index; } bool IndexedQualifiedIdentifier::operator==(const QualifiedIdentifier& id) const { return index == id.index(); } QualifiedIdentifier IndexedQualifiedIdentifier::identifier() const { return QualifiedIdentifier(index); } IndexedQualifiedIdentifier::operator QualifiedIdentifier() const { return QualifiedIdentifier(index); } void initIdentifierRepository() { emptyConstantIdentifierPrivateIndex(); emptyConstantIdentifierPrivate(); emptyConstantQualifiedIdentifierPrivateIndex(); emptyConstantQualifiedIdentifierPrivate(); } } QDebug operator<<(QDebug s, const KDevelop::Identifier& identifier) { s.nospace() << identifier.toString(); return s.space(); } QDebug operator<<(QDebug s, const KDevelop::QualifiedIdentifier& identifier) { s.nospace() << identifier.toString(); return s.space(); } diff --git a/kdevplatform/language/duchain/instantiationinformation.cpp b/kdevplatform/language/duchain/instantiationinformation.cpp index c3e9e7a706..82ce8148e3 100644 --- a/kdevplatform/language/duchain/instantiationinformation.cpp +++ b/kdevplatform/language/duchain/instantiationinformation.cpp @@ -1,191 +1,194 @@ /* This file is part of KDevelop Copyright 2007-2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "instantiationinformation.h" #include "identifier.h" #include "serialization/itemrepository.h" #include "types/typeutils.h" #include #include "types/typealiastype.h" #include "types/typerepository.h" namespace KDevelop { DEFINE_LIST_MEMBER_HASH(InstantiationInformation, templateParameters, IndexedType) QualifiedIdentifier InstantiationInformation::applyToIdentifier(const QualifiedIdentifier& id) const { QualifiedIdentifier ret; if(id.count() > 1) { ret = id; ret.pop(); if(previousInstantiationInformation.index()) ret = previousInstantiationInformation.information().applyToIdentifier(ret); } Identifier lastId(id.last()); KDevVarLengthArray oldTemplateIdentifiers; for(uint a = 0; a < lastId.templateIdentifiersCount(); ++a) oldTemplateIdentifiers.append(lastId.templateIdentifier(a)); lastId.clearTemplateIdentifiers(); for(uint a = 0; a < templateParametersSize(); ++a) { if(templateParameters()[a].abstractType()) { lastId.appendTemplateIdentifier(IndexedTypeIdentifier(templateParameters()[a].abstractType()->toString(), true)); }else{ lastId.appendTemplateIdentifier((uint) oldTemplateIdentifiers.size() > a ? oldTemplateIdentifiers[a] : IndexedTypeIdentifier()); } } for(int a = templateParametersSize(); a < oldTemplateIdentifiers.size(); ++a) lastId.appendTemplateIdentifier(oldTemplateIdentifiers[a]); ret.push(lastId); return ret; } void InstantiationInformation::addTemplateParameter(const KDevelop::AbstractType::Ptr& type) { templateParametersList().append(IndexedType(type)); } QString InstantiationInformation::toString(bool local) const { QString ret; if(previousInstantiationInformation.index() && !local) ret = previousInstantiationInformation.information().toString() + QLatin1String("::"); ret += QLatin1Char('<'); + QStringList types; + types.reserve(templateParametersSize()); for(uint a = 0; a < templateParametersSize(); ++a) { - if(a) - ret += QLatin1String(", "); if(templateParameters()[a].abstractType()) - ret += templateParameters()[a].abstractType()->toString(); + types.append(templateParameters()[a].abstractType()->toString()); + else + // TODO: what should be here instead? + types.append(QString()); } - ret += QLatin1Char('>'); + ret += QLatin1Char('<') + types.join(QLatin1String(", ")) + QLatin1Char('>'); return ret; } InstantiationInformation::InstantiationInformation() : m_refCount(0) { initializeAppendedLists(); } InstantiationInformation::InstantiationInformation(const InstantiationInformation& rhs, bool dynamic) : previousInstantiationInformation(rhs.previousInstantiationInformation), m_refCount(0) { initializeAppendedLists(dynamic); copyListsFrom(rhs); } InstantiationInformation::~InstantiationInformation() { freeAppendedLists(); } InstantiationInformation& InstantiationInformation::operator=(const InstantiationInformation& rhs) { previousInstantiationInformation = rhs.previousInstantiationInformation; copyListsFrom(rhs); return *this; } bool InstantiationInformation::operator==(const InstantiationInformation& rhs) const { if(!(previousInstantiationInformation == rhs.previousInstantiationInformation)) return false; return listsEqual(rhs); } uint InstantiationInformation::hash() const { KDevHash kdevhash; FOREACH_FUNCTION(const IndexedType& param, templateParameters) { kdevhash << param.hash(); } return kdevhash << previousInstantiationInformation.index(); } AbstractRepositoryManager* returnTypeRepository() { return typeRepositoryManager(); } static KDevelop::RepositoryManager > >& instantiationInformationRepository() { static KDevelop::RepositoryManager > > instantiationInformationRepositoryObject(QStringLiteral("Instantiation Information Repository"), 1, &returnTypeRepository); return instantiationInformationRepositoryObject; } uint standardInstantiationInformationIndex() { static uint idx = instantiationInformationRepository()->index( InstantiationInformation() ); return idx; } void initInstantiationInformationRepository() { standardInstantiationInformationIndex(); } IndexedInstantiationInformation::IndexedInstantiationInformation() : m_index(0) { } IndexedInstantiationInformation::IndexedInstantiationInformation(uint index) : m_index(index) { if(m_index == standardInstantiationInformationIndex()) m_index = 0; if(m_index && shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(instantiationInformationRepository()->mutex()); increase(instantiationInformationRepository()->dynamicItemFromIndexSimple(m_index)->m_refCount, m_index); } } IndexedInstantiationInformation::IndexedInstantiationInformation(const IndexedInstantiationInformation& rhs) : m_index(rhs.m_index) { if(m_index && shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(instantiationInformationRepository()->mutex()); increase(instantiationInformationRepository()->dynamicItemFromIndexSimple(m_index)->m_refCount, m_index); } } IndexedInstantiationInformation& IndexedInstantiationInformation::operator=(const IndexedInstantiationInformation& rhs) { if(m_index && shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(instantiationInformationRepository()->mutex()); decrease(instantiationInformationRepository()->dynamicItemFromIndexSimple(m_index)->m_refCount, m_index); } m_index = rhs.m_index; if(m_index && shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(instantiationInformationRepository()->mutex()); increase(instantiationInformationRepository()->dynamicItemFromIndexSimple(m_index)->m_refCount, m_index); } return *this; } IndexedInstantiationInformation::~IndexedInstantiationInformation() { if(m_index && shouldDoDUChainReferenceCounting(this)) { QMutexLocker lock(instantiationInformationRepository()->mutex()); decrease(instantiationInformationRepository()->dynamicItemFromIndexSimple(m_index)->m_refCount, m_index); } } bool IndexedInstantiationInformation::isValid() const { return m_index; } const InstantiationInformation& IndexedInstantiationInformation::information() const { return *instantiationInformationRepository()->itemFromIndex(m_index ? m_index : standardInstantiationInformationIndex()); } IndexedInstantiationInformation InstantiationInformation::indexed() const { return IndexedInstantiationInformation(instantiationInformationRepository()->index(*this)); } } diff --git a/kdevplatform/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp b/kdevplatform/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp index 29e33c6b6e..cd1317a5fb 100644 --- a/kdevplatform/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp +++ b/kdevplatform/language/duchain/navigation/abstractdeclarationnavigationcontext.cpp @@ -1,824 +1,822 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "abstractdeclarationnavigationcontext.h" #include #include #include "../functiondeclaration.h" #include "../functiondefinition.h" #include "../classfunctiondeclaration.h" #include "../namespacealiasdeclaration.h" #include "../forwarddeclaration.h" #include "../types/enumeratortype.h" #include "../types/enumerationtype.h" #include "../types/functiontype.h" #include "../duchainutils.h" #include "../types/pointertype.h" #include "../types/referencetype.h" #include "../types/typeutils.h" #include "../types/typesystem.h" #include "../persistentsymboltable.h" #include #include #include #include #include #include namespace KDevelop { class AbstractDeclarationNavigationContextPrivate { public: DeclarationPointer m_declaration; bool m_fullBackwardSearch = false; }; AbstractDeclarationNavigationContext::AbstractDeclarationNavigationContext(const DeclarationPointer& decl, const TopDUContextPointer& topContext, AbstractNavigationContext* previousContext) : AbstractNavigationContext((topContext ? topContext : TopDUContextPointer(decl ? decl->topContext() : nullptr)), previousContext) , d(new AbstractDeclarationNavigationContextPrivate) { d->m_declaration = decl; //Jump from definition to declaration if possible FunctionDefinition* definition = dynamic_cast(d->m_declaration.data()); if(definition && definition->declaration()) d->m_declaration = DeclarationPointer(definition->declaration()); } AbstractDeclarationNavigationContext::~AbstractDeclarationNavigationContext() { } QString AbstractDeclarationNavigationContext::name() const { if(d->m_declaration.data()) return prettyQualifiedIdentifier(d->m_declaration).toString(); else return declarationName(d->m_declaration); } QString AbstractDeclarationNavigationContext::html(bool shorten) { DUChainReadLocker lock(DUChain::lock(), 300); if ( !lock.locked() ) { return {}; } clear(); AbstractNavigationContext::html(shorten); modifyHtml() += QLatin1String("

") + fontSizePrefix(shorten); addExternalHtml(prefix()); if(!d->m_declaration.data()) { modifyHtml() += i18n("
lost declaration
"); return currentHtml(); } if(auto context = previousContext()) { const QString link = createLink(context->name(), context->name(), NavigationAction(context)); modifyHtml() += navigationHighlight(i18n("Back to %1
", link)); } QExplicitlySharedDataPointer doc; if( !shorten ) { doc = ICore::self()->documentationController()->documentationForDeclaration(d->m_declaration.data()); const AbstractFunctionDeclaration* function = dynamic_cast(d->m_declaration.data()); if( function ) { htmlFunction(); } else if( d->m_declaration->isTypeAlias() || d->m_declaration->type() || d->m_declaration->kind() == Declaration::Instance ) { if( d->m_declaration->isTypeAlias() ) modifyHtml() += importantHighlight(QStringLiteral("typedef ")); if(d->m_declaration->type()) modifyHtml() += i18n("enumerator "); AbstractType::Ptr useType = d->m_declaration->abstractType(); if(d->m_declaration->isTypeAlias()) { //Do not show the own name as type of typedefs if(useType.cast()) useType = useType.cast()->type(); } eventuallyMakeTypeLinks( useType ); modifyHtml() += QLatin1Char(' ') + identifierHighlight(declarationName(d->m_declaration).toHtmlEscaped(), d->m_declaration); if(auto integralType = d->m_declaration->type()) { const QString plainValue = integralType->valueAsString(); if (!plainValue.isEmpty()) { modifyHtml() += QStringLiteral(" = ") + plainValue; } } modifyHtml() += QStringLiteral("
"); }else{ if( d->m_declaration->kind() == Declaration::Type && d->m_declaration->abstractType().cast() ) { htmlClass(); } if ( d->m_declaration->kind() == Declaration::Namespace ) { modifyHtml() += i18n("namespace %1 ", identifierHighlight(d->m_declaration->qualifiedIdentifier().toString().toHtmlEscaped(), d->m_declaration)); } else if ( d->m_declaration->kind() == Declaration::NamespaceAlias ) { modifyHtml() += identifierHighlight(declarationName(d->m_declaration).toHtmlEscaped(), d->m_declaration); } if(d->m_declaration->type()) { EnumerationType::Ptr enumeration = d->m_declaration->type(); modifyHtml() += i18n("enumeration %1 ", identifierHighlight(d->m_declaration->identifier().toString().toHtmlEscaped(), d->m_declaration)); } if(d->m_declaration->isForwardDeclaration()) { ForwardDeclaration* forwardDec = static_cast(d->m_declaration.data()); Declaration* resolved = forwardDec->resolve(topContext().data()); if(resolved) { modifyHtml() += i18n("(resolved forward-declaration: "); makeLink(resolved->identifier().toString(), DeclarationPointer(resolved), NavigationAction::NavigateDeclaration ); modifyHtml() += i18n(") "); }else{ modifyHtml() += i18n("(unresolved forward-declaration) "); QualifiedIdentifier id = forwardDec->qualifiedIdentifier(); const auto& forwardDecFile = forwardDec->topContext()->parsingEnvironmentFile(); uint count; const IndexedDeclaration* decls; PersistentSymbolTable::self().declarations(id, count, decls); for(uint a = 0; a < count; ++a) { auto dec = decls[a].data(); if (!dec || dec->isForwardDeclaration()) { continue; } const auto& decFile = forwardDec->topContext()->parsingEnvironmentFile(); if ((static_cast(decFile) != static_cast(forwardDecFile)) || (decFile && forwardDecFile && decFile->language() != forwardDecFile->language())) { // the language of the declarations must match continue; } modifyHtml() += QStringLiteral("
"); makeLink(i18n("possible resolution from"), DeclarationPointer(dec), NavigationAction::NavigateDeclaration); modifyHtml() += QLatin1Char(' ') + dec->url().str(); } } } modifyHtml() += QStringLiteral("
"); } }else{ AbstractType::Ptr showType = d->m_declaration->abstractType(); if(showType && showType.cast()) { showType = showType.cast()->returnType(); if(showType) modifyHtml() += labelHighlight(i18n("Returns: ")); }else if(showType) { modifyHtml() += labelHighlight(i18n("Type: ")); } if(showType) { eventuallyMakeTypeLinks(showType); modifyHtml() += QStringLiteral(" "); } } QualifiedIdentifier identifier = d->m_declaration->qualifiedIdentifier(); if( identifier.count() > 1 ) { if( d->m_declaration->context() && d->m_declaration->context()->owner() ) { Declaration* decl = d->m_declaration->context()->owner(); FunctionDefinition* definition = dynamic_cast(decl); if(definition && definition->declaration()) decl = definition->declaration(); if(decl->abstractType().cast()) modifyHtml() += labelHighlight(i18n("Enum: ")); else modifyHtml() += labelHighlight(i18n("Container: ")); makeLink( declarationName(DeclarationPointer(decl)), DeclarationPointer(decl), NavigationAction::NavigateDeclaration ); modifyHtml() += QStringLiteral(" "); } else { QualifiedIdentifier parent = identifier; parent.pop(); modifyHtml() += labelHighlight(i18n("Scope: %1 ", typeHighlight(parent.toString().toHtmlEscaped()))); } } if( shorten && !d->m_declaration->comment().isEmpty() ) { QString comment = QString::fromUtf8(d->m_declaration->comment()); if( comment.length() > 60 ) { comment.truncate(60); comment += QLatin1String("..."); } comment.replace(QLatin1Char('\n'), QLatin1Char(' ')); comment.replace(QLatin1String("
"), QLatin1String(" ")); comment.replace(QLatin1String("
"), QLatin1String(" ")); modifyHtml() += commentHighlight(comment.toHtmlEscaped()) + QLatin1String(" "); } QString access = stringFromAccess(d->m_declaration); if( !access.isEmpty() ) modifyHtml() += labelHighlight(i18n("Access: %1 ", propertyHighlight(access.toHtmlEscaped()))); ///@todo Enumerations QString detailsHtml; QStringList details = declarationDetails(d->m_declaration); if( !details.isEmpty() ) { bool first = true; foreach( const QString &str, details ) { if( !first ) detailsHtml += QLatin1String(", "); first = false; detailsHtml += propertyHighlight(str); } } QString kind = declarationKind(d->m_declaration); if( !kind.isEmpty() ) { if( !detailsHtml.isEmpty() ) modifyHtml() += labelHighlight(i18n("Kind: %1 %2 ", importantHighlight(kind.toHtmlEscaped()), detailsHtml)); else modifyHtml() += labelHighlight(i18n("Kind: %1 ", importantHighlight(kind.toHtmlEscaped()))); } if (d->m_declaration->isDeprecated()) { modifyHtml() += labelHighlight(i18n("Status: %1 ", propertyHighlight(i18n("Deprecated")))); } modifyHtml() += QStringLiteral("
"); if(!shorten) htmlAdditionalNavigation(); if( !shorten ) { if(dynamic_cast(d->m_declaration.data())) modifyHtml() += labelHighlight(i18n( "Def.: " )); else modifyHtml() += labelHighlight(i18n( "Decl.: " )); makeLink( QStringLiteral("%1 :%2").arg( d->m_declaration->url().toUrl().fileName() ).arg( d->m_declaration->rangeInCurrentRevision().start().line()+1 ), d->m_declaration, NavigationAction::JumpToSource ); modifyHtml() += QStringLiteral(" "); //modifyHtml() += "
"; if(!dynamic_cast(d->m_declaration.data())) { if( FunctionDefinition* definition = FunctionDefinition::definition(d->m_declaration.data()) ) { modifyHtml() += labelHighlight(i18n( " Def.: " )); makeLink( QStringLiteral("%1 :%2").arg( definition->url().toUrl().fileName() ).arg( definition->rangeInCurrentRevision().start().line()+1 ), DeclarationPointer(definition), NavigationAction::JumpToSource ); } } if( FunctionDefinition* definition = dynamic_cast(d->m_declaration.data()) ) { if(definition->declaration()) { modifyHtml() += labelHighlight(i18n( " Decl.: " )); makeLink( QStringLiteral("%1 :%2").arg( definition->declaration()->url().toUrl().fileName() ).arg( definition->declaration()->rangeInCurrentRevision().start().line()+1 ), DeclarationPointer(definition->declaration()), NavigationAction::JumpToSource ); } } modifyHtml() += QStringLiteral(" "); //The action name _must_ stay "show_uses", since that is also used from outside makeLink(i18n("Show uses"), QStringLiteral("show_uses"), NavigationAction(d->m_declaration, NavigationAction::NavigateUses)); } QByteArray declarationComment = d->m_declaration->comment(); if( !shorten && (!declarationComment.isEmpty() || doc) ) { modifyHtml() += QStringLiteral("

"); if(doc) { QString comment = doc->description(); connect(doc.data(), &IDocumentation::descriptionChanged, this, &AbstractDeclarationNavigationContext::contentsChanged); if(!comment.isEmpty()) { modifyHtml() += QLatin1String("

") + commentHighlight(comment) + QLatin1String("

"); } } QString comment = QString::fromUtf8(declarationComment); if(!comment.isEmpty()) { // if the first paragraph does not contain a tag, we assume that this is a plain-text comment if (!Qt::mightBeRichText(comment)) { // still might contain extra html tags for line breaks (this is the case for doxygen-style comments sometimes) // let's protect them from being removed completely comment.replace(QRegExp(QStringLiteral("
")), QStringLiteral("\n")); comment = comment.toHtmlEscaped(); comment.replace(QLatin1Char('\n'), QLatin1String("
")); //Replicate newlines in html } modifyHtml() += commentHighlight(comment); modifyHtml() += QStringLiteral("

"); } } if(!shorten) { modifyHtml() += declarationSizeInformation(d->m_declaration); } if(!shorten && doc) { modifyHtml() += QLatin1String("

") + i18n("Show documentation for "); makeLink(prettyQualifiedName(d->m_declaration), d->m_declaration, NavigationAction::ShowDocumentation); modifyHtml() += QStringLiteral("

"); } //modifyHtml() += "
"; addExternalHtml(suffix()); modifyHtml() += fontSizeSuffix(shorten) + QLatin1String("

"); return currentHtml(); } AbstractType::Ptr AbstractDeclarationNavigationContext::typeToShow(AbstractType::Ptr type) { return type; } void AbstractDeclarationNavigationContext::htmlFunction() { const AbstractFunctionDeclaration* function = dynamic_cast(d->m_declaration.data()); Q_ASSERT(function); const ClassFunctionDeclaration* classFunDecl = dynamic_cast(d->m_declaration.data()); const FunctionType::Ptr type = d->m_declaration->abstractType().cast(); if( !type ) { modifyHtml() += errorHighlight(QStringLiteral("Invalid type
")); return; } if( !classFunDecl || (!classFunDecl->isConstructor() && !classFunDecl->isDestructor()) ) { // only print return type for global functions and non-ctor/dtor methods eventuallyMakeTypeLinks( type->returnType() ); } modifyHtml() += QLatin1Char(' ') + identifierHighlight(prettyIdentifier(d->m_declaration).toString().toHtmlEscaped(), d->m_declaration); if( type->indexedArgumentsSize() == 0 ) { modifyHtml() += QStringLiteral("()"); } else { modifyHtml() += QStringLiteral("( "); bool first = true; int firstDefaultParam = type->indexedArgumentsSize() - function->defaultParametersSize(); int currentArgNum = 0; QVector decls; if (DUContext* argumentContext = DUChainUtils::getArgumentContext(d->m_declaration.data())) { decls = argumentContext->localDeclarations(topContext().data()); } foreach(const AbstractType::Ptr& argType, type->arguments()) { if( !first ) modifyHtml() += QStringLiteral(", "); first = false; eventuallyMakeTypeLinks( argType ); if (currentArgNum < decls.size()) { modifyHtml() += QLatin1Char(' ') + identifierHighlight(decls[currentArgNum]->identifier().toString().toHtmlEscaped(), d->m_declaration); } if (currentArgNum >= firstDefaultParam) { IndexedString defaultStr = function->defaultParameters()[currentArgNum - firstDefaultParam]; if (!defaultStr.isEmpty()) { modifyHtml() += QLatin1String(" = ") + defaultStr.str().toHtmlEscaped(); } } ++currentArgNum; } modifyHtml() += QStringLiteral(" )"); } modifyHtml() += QStringLiteral("
"); } Identifier AbstractDeclarationNavigationContext::prettyIdentifier(const DeclarationPointer& decl) const { Identifier ret; QualifiedIdentifier q = prettyQualifiedIdentifier(decl); if(!q.isEmpty()) ret = q.last(); return ret; } QualifiedIdentifier AbstractDeclarationNavigationContext::prettyQualifiedIdentifier(DeclarationPointer decl) const { if(decl) return decl->qualifiedIdentifier(); else return QualifiedIdentifier(); } QString AbstractDeclarationNavigationContext::prettyQualifiedName(const DeclarationPointer& decl) const { const auto qid = prettyQualifiedIdentifier(decl); if (qid.isEmpty()) { return i18nc("An anonymous declaration (class, function, etc.)", ""); } return qid.toString(); } void AbstractDeclarationNavigationContext::htmlAdditionalNavigation() { ///Check if the function overrides or hides another one const ClassFunctionDeclaration* classFunDecl = dynamic_cast(d->m_declaration.data()); if(classFunDecl) { Declaration* overridden = DUChainUtils::getOverridden(d->m_declaration.data()); if(overridden) { modifyHtml() += i18n("Overrides a "); makeLink(i18n("function"), QStringLiteral("jump_to_overridden"), NavigationAction(DeclarationPointer(overridden), NavigationAction::NavigateDeclaration)); modifyHtml() += i18n(" from "); makeLink(prettyQualifiedName(DeclarationPointer(overridden->context()->owner())), QStringLiteral("jump_to_overridden_container"), NavigationAction(DeclarationPointer(overridden->context()->owner()), NavigationAction::NavigateDeclaration)); modifyHtml() += QStringLiteral("
"); }else{ //Check if this declarations hides other declarations QList decls; foreach(const DUContext::Import &import, d->m_declaration->context()->importedParentContexts()) if(import.context(topContext().data())) decls += import.context(topContext().data())->findDeclarations(QualifiedIdentifier(d->m_declaration->identifier()), CursorInRevision::invalid(), AbstractType::Ptr(), topContext().data(), DUContext::DontSearchInParent); uint num = 0; foreach(Declaration* decl, decls) { modifyHtml() += i18n("Hides a "); makeLink(i18n("function"), QStringLiteral("jump_to_hide_%1").arg(num), NavigationAction(DeclarationPointer(decl), NavigationAction::NavigateDeclaration)); modifyHtml() += i18n(" from "); makeLink(prettyQualifiedName(DeclarationPointer(decl->context()->owner())), QStringLiteral("jump_to_hide_container_%1").arg(num), NavigationAction(DeclarationPointer(decl->context()->owner()), NavigationAction::NavigateDeclaration)); modifyHtml() += QStringLiteral("
"); ++num; } } ///Show all places where this function is overridden if(classFunDecl->isVirtual()) { Declaration* classDecl = d->m_declaration->context()->owner(); if(classDecl) { uint maxAllowedSteps = d->m_fullBackwardSearch ? (uint)-1 : 10; QList overriders = DUChainUtils::getOverriders(classDecl, classFunDecl, maxAllowedSteps); if(!overriders.isEmpty()) { modifyHtml() += i18n("Overridden in "); bool first = true; foreach(Declaration* overrider, overriders) { if(!first) modifyHtml() += QStringLiteral(", "); first = false; const auto owner = DeclarationPointer(overrider->context()->owner()); const QString name = prettyQualifiedName(owner); makeLink(name, name, NavigationAction(DeclarationPointer(overrider), NavigationAction::NavigateDeclaration)); } modifyHtml() += QStringLiteral("
"); } if(maxAllowedSteps == 0) createFullBackwardSearchLink(overriders.isEmpty() ? i18n("Overriders possible, show all") : i18n("More overriders possible, show all")); } } } ///Show all classes that inherit this one uint maxAllowedSteps = d->m_fullBackwardSearch ? (uint)-1 : 10; QList inheriters = DUChainUtils::getInheriters(d->m_declaration.data(), maxAllowedSteps); if(!inheriters.isEmpty()) { modifyHtml() += i18n("Inherited by "); bool first = true; foreach(Declaration* importer, inheriters) { if(!first) modifyHtml() += QStringLiteral(", "); first = false; const QString importerName = prettyQualifiedName(DeclarationPointer(importer)); makeLink(importerName, importerName, NavigationAction(DeclarationPointer(importer), NavigationAction::NavigateDeclaration)); } modifyHtml() += QStringLiteral("
"); } if(maxAllowedSteps == 0) createFullBackwardSearchLink(inheriters.isEmpty() ? i18n("Inheriters possible, show all") : i18n("More inheriters possible, show all")); } void AbstractDeclarationNavigationContext::createFullBackwardSearchLink(const QString& string) { makeLink(string, QStringLiteral("m_fullBackwardSearch=true"), NavigationAction(QStringLiteral("m_fullBackwardSearch=true"))); modifyHtml() += QStringLiteral("
"); } NavigationContextPointer AbstractDeclarationNavigationContext::executeKeyAction( QString key ) { if(key == QLatin1String("m_fullBackwardSearch=true")) { d->m_fullBackwardSearch = true; clear(); } return NavigationContextPointer(this); } void AbstractDeclarationNavigationContext::htmlClass() { StructureType::Ptr klass = d->m_declaration->abstractType().cast(); Q_ASSERT(klass); ClassDeclaration* classDecl = dynamic_cast(klass->declaration(topContext().data())); if(classDecl) { switch ( classDecl->classType() ) { case ClassDeclarationData::Class: modifyHtml() += QStringLiteral("class "); break; case ClassDeclarationData::Struct: modifyHtml() += QStringLiteral("struct "); break; case ClassDeclarationData::Union: modifyHtml() += QStringLiteral("union "); break; case ClassDeclarationData::Interface: modifyHtml() += QStringLiteral("interface "); break; case ClassDeclarationData::Trait: modifyHtml() += QStringLiteral("trait "); break; } eventuallyMakeTypeLinks( klass.cast() ); FOREACH_FUNCTION( const BaseClassInstance& base, classDecl->baseClasses ) { modifyHtml() += QLatin1String(", ") + stringFromAccess(base.access) + QLatin1Char(' ') + (base.virtualInheritance ? QStringLiteral("virtual") : QString()) + QLatin1Char(' '); eventuallyMakeTypeLinks(base.baseClass.abstractType()); } } else { /// @todo How can we get here? and should this really be a class? modifyHtml() += QStringLiteral("class "); eventuallyMakeTypeLinks( klass.cast() ); } modifyHtml() += QStringLiteral(" "); } void AbstractDeclarationNavigationContext::htmlIdentifiedType(AbstractType::Ptr type, const IdentifiedType* idType) { Q_ASSERT(type); Q_ASSERT(idType); if( Declaration* decl = idType->declaration(topContext().data()) ) { //Remove the last template-identifiers, because we create those directly QualifiedIdentifier id = prettyQualifiedIdentifier(DeclarationPointer(decl)); Identifier lastId = id.last(); id.pop(); lastId.clearTemplateIdentifiers(); id.push(lastId); if(decl->context() && decl->context()->owner()) { //Also create full type-links for the context around AbstractType::Ptr contextType = decl->context()->owner()->abstractType(); IdentifiedType* contextIdType = dynamic_cast(contextType.data()); if(contextIdType && !contextIdType->equals(idType)) { //Create full type information for the context if(!id.isEmpty()) id = id.mid(id.count()-1); htmlIdentifiedType(contextType, contextIdType); modifyHtml() += QStringLiteral("::").toHtmlEscaped(); } } //We leave out the * and & reference and pointer signs, those are added to the end makeLink(id.toString() , DeclarationPointer(idType->declaration(topContext().data())), NavigationAction::NavigateDeclaration ); } else { qCDebug(LANGUAGE) << "could not resolve declaration:" << idType->declarationId().isDirect() << idType->qualifiedIdentifier().toString() << "in top-context" << topContext()->url().str(); modifyHtml() += typeHighlight(type->toString().toHtmlEscaped()); } } void AbstractDeclarationNavigationContext::eventuallyMakeTypeLinks( AbstractType::Ptr type ) { type = typeToShow(type); if( !type ) { modifyHtml() += typeHighlight(QStringLiteral("").toHtmlEscaped()); return; } AbstractType::Ptr target = TypeUtils::targetTypeKeepAliases( type, topContext().data() ); const IdentifiedType* idType = dynamic_cast( target.data() ); qCDebug(LANGUAGE) << "making type-links for" << type->toString(); if( idType && idType->declaration(topContext().data()) ) { ///@todo This is C++ specific, move into subclass if(target->modifiers() & AbstractType::ConstModifier) modifyHtml() += typeHighlight(QStringLiteral("const ")); htmlIdentifiedType(target, idType); //We need to exchange the target type, else template-parameters may confuse this SimpleTypeExchanger exchangeTarget(target, AbstractType::Ptr()); AbstractType::Ptr exchanged = exchangeTarget.exchange(type); if(exchanged) { QString typeSuffixString = exchanged->toString(); QRegExp suffixExp(QStringLiteral("\\&|\\*")); int suffixPos = typeSuffixString.indexOf(suffixExp); if(suffixPos != -1) modifyHtml() += typeHighlight(typeSuffixString.mid(suffixPos)); } } else { if(idType) { qCDebug(LANGUAGE) << "identified type could not be resolved:" << idType->qualifiedIdentifier() << idType->declarationId().isValid() << idType->declarationId().isDirect(); } modifyHtml() += typeHighlight(type->toString().toHtmlEscaped()); } } DeclarationPointer AbstractDeclarationNavigationContext::declaration() const { return d->m_declaration; } QString AbstractDeclarationNavigationContext::identifierHighlight(const QString& identifier, const DeclarationPointer& decl) const { QString ret = nameHighlight(identifier); if (!decl) { return ret; } if (decl->isDeprecated()) { ret = QStringLiteral("") + ret + QStringLiteral(""); } return ret; } QString AbstractDeclarationNavigationContext::stringFromAccess(Declaration::AccessPolicy access) { switch(access) { case Declaration::Private: return QStringLiteral("private"); case Declaration::Protected: return QStringLiteral("protected"); case Declaration::Public: return QStringLiteral("public"); default: break; } return QString(); } QString AbstractDeclarationNavigationContext::stringFromAccess(const DeclarationPointer& decl) { const ClassMemberDeclaration* memberDecl = dynamic_cast(decl.data()); if( memberDecl ) { return stringFromAccess(memberDecl->accessPolicy()); } return QString(); } QString AbstractDeclarationNavigationContext::declarationName( const DeclarationPointer& decl ) const { if( NamespaceAliasDeclaration* alias = dynamic_cast(decl.data()) ) { if( alias->identifier().isEmpty() ) return QLatin1String("using namespace ") + alias->importIdentifier().toString(); else return QLatin1String("namespace ") + alias->identifier().toString() + QLatin1String(" = ") + alias->importIdentifier().toString(); } if( !decl ) return i18nc("A declaration that is unknown", "Unknown"); else return prettyIdentifier(decl).toString(); } QStringList AbstractDeclarationNavigationContext::declarationDetails(const DeclarationPointer& decl) { QStringList details; const AbstractFunctionDeclaration* function = dynamic_cast(decl.data()); const ClassMemberDeclaration* memberDecl = dynamic_cast(decl.data()); if( memberDecl ) { if( memberDecl->isMutable() ) details << QStringLiteral("mutable"); if( memberDecl->isRegister() ) details << QStringLiteral("register"); if( memberDecl->isStatic() ) details << QStringLiteral("static"); if( memberDecl->isAuto() ) details << QStringLiteral("auto"); if( memberDecl->isExtern() ) details << QStringLiteral("extern"); if( memberDecl->isFriend() ) details << QStringLiteral("friend"); } if( decl->isDefinition() ) details << i18nc("tells if a declaration is defining the variable's value", "definition"); if( decl->isExplicitlyDeleted() ) details << QStringLiteral("deleted"); if( memberDecl && memberDecl->isForwardDeclaration() ) details << i18nc("as in c++ forward declaration", "forward"); AbstractType::Ptr t(decl->abstractType()); if( t ) { if( t->modifiers() & AbstractType::ConstModifier ) details << i18nc("a variable that won't change, const", "constant"); if( t->modifiers() & AbstractType::VolatileModifier ) details << QStringLiteral("volatile"); } if( function ) { if( function->isInline() ) details << QStringLiteral("inline"); if( function->isExplicit() ) details << QStringLiteral("explicit"); if( function->isVirtual() ) details << QStringLiteral("virtual"); const ClassFunctionDeclaration* classFunDecl = dynamic_cast(decl.data()); if( classFunDecl ) { if( classFunDecl->isSignal() ) details << QStringLiteral("signal"); if( classFunDecl->isSlot() ) details << QStringLiteral("slot"); if( classFunDecl->isFinal() ) details << QStringLiteral("final"); if( classFunDecl->isConstructor() ) details << QStringLiteral("constructor"); if( classFunDecl->isDestructor() ) details << QStringLiteral("destructor"); if( classFunDecl->isConversionFunction() ) details << QStringLiteral("conversion-function"); if( classFunDecl->isAbstract() ) details << QStringLiteral("abstract"); } } return details; } QString AbstractDeclarationNavigationContext::declarationSizeInformation(const DeclarationPointer& decl) { // Note that ClassMemberDeclaration also includes ClassDeclaration, which uses the sizeOf and alignOf fields, // but normally leaves the bitOffsetOf unset (-1). const ClassMemberDeclaration* memberDecl = dynamic_cast(decl.data()); if (memberDecl && (memberDecl->bitOffsetOf() > 0 || memberDecl->sizeOf() > 0 || memberDecl->alignOf() > 0)) { QString sizeInfo = "

"; if (memberDecl->bitOffsetOf() >= 0) { const auto byteOffset = memberDecl->bitOffsetOf() / 8; const auto bitOffset = memberDecl->bitOffsetOf() % 8; const QString byteOffsetStr = i18np("1 Byte", "%1 Bytes", byteOffset); const QString bitOffsetStr = bitOffset ? i18np("1 Bit", "%1 Bits", bitOffset) : QString(); - sizeInfo += i18n("offset in parent: %1", bitOffset ? i18nc("%1: bytes, %2: bits", "%1, %2", byteOffsetStr, bitOffsetStr) : byteOffsetStr); - sizeInfo += "; "; + sizeInfo += i18n("offset in parent: %1", bitOffset ? i18nc("%1: bytes, %2: bits", "%1, %2", byteOffsetStr, bitOffsetStr) : byteOffsetStr) + QLatin1String("; "); } if (memberDecl->sizeOf() >= 0) { - sizeInfo += i18n("size: %1 Bytes", memberDecl->sizeOf()); - sizeInfo += "; "; + sizeInfo += i18n("size: %1 Bytes", memberDecl->sizeOf()) + QLatin1String("; "); } if (memberDecl->alignOf() >= 0) { sizeInfo += i18n("aligned to: %1 Bytes", memberDecl->alignOf()); } sizeInfo += "

"; return sizeInfo; } return QString(); } } diff --git a/kdevplatform/language/duchain/navigation/usescollector.cpp b/kdevplatform/language/duchain/navigation/usescollector.cpp index 8b014e33e3..37f3c2b083 100644 --- a/kdevplatform/language/duchain/navigation/usescollector.cpp +++ b/kdevplatform/language/duchain/navigation/usescollector.cpp @@ -1,427 +1,429 @@ /* 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 "usescollector.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "../classmemberdeclaration.h" #include "../abstractfunctiondeclaration.h" #include "../functiondefinition.h" #include #include #include #include using namespace KDevelop; ///@todo make this language-neutral static Identifier destructorForName(const Identifier& name) { QString str = name.identifier().str(); if(str.startsWith(QLatin1Char('~'))) return Identifier(str); return Identifier(QLatin1Char('~') + str); } ///@todo Only collect uses within currently loaded projects template void collectImporters(ImportanceChecker& checker, ParsingEnvironmentFile* current, QSet& visited, QSet& collected) { //Ignore proxy-contexts while collecting. Those build a parallel and much more complicated structure. if(current->isProxyContext()) return; if(visited.contains(current)) return; visited.insert(current); if(checker(current)) collected.insert(current); foreach(const ParsingEnvironmentFilePointer& importer, current->importers()) if(importer.data()) collectImporters(checker, importer.data(), visited, collected); else qCDebug(LANGUAGE) << "missing environment-file, strange"; } ///The returned set does not include the file itself ///@param visited should be empty on each call, used to prevent endless recursion void allImportedFiles(ParsingEnvironmentFilePointer file, QSet& set, QSet& visited) { foreach(const ParsingEnvironmentFilePointer &import, file->imports()) { if(!import) { qCDebug(LANGUAGE) << "warning: missing import"; continue; } if(!visited.contains(import)) { visited.insert(import); set.insert(import->url()); allImportedFiles(import, set, visited); } } } void UsesCollector::setCollectConstructors(bool process) { m_collectConstructors = process; } void UsesCollector::setProcessDeclarations(bool process) { m_processDeclarations = process; } void UsesCollector::setCollectOverloads(bool collect) { m_collectOverloads = collect; } void UsesCollector::setCollectDefinitions(bool collect) { m_collectDefinitions = collect; } QList UsesCollector::declarations() { return m_declarations; } bool UsesCollector::isReady() const { return m_waitForUpdate.size() == m_updateReady.size(); } bool UsesCollector::shouldRespectFile(IndexedString document) { return (bool)ICore::self()->projectController()->findProjectForUrl(document.toUrl()) || (bool)ICore::self()->documentController()->documentForUrl(document.toUrl()); } struct ImportanceChecker { explicit ImportanceChecker(UsesCollector& collector) : m_collector(collector) { } bool operator ()(ParsingEnvironmentFile* file) { return m_collector.shouldRespectFile(file->url()); } UsesCollector& m_collector; }; void UsesCollector::startCollecting() { DUChainReadLocker lock(DUChain::lock()); if(Declaration* decl = m_declaration.data()) { if(m_collectDefinitions) { if(FunctionDefinition* def = dynamic_cast(decl)) { //Jump from definition to declaration Declaration* declaration = def->declaration(); if(declaration) decl = declaration; } } ///Collect all overloads into "decls" QList decls; if(m_collectOverloads && decl->context()->owner() && decl->context()->type() == DUContext::Class) { //First find the overridden base, and then all overriders of that base. while(Declaration* overridden = DUChainUtils::getOverridden(decl)) decl = overridden; uint maxAllowedSteps = 10000; decls += DUChainUtils::getOverriders( decl->context()->owner(), decl, maxAllowedSteps ); if(maxAllowedSteps == 10000) { ///@todo Fail! } } decls << decl; ///Collect all "parsed versions" or forward-declarations etc. here, into allDeclarations QSet allDeclarations; foreach(Declaration* overload, decls) { m_declarations = DUChainUtils::collectAllVersions(overload); foreach(const IndexedDeclaration &d, m_declarations) { if(!d.data() || d.data()->id() != overload->id()) continue; allDeclarations.insert(d); if(m_collectConstructors && d.data() && d.data()->internalContext() && d.data()->internalContext()->type() == DUContext::Class) { QList constructors = d.data()->internalContext()->findLocalDeclarations(d.data()->identifier(), CursorInRevision::invalid(), nullptr, AbstractType::Ptr(), DUContext::OnlyFunctions); foreach(Declaration* constructor, constructors) { ClassFunctionDeclaration* classFun = dynamic_cast(constructor); if(classFun && classFun->isConstructor()) allDeclarations.insert(IndexedDeclaration(constructor)); } Identifier destructorId = destructorForName(d.data()->identifier()); QList destructors = d.data()->internalContext()->findLocalDeclarations(destructorId, CursorInRevision::invalid(), nullptr, AbstractType::Ptr(), DUContext::OnlyFunctions); foreach(Declaration* destructor, destructors) { ClassFunctionDeclaration* classFun = dynamic_cast(destructor); if(classFun && classFun->isDestructor()) allDeclarations.insert(IndexedDeclaration(destructor)); } } } } ///Collect definitions for declarations if(m_collectDefinitions) { foreach(const IndexedDeclaration d, allDeclarations) { Declaration* definition = FunctionDefinition::definition(d.data()); if(definition) { qCDebug(LANGUAGE) << "adding definition"; allDeclarations.insert(IndexedDeclaration(definition)); } } } m_declarations.clear(); ///Step 4: Copy allDeclarations into m_declarations, build top-context list, etc. QList candidateTopContexts; + candidateTopContexts.reserve(allDeclarations.size()); + m_declarations.reserve(allDeclarations.size()); foreach(const IndexedDeclaration d, allDeclarations) { m_declarations << d; m_declarationTopContexts.insert(d.indexedTopContext()); //We only collect declarations with the same type here.. candidateTopContexts << d.indexedTopContext().data(); } ImportanceChecker checker(*this); QSet visited; QSet collected; qCDebug(LANGUAGE) << "count of source candidate top-contexts:" << candidateTopContexts.size(); ///We use ParsingEnvironmentFile to collect all the relevant importers, because loading those is very cheap, compared ///to loading a whole TopDUContext. if(decl->inSymbolTable()) { //The declaration can only be used from other contexts if it is in the symbol table foreach(const ReferencedTopDUContext &top, candidateTopContexts) { if(top->parsingEnvironmentFile()) { collectImporters(checker, top->parsingEnvironmentFile().data(), visited, collected); //In C++, visibility is not handled strictly through the import-structure. //It may happen that an object is visible because of an earlier include. //We can not perfectly handle that, but we can at least handle it if the header includes //the header that contains the declaration. That header may be parsed empty due to header-guards, //but we still need to pick it up here. QList allVersions = DUChain::self()->allEnvironmentFiles(top->url()); foreach(const ParsingEnvironmentFilePointer& version, allVersions) collectImporters(checker, version.data(), visited, collected); } } } KDevelop::ParsingEnvironmentFile* file=decl->topContext()->parsingEnvironmentFile().data(); if(!file) return; if(checker(file)) collected.insert(file); { QSet filteredCollected; QMap grepCache; // Filter the collected files by performing a grep foreach(ParsingEnvironmentFile* file, collected) { IndexedString url = file->url(); QMap< IndexedString, bool >::iterator grepCacheIt = grepCache.find(url); if(grepCacheIt == grepCache.end()) { CodeRepresentation::Ptr repr = KDevelop::createCodeRepresentation( url ); if(repr) { QVector found = repr->grep(decl->identifier().identifier().str()); grepCacheIt = grepCache.insert(url, !found.isEmpty()); } } if(grepCacheIt.value()) filteredCollected << file; } qCDebug(LANGUAGE) << "Collected contexts for full re-parse, before filtering: " << collected.size() << " after filtering: " << filteredCollected.size(); collected = filteredCollected; } ///We have all importers now. However since we can tell parse-jobs to also update all their importers, we only need to ///update the "root" top-contexts that open the whole set with their imports. QSet rootFiles; QSet allFiles; foreach(ParsingEnvironmentFile* importer, collected) { QSet allImports; QSet visited; allImportedFiles(ParsingEnvironmentFilePointer(importer), allImports, visited); //Remove all files from the "root" set that are imported by this one ///@todo more intelligent rootFiles -= allImports; allFiles += allImports; allFiles.insert(importer->url()); rootFiles.insert(importer->url()); } emit maximumProgressSignal(rootFiles.size()); maximumProgress(rootFiles.size()); //If we used the AllDeclarationsContextsAndUsesRecursive flag here, we would compute way too much. This way we only //set the minimum-features selectively on the files we really require them on. foreach(ParsingEnvironmentFile* file, collected) m_staticFeaturesManipulated.insert(file->url()); m_staticFeaturesManipulated.insert(decl->url()); foreach(const IndexedString &file, m_staticFeaturesManipulated) ParseJob::setStaticMinimumFeatures(file, TopDUContext::AllDeclarationsContextsAndUses); m_waitForUpdate = rootFiles; foreach(const IndexedString &file, rootFiles) { qCDebug(LANGUAGE) << "updating root file:" << file.str(); DUChain::self()->updateContextForUrl(file, TopDUContext::AllDeclarationsContextsAndUses, this); } }else{ emit maximumProgressSignal(0); maximumProgress(0); } } void UsesCollector::maximumProgress(uint max) { Q_UNUSED(max); } UsesCollector::UsesCollector(IndexedDeclaration declaration) : m_declaration(declaration), m_collectOverloads(true), m_collectDefinitions(true), m_collectConstructors(false), m_processDeclarations(true) { } UsesCollector::~UsesCollector() { ICore::self()->languageController()->backgroundParser()->revertAllRequests(this); foreach(const IndexedString &file, m_staticFeaturesManipulated) ParseJob::unsetStaticMinimumFeatures(file, TopDUContext::AllDeclarationsContextsAndUses); } void UsesCollector::progress(uint processed, uint total) { Q_UNUSED(processed); Q_UNUSED(total); } void UsesCollector::updateReady(const KDevelop::IndexedString& url, KDevelop::ReferencedTopDUContext topContext) { DUChainReadLocker lock(DUChain::lock()); if(!topContext) { qCDebug(LANGUAGE) << "failed updating" << url.str(); }else{ if(topContext->parsingEnvironmentFile() && topContext->parsingEnvironmentFile()->isProxyContext()) { ///Use the attached content-context instead foreach(const DUContext::Import &import, topContext->importedParentContexts()) { if(import.context(nullptr) && import.context(nullptr)->topContext()->parsingEnvironmentFile() && !import.context(nullptr)->topContext()->parsingEnvironmentFile()->isProxyContext()) { if((import.context(nullptr)->topContext()->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ReferencedTopDUContext newTop(import.context(nullptr)->topContext()); topContext = newTop; break; } } } if(topContext->parsingEnvironmentFile() && topContext->parsingEnvironmentFile()->isProxyContext()) { qCDebug(LANGUAGE) << "got bad proxy-context for" << url.str(); topContext = nullptr; } } } if(m_waitForUpdate.contains(url) && !m_updateReady.contains(url)) { m_updateReady << url; m_checked.clear(); emit progressSignal(m_updateReady.size(), m_waitForUpdate.size()); progress(m_updateReady.size(), m_waitForUpdate.size()); } if(!topContext || !topContext->parsingEnvironmentFile()) { qCDebug(LANGUAGE) << "bad top-context"; return; } if(!m_staticFeaturesManipulated.contains(url)) return; //Not interesting if(!(topContext->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ///@todo With simplified environment-matching, the same file may have been imported multiple times, ///while only one of those was updated. We have to check here whether this file is just such an import, ///or whether we work on with it. ///@todo We will lose files that were edited right after their update here. qCWarning(LANGUAGE) << "WARNING: context" << topContext->url().str() << "does not have the required features!!"; ICore::self()->uiController()->showErrorMessage(QLatin1String("Updating ") + ICore::self()->projectController()->prettyFileName(topContext->url().toUrl(), KDevelop::IProjectController::FormatPlain) + QLatin1String(" failed!"), 5); return; } if(topContext->parsingEnvironmentFile()->needsUpdate()) { qCWarning(LANGUAGE) << "WARNING: context" << topContext->url().str() << "is not up to date!"; ICore::self()->uiController()->showErrorMessage(i18n("%1 still needs an update!", ICore::self()->projectController()->prettyFileName(topContext->url().toUrl(), KDevelop::IProjectController::FormatPlain)), 5); // return; } IndexedTopDUContext indexed(topContext.data()); if(m_checked.contains(indexed)) return; if(!topContext.data()) { qCDebug(LANGUAGE) << "updated top-context is zero:" << url.str(); return; } m_checked.insert(indexed); if(m_declaration.data() && ((m_processDeclarations && m_declarationTopContexts.contains(indexed)) || DUChainUtils::contextHasUse(topContext.data(), m_declaration.data()))) { if(!m_processed.contains(topContext->url())) { m_processed.insert(topContext->url()); lock.unlock(); emit processUsesSignal(topContext); processUses(topContext); lock.lock(); } } else { if(!m_declaration.data()) { qCDebug(LANGUAGE) << "declaration has become invalid"; } } QList imports; foreach(const DUContext::Import &imported, topContext->importedParentContexts()) if(imported.context(nullptr) && imported.context(nullptr)->topContext()) imports << KDevelop::ReferencedTopDUContext(imported.context(nullptr)->topContext()); foreach(const KDevelop::ReferencedTopDUContext &import, imports) { IndexedString url = import->url(); lock.unlock(); updateReady(url, import); lock.lock(); } } IndexedDeclaration UsesCollector::declaration() const { return m_declaration; } diff --git a/kdevplatform/language/duchain/navigation/useswidget.cpp b/kdevplatform/language/duchain/navigation/useswidget.cpp index 488d96ce84..f5cb162a3b 100644 --- a/kdevplatform/language/duchain/navigation/useswidget.cpp +++ b/kdevplatform/language/duchain/navigation/useswidget.cpp @@ -1,701 +1,698 @@ /* Copyright 2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "useswidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QString backgroundColor(bool isHighlighted) { if (isHighlighted) { return QColor(251, 150, 242).name(); } else { return QColor(251, 250, 150).name(); } } } const int tooltipContextSize = 2; //How many lines around the use are shown in the tooltip ///The returned text is fully escaped ///@param cutOff The total count of characters that should be cut of, all in all on both sides together. ///@param range The range that is highlighted, and that will be preserved during cutting, given that there is enough room beside it. QString highlightAndEscapeUseText(QString line, int cutOff, KTextEditor::Range range) { int leftCutRoom = range.start().column(); int rightCutRoom = line.length() - range.end().column(); if(range.start().column() < 0 || range.end().column() > line.length() || cutOff > leftCutRoom + rightCutRoom) return QString(); //Not enough room for cutting off on sides int leftCut = 0; int rightCut = 0; if(leftCutRoom < rightCutRoom) { if(leftCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. leftCut = cutOff / 2; rightCut = cutOff - leftCut; }else{ //Not enough room in left side, but enough room all together leftCut = leftCutRoom; rightCut = cutOff - leftCut; } }else{ if(rightCutRoom * 2 >= cutOff) { //Enough room on both sides. Just cut. rightCut = cutOff / 2; leftCut = cutOff - rightCut; }else{ //Not enough room in right side, but enough room all together rightCut = rightCutRoom; leftCut = cutOff - rightCut; } } Q_ASSERT(leftCut + rightCut <= cutOff); line = line.left(line.length() - rightCut); line = line.mid(leftCut); range += KTextEditor::Range(0, -leftCut, 0, -leftCut); Q_ASSERT(range.start().column() >= 0 && range.end().column() <= line.length()); //TODO: share code with context browser // mixing (255, 255, 0, 100) with white yields this: const QColor foreground(0, 0, 0); return QLatin1String("") + line.left(range.start().column()).toHtmlEscaped() + QLatin1String("") + line.mid(range.start().column(), range.end().column() - range.start().column()).toHtmlEscaped() + QLatin1String("") + line.mid(range.end().column(), line.length() - range.end().column()).toHtmlEscaped() + QLatin1String(""); } /** * Note: the links in the HTML here are only used for styling * the navigation is implemented in the mouse press event handler */ OneUseWidget::OneUseWidget(IndexedDeclaration declaration, const IndexedString& document, KTextEditor::Range range, const CodeRepresentation& code) : m_range(new PersistentMovingRange(range, document)), m_declaration(declaration), m_document(document) { //Make the sizing of this widget independent of the content, because we will adapt the content to the size setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); m_sourceLine = code.line(m_range->range().start().line()); m_layout = new QHBoxLayout(this); m_layout->setContentsMargins(0, 0, 0, 0); setLayout(m_layout); setCursor(Qt::PointingHandCursor); m_label = new QLabel(this); m_icon = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme(QStringLiteral("code-function")).pixmap(16)); DUChainReadLocker lock(DUChain::lock()); QString text = QLatin1String("") + i18nc("refers to a line in source code", "Line %1:", range.start().line()) + QStringLiteral(""); if(!m_sourceLine.isEmpty() && m_sourceLine.length() > m_range->range().end().column()) { text += QLatin1String("  ") + highlightAndEscapeUseText(m_sourceLine, 0, m_range->range()); //Useful tooltip: int start = m_range->range().start().line() - tooltipContextSize; int end = m_range->range().end().line() + tooltipContextSize + 1; - QString toolTipText; + QStringList toolTipLines; for(int a = start; a < end; ++a) { QString lineText = code.line(a).toHtmlEscaped(); if (m_range->range().start().line() <= a && m_range->range().end().line() >= a) { lineText = QStringLiteral("") + lineText + QStringLiteral(""); } if(!lineText.trimmed().isEmpty()) { - toolTipText += lineText + QLatin1String("
"); + toolTipLines.append(lineText); } } - if ( toolTipText.endsWith(QLatin1String("
")) ) { - toolTipText.remove(toolTipText.length() - 4, 4); - } - setToolTip(QStringLiteral("
") + toolTipText + QStringLiteral("
")); + setToolTip(QStringLiteral("
") + toolTipLines.join(QLatin1String("
")) + QStringLiteral("
")); } m_label->setText(text); m_layout->addWidget(m_icon); m_layout->addWidget(m_label); m_layout->setAlignment(Qt::AlignLeft); } void OneUseWidget::setHighlighted(bool highlight) { if (highlight == m_isHighlighted) { return; } if (highlight) { m_label->setText(m_label->text().replace(QLatin1String("background-color:") + backgroundColor(false), QLatin1String("background-color:") + backgroundColor(true))); m_isHighlighted = true; } else { m_label->setText(m_label->text().replace(QLatin1String("background-color:") + backgroundColor(true), QLatin1String("background-color:") + backgroundColor(false))); m_isHighlighted = false; } } bool KDevelop::OneUseWidget::isHighlighted() const { return m_isHighlighted; } void OneUseWidget::activateLink() { ICore::self()->documentController()->openDocument(m_document.toUrl(), m_range->range().start()); } void OneUseWidget::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton && !event->modifiers()) { activateLink(); event->accept(); } } OneUseWidget::~OneUseWidget() { } void OneUseWidget::resizeEvent ( QResizeEvent * event ) { ///Adapt the content QSize size = event->size(); KTextEditor::Range range = m_range->range(); int cutOff = 0; int maxCutOff = m_sourceLine.length() - (range.end().column() - range.start().column()); //Reset so we also get more context while up-sizing m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1:", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); /// FIXME: this is incredibly ugly and slow... we could simply paint the text ourselves and elide it properly while(sizeHint().width() > size.width() && cutOff < maxCutOff) { //We've got to save space m_label->setText(QStringLiteral("") + i18nc("Refers to a line in source code", "Line %1:", range.start().line()+1) + QStringLiteral(" ") + highlightAndEscapeUseText(m_sourceLine, cutOff, range)); cutOff += 5; } event->accept(); QWidget::resizeEvent(event); } void NavigatableWidgetList::setShowHeader(bool show) { if(show && !m_headerLayout->parent()) m_layout->insertLayout(0, m_headerLayout); else m_headerLayout->setParent(nullptr); } NavigatableWidgetList::~NavigatableWidgetList() { delete m_headerLayout; } NavigatableWidgetList::NavigatableWidgetList(bool allowScrolling, uint maxHeight, bool vertical) : m_allowScrolling(allowScrolling) { m_layout = new QVBoxLayout; m_layout->setMargin(0); m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); m_layout->setSpacing(0); setBackgroundRole(QPalette::Base); m_useArrows = false; if(vertical) m_itemLayout = new QVBoxLayout; else m_itemLayout = new QHBoxLayout; m_itemLayout->setContentsMargins(0, 0, 0, 0); m_itemLayout->setMargin(0); m_itemLayout->setSpacing(0); // m_layout->setSizeConstraint(QLayout::SetMinAndMaxSize); // setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum); setWidgetResizable(true); m_headerLayout = new QHBoxLayout; m_headerLayout->setMargin(0); m_headerLayout->setSpacing(0); if(m_useArrows) { auto previousButton = new QToolButton(); previousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); auto nextButton = new QToolButton(); nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); m_headerLayout->addWidget(previousButton); m_headerLayout->addWidget(nextButton); } //hide these buttons for now, they're senseless m_layout->addLayout(m_headerLayout); QHBoxLayout* spaceLayout = new QHBoxLayout; spaceLayout->addSpacing(10); spaceLayout->addLayout(m_itemLayout); m_layout->addLayout(spaceLayout); if(maxHeight) setMaximumHeight(maxHeight); if(m_allowScrolling) { QWidget* contentsWidget = new QWidget; contentsWidget->setLayout(m_layout); setWidget(contentsWidget); }else{ setLayout(m_layout); } } void NavigatableWidgetList::deleteItems() { foreach(QWidget* item, items()) delete item; } void NavigatableWidgetList::addItem(QWidget* widget, int pos) { if(pos == -1) m_itemLayout->addWidget(widget); else m_itemLayout->insertWidget(pos, widget); } QList NavigatableWidgetList::items() const { QList ret; for(int a = 0; a < m_itemLayout->count(); ++a) { QWidgetItem* widgetItem = dynamic_cast(m_itemLayout->itemAt(a)); if(widgetItem) { ret << widgetItem->widget(); } } return ret; } bool NavigatableWidgetList::hasItems() const { return (bool)m_itemLayout->count(); } void NavigatableWidgetList::addHeaderItem(QWidget* widget, Qt::Alignment alignment) { if(m_useArrows) { Q_ASSERT(m_headerLayout->count() >= 2); //At least the 2 back/next buttons m_headerLayout->insertWidget(m_headerLayout->count()-1, widget, alignment); }else{ //We need to do this so the header doesn't get stretched widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); m_headerLayout->insertWidget(m_headerLayout->count(), widget, alignment); // widget->setMaximumHeight(20); } } ///Returns whether the uses in the child should be a new uses-group bool isNewGroup(DUContext* parent, DUContext* child) { if(parent->type() == DUContext::Other && child->type() == DUContext::Other) return false; else return true; } uint countUses(int usedDeclarationIndex, DUContext* context) { uint ret = 0; for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ++ret; foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += countUses(usedDeclarationIndex, child); return ret; } QList createUseWidgets(const CodeRepresentation& code, int usedDeclarationIndex, IndexedDeclaration decl, DUContext* context) { QList ret; VERIFY_FOREGROUND_LOCKED for(int useIndex = 0; useIndex < context->usesCount(); ++useIndex) if(context->uses()[useIndex].m_declarationIndex == usedDeclarationIndex) ret << new OneUseWidget(decl, context->url(), context->transformFromLocalRevision(context->uses()[useIndex].m_range), code); foreach(DUContext* child, context->childContexts()) if(!isNewGroup(context, child)) ret += createUseWidgets(code, usedDeclarationIndex, decl, child); return ret; } ContextUsesWidget::ContextUsesWidget(const CodeRepresentation& code, QList usedDeclarations, IndexedDUContext context) : m_context(context) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); QString headerText = i18n("Unknown context"); setUpdatesEnabled(false); if(context.data()) { DUContext* ctx = context.data(); if(ctx->scopeIdentifier(true).isEmpty()) headerText = i18n("Global"); else { headerText = ctx->scopeIdentifier(true).toString(); if(ctx->type() == DUContext::Function || (ctx->owner() && ctx->owner()->isFunctionDeclaration())) headerText += QLatin1String("(...)"); } QSet hadIndices; foreach(const IndexedDeclaration usedDeclaration, usedDeclarations) { int usedDeclarationIndex = ctx->topContext()->indexForUsedDeclaration(usedDeclaration.data(), false); if(hadIndices.contains(usedDeclarationIndex)) continue; hadIndices.insert(usedDeclarationIndex); if(usedDeclarationIndex != std::numeric_limits::max()) { foreach(OneUseWidget* widget, createUseWidgets(code, usedDeclarationIndex, usedDeclaration, ctx)) addItem(widget); } } } QLabel* headerLabel = new QLabel(i18nc("%1: source file", "In %1", QLatin1String("") + headerText.toHtmlEscaped() + QLatin1String(": "))); addHeaderItem(headerLabel); setUpdatesEnabled(true); connect(headerLabel, &QLabel::linkActivated, this, &ContextUsesWidget::linkWasActivated); } void ContextUsesWidget::linkWasActivated(const QString& link) { if ( link == QLatin1String("navigateToFunction") ) { DUChainReadLocker lock(DUChain::lock()); DUContext* context = m_context.context(); if(context) { CursorInRevision contextStart = context->range().start; KTextEditor::Cursor cursor(contextStart.line, contextStart.column); QUrl url = context->url().toUrl(); lock.unlock(); ForegroundLock fgLock; ICore::self()->documentController()->openDocument(url, cursor); } } } DeclarationWidget::DeclarationWidget(const CodeRepresentation& code, const IndexedDeclaration& decl) { setFrameShape(NoFrame); DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); if (Declaration* dec = decl.data()) { QLabel* headerLabel = new QLabel(dec->isDefinition() ? i18n("Definition") : i18n("Declaration")); addHeaderItem(headerLabel); addItem(new OneUseWidget(decl, dec->url(), dec->rangeInCurrentRevision(), code)); } setUpdatesEnabled(true); } TopContextUsesWidget::TopContextUsesWidget(IndexedDeclaration declaration, QList allDeclarations, IndexedTopDUContext topContext) : m_topContext(topContext) , m_declaration(declaration) , m_allDeclarations(allDeclarations) , m_usesCount(0) { m_itemLayout->setContentsMargins(10, 0, 0, 5); setFrameShape(NoFrame); setUpdatesEnabled(false); DUChainReadLocker lock(DUChain::lock()); QHBoxLayout * labelLayout = new QHBoxLayout; labelLayout->setContentsMargins(0, -1, 0, 0); // let's keep the spacing *above* the line QWidget* headerWidget = new QWidget; headerWidget->setLayout(labelLayout); headerWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); QLabel* label = new QLabel(this); m_icon = new QLabel(this); m_toggleButton = new QLabel(this); m_icon->setPixmap(QIcon::fromTheme(QStringLiteral("code-class")).pixmap(16)); labelLayout->addWidget(m_icon); labelLayout->addWidget(label); labelLayout->addWidget(m_toggleButton); labelLayout->setAlignment(Qt::AlignLeft); if(topContext.isLoaded()) m_usesCount = DUChainUtils::contextCountUses(topContext.data(), declaration.data()); QString labelText = i18ncp("%1: number of uses, %2: filename with uses", "%2: 1 use", "%2: %1 uses", m_usesCount, ICore::self()->projectController()->prettyFileName(topContext.url().toUrl())); label->setText(labelText); m_toggleButton->setText(QLatin1String("   [") + i18nc("Refers to closing a UI element", "Collapse") + QLatin1String("]")); connect(m_toggleButton, &QLabel::linkActivated, this, &TopContextUsesWidget::labelClicked); addHeaderItem(headerWidget); setUpdatesEnabled(true); } int TopContextUsesWidget::usesCount() const { return m_usesCount; } QList buildContextUses(const CodeRepresentation& code, const QList& declarations, DUContext* context) { QList ret; if(!context->parentContext() || isNewGroup(context->parentContext(), context)) { ContextUsesWidget* created = new ContextUsesWidget(code, declarations, context); if(created->hasItems()) ret << created; else delete created; } foreach(DUContext* child, context->childContexts()) ret += buildContextUses(code, declarations, child); return ret; } void TopContextUsesWidget::setExpanded(bool expanded) { if(!expanded) { m_toggleButton->setText(QLatin1String("   [") + i18nc("Refers to opening a UI element", "Expand") + QLatin1String("]")); deleteItems(); }else{ m_toggleButton->setText(QLatin1String("   [") + i18nc("Refers to closing a UI element", "Collapse") + QLatin1String("]")); if(hasItems()) return; DUChainReadLocker lock(DUChain::lock()); TopDUContext* topContext = m_topContext.data(); if(topContext && m_declaration.data()) { CodeRepresentation::Ptr code = createCodeRepresentation(topContext->url()); setUpdatesEnabled(false); IndexedTopDUContext localTopContext(topContext); foreach(const IndexedDeclaration &decl, m_allDeclarations) { if(decl.indexedTopContext() == localTopContext) { addItem(new DeclarationWidget(*code, decl)); } } foreach(ContextUsesWidget* usesWidget, buildContextUses(*code, m_allDeclarations, topContext)) { addItem(usesWidget); } setUpdatesEnabled(true); } } } void TopContextUsesWidget::labelClicked() { if(hasItems()) { setExpanded(false); }else{ setExpanded(true); } } UsesWidget::~UsesWidget() { if (m_collector) { m_collector->setWidget(nullptr); } } UsesWidget::UsesWidget(const IndexedDeclaration& declaration, const QSharedPointer& customCollector) : NavigatableWidgetList(true) { DUChainReadLocker lock(DUChain::lock()); setUpdatesEnabled(false); m_headerLine = new QLabel; redrawHeaderLine(); connect(m_headerLine, &QLabel::linkActivated, this, &UsesWidget::headerLinkActivated); m_layout->insertWidget(0, m_headerLine, 0, Qt::AlignTop); m_layout->setAlignment(Qt::AlignTop); m_itemLayout->setAlignment(Qt::AlignTop); m_progressBar = new QProgressBar; addHeaderItem(m_progressBar); if (!customCollector) { m_collector = QSharedPointer(new UsesWidget::UsesWidgetCollector(declaration)); } else { m_collector = customCollector; } m_collector->setProcessDeclarations(true); m_collector->setWidget(this); m_collector->startCollecting(); setUpdatesEnabled(true); } void UsesWidget::redrawHeaderLine() { m_headerLine->setText(headerLineText()); } const QString UsesWidget::headerLineText() const { return i18np("1 use found", "%1 uses found", countAllUses()) + QLatin1String(" • " "[") + i18n("Expand all") + QLatin1String("] • " "[") + i18n("Collapse all") + QLatin1String("]"); } unsigned int UsesWidget::countAllUses() const { unsigned int totalUses = 0; foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { totalUses += useWidget->usesCount(); } } return totalUses; } void UsesWidget::setAllExpanded(bool expanded) { foreach ( QWidget* w, items() ) { if ( TopContextUsesWidget* useWidget = dynamic_cast(w) ) { useWidget->setExpanded(expanded); } } } void UsesWidget::headerLinkActivated(const QString& linkName) { if(linkName == QLatin1String("expandAll")) { setAllExpanded(true); } else if(linkName == QLatin1String("collapseAll")) { setAllExpanded(false); } } UsesWidget::UsesWidgetCollector::UsesWidgetCollector(IndexedDeclaration decl) : UsesCollector(decl), m_widget(nullptr) { } void UsesWidget::UsesWidgetCollector::setWidget(UsesWidget* widget ) { m_widget = widget; } void UsesWidget::UsesWidgetCollector::maximumProgress(uint max) { if (!m_widget) { return; } if(m_widget->m_progressBar) { m_widget->m_progressBar->setMaximum(max); m_widget->m_progressBar->setMinimum(0); m_widget->m_progressBar->setValue(0); }else{ qCWarning(LANGUAGE) << "maximumProgress called twice"; } } void UsesWidget::UsesWidgetCollector::progress(uint processed, uint total) { if (!m_widget) { return; } m_widget->redrawHeaderLine(); if(m_widget->m_progressBar) { m_widget->m_progressBar->setValue(processed); if(processed == total) { m_widget->setUpdatesEnabled(false); delete m_widget->m_progressBar; m_widget->m_progressBar = nullptr; m_widget->setShowHeader(false); m_widget->setUpdatesEnabled(true); } }else{ qCWarning(LANGUAGE) << "progress() called too often"; } } void UsesWidget::UsesWidgetCollector::processUses( KDevelop::ReferencedTopDUContext topContext ) { if (!m_widget) { return; } DUChainReadLocker lock; qCDebug(LANGUAGE) << "processing" << topContext->url().str(); TopContextUsesWidget* widget = new TopContextUsesWidget(declaration(), declarations(), topContext.data()); // move to back if it's just the declaration/definition bool toBack = widget->usesCount() == 0; // move to front the item belonging to the current open document IDocument* doc = ICore::self()->documentController()->activeDocument(); bool toFront = doc && (doc->url() == topContext->url().toUrl()); widget->setExpanded(true); m_widget->addItem(widget, toFront ? 0 : toBack ? widget->items().size() : -1); m_widget->redrawHeaderLine(); } QSize KDevelop::UsesWidget::sizeHint() const { QSize ret = QWidget::sizeHint(); if(ret.height() < 300) ret.setHeight(300); return ret; } diff --git a/kdevplatform/language/duchain/parsingenvironment.cpp b/kdevplatform/language/duchain/parsingenvironment.cpp index f4d3dfdea6..8fb329d9ef 100644 --- a/kdevplatform/language/duchain/parsingenvironment.cpp +++ b/kdevplatform/language/duchain/parsingenvironment.cpp @@ -1,388 +1,389 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "parsingenvironment.h" #include "topducontext.h" #include "duchainregister.h" #include "topducontextdynamicdata.h" #include "duchain.h" #include "duchainlock.h" #include "topducontextdata.h" #include #include #define ENSURE_READ_LOCKED if(indexedTopContext().isValid()) { ENSURE_CHAIN_READ_LOCKED } #define ENSURE_WRITE_LOCKED if(indexedTopContext().isValid()) { ENSURE_CHAIN_READ_LOCKED } namespace KDevelop { StaticParsingEnvironmentData* ParsingEnvironmentFile::m_staticData = nullptr; #if 0 ///Wrapper class around objects that are managed through the DUChain, and may contain arbitrary objects ///that support duchain-like store (IndexedString, StorableSet, and the likes). The object must not contain pointers ///or other non-persistent data. /// ///The object is stored during the normal duchain storage/cleanup cycles. template struct PersistentDUChainObject { ///@param fileName File-name that will be used to store the data of the object in the duchain directory PersistentDUChainObject(QString fileName) { object = (T*) new char[sizeof(T)]; if(!DUChain::self()->addPersistentObject(object, fileName, sizeof(T))) { //The constructor is called only if the object did not exist yet new (object) T(); } } ~PersistentDUChainObject() { DUChain::self()->unregisterPersistentObject(object); delete[] object; } T* object; }; #endif REGISTER_DUCHAIN_ITEM(ParsingEnvironmentFile); TopDUContext::Features ParsingEnvironmentFile::features() const { ENSURE_READ_LOCKED return d_func()->m_features; } ParsingEnvironment::ParsingEnvironment() { } ParsingEnvironment::~ParsingEnvironment() { } IndexedString ParsingEnvironmentFile::url() const { ENSURE_READ_LOCKED return d_func()->m_url; } bool ParsingEnvironmentFile::needsUpdate(const ParsingEnvironment* /*environment*/) const { ENSURE_READ_LOCKED return d_func()->m_allModificationRevisions.needsUpdate(); } bool ParsingEnvironmentFile::matchEnvironment(const ParsingEnvironment* /*environment*/) const { ENSURE_READ_LOCKED return true; } void ParsingEnvironmentFile::setTopContext(KDevelop::IndexedTopDUContext context) { if(d_func()->m_topContext == context) return; ENSURE_WRITE_LOCKED d_func_dynamic()->m_topContext = context; //Enforce an update of the 'features satisfied' caches TopDUContext::Features oldFeatures = features(); setFeatures(TopDUContext::Empty); setFeatures(oldFeatures); } KDevelop::IndexedTopDUContext ParsingEnvironmentFile::indexedTopContext() const { return d_func()->m_topContext; } const ModificationRevisionSet& ParsingEnvironmentFile::allModificationRevisions() const { ENSURE_READ_LOCKED return d_func()->m_allModificationRevisions; } void ParsingEnvironmentFile::addModificationRevisions(const ModificationRevisionSet& revisions) { ENSURE_WRITE_LOCKED d_func_dynamic()->m_allModificationRevisions += revisions; } ParsingEnvironmentFile::ParsingEnvironmentFile(ParsingEnvironmentFileData& data, const IndexedString& url) : DUChainBase(data) { d_func_dynamic()->m_url = url; d_func_dynamic()->m_modificationTime = ModificationRevision::revisionForFile(url); addModificationRevision(url, d_func_dynamic()->m_modificationTime); Q_ASSERT(d_func()->m_allModificationRevisions.index()); } ParsingEnvironmentFile::ParsingEnvironmentFile(const IndexedString& url) : DUChainBase(*new ParsingEnvironmentFileData()) { d_func_dynamic()->setClassId(this); d_func_dynamic()->m_url = url; d_func_dynamic()->m_modificationTime = ModificationRevision::revisionForFile(url); addModificationRevision(url, d_func_dynamic()->m_modificationTime); Q_ASSERT(d_func()->m_allModificationRevisions.index()); } TopDUContext* ParsingEnvironmentFile::topContext() const { ENSURE_READ_LOCKED return indexedTopContext().data(); } ParsingEnvironmentFile::~ParsingEnvironmentFile() { } ParsingEnvironmentFile::ParsingEnvironmentFile(ParsingEnvironmentFileData& data) : DUChainBase(data) { //If this triggers, the item has most probably not been initialized with the correct constructor that takes an IndexedString. Q_ASSERT(d_func()->m_allModificationRevisions.index()); } int ParsingEnvironment::type() const { return StandardParsingEnvironment; } int ParsingEnvironmentFile::type() const { ENSURE_READ_LOCKED return StandardParsingEnvironment; } bool ParsingEnvironmentFile::isProxyContext() const { ENSURE_READ_LOCKED return d_func()->m_isProxyContext; } void ParsingEnvironmentFile::setIsProxyContext(bool is) { ENSURE_WRITE_LOCKED d_func_dynamic()->m_isProxyContext = is; } QList< QExplicitlySharedDataPointer > ParsingEnvironmentFile::imports() const { ENSURE_READ_LOCKED QList imp; IndexedTopDUContext top = indexedTopContext(); if(top.isLoaded()) { TopDUContext* topCtx = top.data(); + imp.reserve(topCtx->d_func()->m_importedContextsSize()); FOREACH_FUNCTION(const DUContext::Import& import, topCtx->d_func()->m_importedContexts) imp << import.indexedContext(); }else{ imp = TopDUContextDynamicData::loadImports(top.index()); } QList< QExplicitlySharedDataPointer > ret; foreach(const IndexedDUContext ctx, imp) { QExplicitlySharedDataPointer item = DUChain::self()->environmentFileForDocument(ctx.topContextIndex()); if(item) { ret << item; }else{ qCDebug(LANGUAGE) << url().str() << indexedTopContext().index() << ": invalid import" << ctx.topContextIndex(); } } return ret; } QList< QExplicitlySharedDataPointer > ParsingEnvironmentFile::importers() const { ENSURE_READ_LOCKED QList imp; IndexedTopDUContext top = indexedTopContext(); if(top.isLoaded()) { TopDUContext* topCtx = top.data(); FOREACH_FUNCTION(const IndexedDUContext& ctx, topCtx->d_func()->m_importers) imp << ctx; }else{ imp = TopDUContextDynamicData::loadImporters(top.index()); } QList< QExplicitlySharedDataPointer > ret; foreach(const IndexedDUContext ctx, imp) { QExplicitlySharedDataPointer f = DUChain::self()->environmentFileForDocument(ctx.topContextIndex()); if(f) ret << f; else qCDebug(LANGUAGE) << url().str() << indexedTopContext().index() << ": invalid importer context" << ctx.topContextIndex(); } return ret; } QMutex featureSatisfactionMutex; inline bool satisfied(TopDUContext::Features features, TopDUContext::Features required) { return (features & required) == required; } ///Makes sure the file has the correct features attached, and if minimumFeatures contains AllDeclarationsContextsAndUsesForRecursive, then also checks all imports. bool ParsingEnvironmentFile::featuresMatch(TopDUContext::Features minimumFeatures, QSet& checked) const { if(checked.contains(this)) return true; checked.insert(this); TopDUContext::Features localRequired = (TopDUContext::Features) (minimumFeatures | ParseJob::staticMinimumFeatures(url())); //Check other 'local' requirements localRequired = (TopDUContext::Features)(localRequired & (TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::AST)); if(!satisfied(features(), localRequired)) return false; if(ParseJob::hasStaticMinimumFeatures()) { //Do a manual recursion to check whether any of the relevant contexts has static minimum features set ///@todo Only do this if one of the imports actually has static features attached (by RecursiveImports set intersection) foreach(const ParsingEnvironmentFilePointer &import, imports()) if(!import->featuresMatch(minimumFeatures & TopDUContext::Recursive ? minimumFeatures : ((TopDUContext::Features)0), checked)) return false; }else if(minimumFeatures & TopDUContext::Recursive) { QMutexLocker lock(&featureSatisfactionMutex); TopDUContext::IndexedRecursiveImports recursiveImportIndices = d_func()->m_importsCache; if(recursiveImportIndices.isEmpty()) { //Unfortunately, we have to load the top-context TopDUContext* top = topContext(); if(top) recursiveImportIndices = top->recursiveImportIndices(); } ///@todo Do not create temporary intersected sets //Use the features-cache to efficiently check the recursive satisfaction of the features if(satisfied(minimumFeatures, TopDUContext::AST) && !((m_staticData->ASTSatisfied & recursiveImportIndices) == recursiveImportIndices)) return false; if(satisfied(minimumFeatures, TopDUContext::AllDeclarationsContextsAndUses)) return (m_staticData->allDeclarationsAndUsesSatisfied & recursiveImportIndices) == recursiveImportIndices; else if(satisfied(minimumFeatures, TopDUContext::AllDeclarationsAndContexts)) return (m_staticData->allDeclarationsSatisfied & recursiveImportIndices) == recursiveImportIndices; else if(satisfied(minimumFeatures, TopDUContext::VisibleDeclarationsAndContexts)) return (m_staticData->visibleDeclarationsSatisfied & recursiveImportIndices) == recursiveImportIndices; else if(satisfied(minimumFeatures, TopDUContext::SimplifiedVisibleDeclarationsAndContexts)) return (m_staticData->simplifiedVisibleDeclarationsSatisfied & recursiveImportIndices) == recursiveImportIndices; } return true; } void ParsingEnvironmentFile::setFeatures(TopDUContext::Features features) { if(d_func()->m_features == features) return; ENSURE_WRITE_LOCKED d_func_dynamic()->m_features = features; if(indexedTopContext().isValid()) { QMutexLocker lock(&featureSatisfactionMutex); if(!satisfied(features, TopDUContext::SimplifiedVisibleDeclarationsAndContexts)) m_staticData->simplifiedVisibleDeclarationsSatisfied.remove(indexedTopContext()); else m_staticData->simplifiedVisibleDeclarationsSatisfied.insert(indexedTopContext()); if(!satisfied(features, TopDUContext::VisibleDeclarationsAndContexts)) m_staticData->visibleDeclarationsSatisfied.remove(indexedTopContext()); else m_staticData->visibleDeclarationsSatisfied.insert(indexedTopContext()); if(!satisfied(features, TopDUContext::AllDeclarationsAndContexts)) m_staticData->allDeclarationsSatisfied.remove(indexedTopContext()); else m_staticData->allDeclarationsSatisfied.insert(indexedTopContext()); if(!satisfied(features, TopDUContext::AllDeclarationsContextsAndUses)) m_staticData->allDeclarationsAndUsesSatisfied.remove(indexedTopContext()); else m_staticData->allDeclarationsAndUsesSatisfied.insert(indexedTopContext()); if(!satisfied(features, TopDUContext::AST)) m_staticData->ASTSatisfied.remove(indexedTopContext()); else m_staticData->ASTSatisfied.insert(indexedTopContext()); } } bool ParsingEnvironmentFile::featuresSatisfied(KDevelop::TopDUContext::Features minimumFeatures) const { ENSURE_READ_LOCKED QSet checked; if(minimumFeatures & TopDUContext::ForceUpdate) return false; return featuresMatch(minimumFeatures, checked); } void ParsingEnvironmentFile::clearModificationRevisions() { ENSURE_WRITE_LOCKED d_func_dynamic()->m_allModificationRevisions.clear(); d_func_dynamic()->m_allModificationRevisions.addModificationRevision(d_func()->m_url, d_func()->m_modificationTime); } void ParsingEnvironmentFile::addModificationRevision(const IndexedString& url, const ModificationRevision& revision) { ENSURE_WRITE_LOCKED d_func_dynamic()->m_allModificationRevisions.addModificationRevision(url, revision); { //Test Q_ASSERT(d_func_dynamic()->m_allModificationRevisions.index()); bool result = d_func_dynamic()->m_allModificationRevisions.removeModificationRevision(url, revision); Q_UNUSED( result ); Q_ASSERT( result ); d_func_dynamic()->m_allModificationRevisions.addModificationRevision(url, revision); } } void ParsingEnvironmentFile::setModificationRevision( const KDevelop::ModificationRevision& rev ) { ENSURE_WRITE_LOCKED Q_ASSERT(d_func_dynamic()->m_allModificationRevisions.index()); bool result = d_func_dynamic()->m_allModificationRevisions.removeModificationRevision(d_func()->m_url, d_func()->m_modificationTime); Q_ASSERT( result ); Q_UNUSED( result ); #ifdef LEXERCACHE_DEBUG if(debugging()) { qCDebug(LANGUAGE) << id(this) << "setting modification-revision" << rev.toString(); } #endif d_func_dynamic()->m_modificationTime = rev; #ifdef LEXERCACHE_DEBUG if(debugging()) { qCDebug(LANGUAGE) << id(this) << "new modification-revision" << m_modificationTime; } #endif d_func_dynamic()->m_allModificationRevisions.addModificationRevision(d_func()->m_url, d_func()->m_modificationTime); } KDevelop::ModificationRevision ParsingEnvironmentFile::modificationRevision() const { ENSURE_READ_LOCKED return d_func()->m_modificationTime; } IndexedString ParsingEnvironmentFile::language() const { return d_func()->m_language; } void ParsingEnvironmentFile::setLanguage(IndexedString language) { d_func_dynamic()->m_language = language; } const KDevelop::TopDUContext::IndexedRecursiveImports& ParsingEnvironmentFile::importsCache() const { return d_func()->m_importsCache; } void ParsingEnvironmentFile::setImportsCache(const KDevelop::TopDUContext::IndexedRecursiveImports& importsCache) { d_func_dynamic()->m_importsCache = importsCache; } } //KDevelop diff --git a/kdevplatform/language/duchain/problem.cpp b/kdevplatform/language/duchain/problem.cpp index 44ad00c1b6..de7da1bbbd 100644 --- a/kdevplatform/language/duchain/problem.cpp +++ b/kdevplatform/language/duchain/problem.cpp @@ -1,284 +1,285 @@ /* This file is part of KDevelop Copyright 2007 Hamish Rodda This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "problem.h" #include "duchainregister.h" #include "topducontextdynamicdata.h" #include "topducontext.h" #include "topducontextdata.h" #include "duchain.h" #include "duchainlock.h" #include #include #include namespace KDevelop { REGISTER_DUCHAIN_ITEM(Problem); DEFINE_LIST_MEMBER_HASH(ProblemData, diagnostics, LocalIndexedProblem) } using namespace KDevelop; LocalIndexedProblem::LocalIndexedProblem(const ProblemPointer& problem, const TopDUContext* top) : m_index(problem->m_indexInTopContext) { ENSURE_CHAIN_READ_LOCKED // ensure child problems are properly serialized before we serialize the parent problem // see below, the diagnostic size is kept in sync by the mutable API of Problem // the const cast is ugly but we don't really "change" the state as observed from the outside auto& serialized = const_cast(problem.data())->d_func_dynamic()->diagnosticsList(); serialized.clear(); + serialized.reserve(problem->m_diagnostics.size()); foreach(const ProblemPointer& child, problem->m_diagnostics) { serialized << LocalIndexedProblem(child, top); } if (!m_index) { m_index = top->m_dynamicData->allocateProblemIndex(problem); } } ProblemPointer LocalIndexedProblem::data(const TopDUContext* top) const { if (!m_index) { return {}; } return top->m_dynamicData->getProblemForIndex(m_index); } Problem::Problem() : DUChainBase(*new ProblemData) { d_func_dynamic()->setClassId(this); } Problem::Problem(ProblemData& data) : DUChainBase(data) { } Problem::~Problem() { } TopDUContext* Problem::topContext() const { return m_topContext.data(); } IndexedString Problem::url() const { return d_func()->url; } DocumentRange Problem::finalLocation() const { return DocumentRange(d_func()->url, d_func()->m_range.castToSimpleRange()); } void Problem::setFinalLocation(const DocumentRange& location) { setRange(RangeInRevision::castFromSimpleRange(location)); d_func_dynamic()->url = location.document; } IProblem::FinalLocationMode Problem::finalLocationMode() const { return d_func()->finalLocationMode; } void Problem::setFinalLocationMode(IProblem::FinalLocationMode mode) { d_func_dynamic()->finalLocationMode = mode; } void Problem::clearDiagnostics() { m_diagnostics.clear(); // keep serialization in sync, see also LocalIndexedProblem ctor above d_func_dynamic()->diagnosticsList().clear(); } QVector Problem::diagnostics() const { QVector vector; foreach(ProblemPointer ptr, m_diagnostics) { vector.push_back(ptr); } return vector; } void Problem::setDiagnostics(const QVector &diagnostics) { clearDiagnostics(); foreach(const IProblem::Ptr& problem, diagnostics) { addDiagnostic(problem); } } void Problem::addDiagnostic(const IProblem::Ptr &diagnostic) { Problem *problem = dynamic_cast(diagnostic.data()); Q_ASSERT(problem != nullptr); ProblemPointer ptr(problem); m_diagnostics << ptr; } QString Problem::description() const { return d_func()->description.str(); } void Problem::setDescription(const QString& description) { d_func_dynamic()->description = IndexedString(description); } QString Problem::explanation() const { return d_func()->explanation.str(); } void Problem::setExplanation(const QString& explanation) { d_func_dynamic()->explanation = IndexedString(explanation); } IProblem::Source Problem::source() const { return d_func()->source; } void Problem::setSource(IProblem::Source source) { d_func_dynamic()->source = source; } QExplicitlySharedDataPointer Problem::solutionAssistant() const { return {}; } IProblem::Severity Problem::severity() const { return d_func()->severity; } void Problem::setSeverity(Severity severity) { d_func_dynamic()->severity = severity; } QString Problem::severityString() const { switch(severity()) { case IProblem::NoSeverity: return {}; case IProblem::Error: return i18n("Error"); case IProblem::Warning: return i18n("Warning"); case IProblem::Hint: return i18n("Hint"); } return QString(); } QString Problem::sourceString() const { switch (source()) { case IProblem::Disk: return i18n("Disk"); case IProblem::Preprocessor: return i18n("Preprocessor"); case IProblem::Lexer: return i18n("Lexer"); case IProblem::Parser: return i18n("Parser"); case IProblem::DUChainBuilder: return i18n("Definition-Use Chain"); case IProblem::SemanticAnalysis: return i18n("Semantic analysis"); case IProblem::ToDo: return i18n("To-do"); case IProblem::Unknown: default: return i18n("Unknown"); } } QString Problem::toString() const { return i18nc(": in :[]: (found by )", "%1: %2 in %3:[(%4,%5),(%6,%7)]: %8 (found by %9)" , severityString() , description() , url().str() , range().start.line , range().start.column , range().end.line , range().end.column , (explanation().isEmpty() ? i18n("") : explanation()) , sourceString()); } void Problem::rebuildDynamicData(DUContext* parent, uint ownIndex) { auto top = dynamic_cast(parent); Q_ASSERT(top); m_topContext = top; m_indexInTopContext = ownIndex; // deserialize child diagnostics here, as the top-context might get unloaded // but we still want to keep the child-diagnostics in-tact, as one would assume // a shared-ptr works. const auto data = d_func(); m_diagnostics.reserve(data->diagnosticsSize()); for (uint i = 0; i < data->diagnosticsSize(); ++i) { m_diagnostics << ProblemPointer(data->diagnostics()[i].data(top)); } DUChainBase::rebuildDynamicData(parent, ownIndex); } QDebug operator<<(QDebug s, const Problem& problem) { s.nospace() << problem.toString(); return s.space(); } QDebug operator<<(QDebug s, const ProblemPointer& problem) { if (!problem) { s.nospace() << ""; } else { s.nospace() << problem->toString(); } return s.space(); } diff --git a/kdevplatform/language/duchain/stringhelpers.cpp b/kdevplatform/language/duchain/stringhelpers.cpp index ac733e1302..d582ef505f 100644 --- a/kdevplatform/language/duchain/stringhelpers.cpp +++ b/kdevplatform/language/duchain/stringhelpers.cpp @@ -1,586 +1,589 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "stringhelpers.h" #include "safetycounter.h" #include #include #include #include namespace { template int strip_impl(const T& str, T& from) { if( str.isEmpty() ) return 0; int i = 0; int ip = 0; int s = from.length(); for( int a = 0; a < s; a++ ) { if( QChar(from[a]).isSpace() ) { continue; } else { if( from[a] == str[i] ) { i++; ip = a+1; if( i == (int)str.length() ) break; } else { break; } } } if( ip ) { from.remove(0, ip); } return s - from.length(); } template int rStrip_impl(const T& str, T& from) { if( str.isEmpty() ) return 0; int i = 0; int ip = from.length(); int s = from.length(); for( int a = s-1; a >= 0; a-- ) { if( QChar( from[a] ).isSpace() ) { ///@todo Check whether this can cause problems in utf-8, as only one real character is treated! continue; } else { if( from[a] == str[i] ) { i++; ip = a; if( i == (int)str.length() ) break; } else { break; } } } if( ip != (int)from.length() ) { from = from.left( ip ); } return s - from.length(); } template T formatComment_impl(const T& comment) { T ret; QList lines = comment.split('\n'); if ( !lines.isEmpty() ) { auto it = lines.begin(); auto eit = lines.end(); // remove common leading chars from the beginning of lines for( ; it != eit; ++it ) { // don't trigger repeated temporary allocations here static const T tripleSlash("///"); static const T doubleSlash("//"); static const T doubleStar("**"); static const T slashDoubleStar("/**"); strip_impl( tripleSlash, *it ); strip_impl( doubleSlash, *it ); strip_impl( doubleStar, *it ); rStrip_impl( slashDoubleStar, *it ); } foreach(const T& line, lines) { if(!ret.isEmpty()) ret += '\n'; ret += line; } } return ret.trimmed(); } } namespace KDevelop { class ParamIteratorPrivate { public: QString m_prefix; QString m_source; QString m_parens; int m_cur; int m_curEnd; int m_end; int next() const { return findCommaOrEnd( m_source, m_cur, m_parens[ 1 ] ); } }; bool parenFits( QChar c1, QChar c2 ) { if (c1 == QLatin1Char('<') && c2 == QLatin1Char('>')) return true; else if (c1 == QLatin1Char('(') && c2 == QLatin1Char(')')) return true; else if (c1 == QLatin1Char('[') && c2 == QLatin1Char(']')) return true; else if (c1 == QLatin1Char('{') && c2 == QLatin1Char('}')) return true; else return false; } int findClose( const QString& str , int pos ) { int depth = 0; QList st; QChar last = QLatin1Char(' '); for( int a = pos; a < (int)str.length(); a++) { switch(str[a].unicode()) { case '<': case '(': case '[': case '{': st.push_front( str[a] ); depth++; break; case '>': if (last == QLatin1Char('-')) break; Q_FALLTHROUGH(); case ')': case ']': case '}': if( !st.isEmpty() && parenFits(st.front(), str[a]) ) { depth--; st.pop_front(); } break; case '"': last = str[a]; a++; while (a < (int)str.length() && (str[a] != QLatin1Char('"') || last == QLatin1Char('\\'))) { last = str[a]; a++; } continue; break; case '\'': last = str[a]; a++; while( a < (int)str.length() && (str[a] != QLatin1Char('\'') || last == QLatin1Char('\\'))) { last = str[a]; a++; } continue; break; } last = str[a]; if( depth == 0 ) { return a; } } return -1; } int findCommaOrEnd( const QString& str , int pos, QChar validEnd) { for( int a = pos; a < (int)str.length(); a++) { switch(str[a].unicode()) { case '"': case '(': case '[': case '{': case '<': a = findClose( str, a ); if( a == -1 ) return str.length(); break; case ')': case ']': case '}': case '>': if( validEnd != QLatin1Char(' ') && validEnd != str[a] ) continue; Q_FALLTHROUGH(); case ',': return a; } } return str.length(); } QString reverse( const QString& str ) { QString ret; int len = str.length(); + ret.reserve(len); for( int a = len-1; a >= 0; --a ) { switch(str[a].unicode()) { case '(': ret += QLatin1Char(')'); continue; case '[': ret += QLatin1Char(']'); continue; case '{': ret += QLatin1Char('}'); continue; case '<': ret += QLatin1Char('>'); continue; case ')': ret += QLatin1Char('('); continue; case ']': ret += QLatin1Char('['); continue; case '}': ret += QLatin1Char('{'); continue; case '>': ret += QLatin1Char('<'); continue; default: ret += str[a]; continue; } } return ret; } ///@todo this hackery sucks QString escapeForBracketMatching(QString str) { str.replace(QStringLiteral("<<"), QStringLiteral("$&")); str.replace(QStringLiteral(">>"), QStringLiteral("$$")); str.replace(QStringLiteral("\\\""), QStringLiteral("$!")); str.replace(QStringLiteral("->"), QStringLiteral("$?")); return str; } QString escapeFromBracketMatching(QString str) { str.replace(QStringLiteral("$&"), QStringLiteral("<<")); str.replace(QStringLiteral("$$"), QStringLiteral(">>")); str.replace(QStringLiteral("$!"), QStringLiteral("\\\"")); str.replace(QStringLiteral("$?"), QStringLiteral("->")); return str; } void skipFunctionArguments(QString str, QStringList& skippedArguments, int& argumentsStart ) { QString withStrings = escapeForBracketMatching(str); str = escapeForBracketMatching(clearStrings(str)); //Blank out everything that can confuse the bracket-matching algorithm QString reversed = reverse( str.left(argumentsStart) ); QString withStringsReversed = reverse( withStrings.left(argumentsStart) ); //Now we should decrease argumentStart at the end by the count of steps we go right until we find the beginning of the function SafetyCounter s( 1000 ); int pos = 0; int len = reversed.length(); //we are searching for an opening-brace, but the reversion has also reversed the brace while( pos < len && s ) { int lastPos = pos; pos = KDevelop::findCommaOrEnd( reversed, pos ) ; if( pos > lastPos ) { QString arg = reverse( withStringsReversed.mid(lastPos, pos-lastPos) ).trimmed(); if( !arg.isEmpty() ) skippedArguments.push_front( escapeFromBracketMatching(arg) ); //We are processing the reversed reverseding, so push to front } if( reversed[pos] == QLatin1Char(')') || reversed[pos] == QLatin1Char('>') ) break; else ++pos; } if( !s ) { qCDebug(LANGUAGE) << "skipFunctionArguments: Safety-counter triggered"; } argumentsStart -= pos; } QString reduceWhiteSpace(QString str) { str = str.trimmed(); QString ret; + const int len = str.length(); + ret.reserve(len); QChar spaceChar = QLatin1Char(' '); bool hadSpace = false; - for( int a = 0; a < str.length(); a++ ) { + for (int a = 0; a < len; ++a) { if( str[a].isSpace() ) { hadSpace = true; } else { if( hadSpace ) { hadSpace = false; ret += spaceChar; } ret += str[a]; } } - + ret.squeeze(); return ret; } void fillString( QString& str, int start, int end, QChar replacement ) { for( int a = start; a < end; a++) str[a] = replacement; } QString stripFinalWhitespace(QString str) { for( int a = str.length() - 1; a >= 0; --a ) { if( !str[a].isSpace() ) return str.left( a+1 ); } return QString(); } QString clearComments( QString str, QChar replacement ) { QString withoutStrings = clearStrings(str, '$'); int pos = -1, newlinePos = -1, endCommentPos = -1, nextPos = -1, dest = -1; while ( (pos = str.indexOf(QLatin1Char('/'), pos + 1)) != -1 ) { newlinePos = withoutStrings.indexOf('\n', pos); if (withoutStrings[pos + 1] == QLatin1Char('/')) { //C style comment dest = newlinePos == -1 ? str.length() : newlinePos; fillString(str, pos, dest, replacement); pos = dest; } else if (withoutStrings[pos + 1] == QLatin1Char('*')) { //CPP style comment endCommentPos = withoutStrings.indexOf(QStringLiteral("*/"), pos + 2); if (endCommentPos != -1) endCommentPos += 2; dest = endCommentPos == -1 ? str.length() : endCommentPos; while (pos < dest) { nextPos = (dest > newlinePos && newlinePos != -1) ? newlinePos : dest; fillString(str, pos, nextPos, replacement); pos = nextPos; if (pos == newlinePos) { ++pos; //Keep newlines intact, skip them newlinePos = withoutStrings.indexOf(QLatin1Char('\n'), pos + 1); } } } } return str; } QString clearStrings( QString str, QChar replacement ) { bool inString = false; for(int pos = 0; pos < str.length(); ++pos) { //Skip cpp comments if(!inString && pos + 1 < str.length() && str[pos] == QLatin1Char('/') && str[pos+1] == QLatin1Char('*')) { pos += 2; while(pos + 1 < str.length()) { if (str[pos] == '*' && str[pos + 1] == QLatin1Char('/')) { ++pos; break; } ++pos; } } //Skip cstyle comments if(!inString && pos + 1 < str.length() && str[pos] == QLatin1Char('/') && str[pos+1] == QLatin1Char('/')) { pos += 2; while(pos < str.length() && str[pos] != QLatin1Char('\n')) { ++pos; } } //Skip a character a la 'b' if (!inString && str[pos] == QLatin1Char('\'') && pos + 3 <= str.length()) { //skip the opening ' str[pos] = replacement; ++pos; if (str[pos] == QLatin1Char('\\')) { //Skip an escape character str[pos] = replacement; ++pos; } //Skip the actual character str[pos] = replacement; ++pos; //Skip the closing ' if (pos < str.length() && str[pos] == QLatin1Char('\'')) { str[pos] = replacement; } continue; } bool intoString = false; if (str[pos] == QLatin1Char('"') && !inString) intoString = true; if(inString || intoString) { if(inString) { if(str[pos] == QLatin1Char('"')) inString = false; }else{ inString = true; } bool skip = false; if (str[pos] == QLatin1Char('\\')) skip = true; str[pos] = replacement; if(skip) { ++pos; if(pos < str.length()) str[pos] = replacement; } } } return str; } int strip(const QByteArray& str, QByteArray& from) { return strip_impl(str, from); } int rStrip(const QByteArray& str, QByteArray& from) { return rStrip_impl(str, from); } QByteArray formatComment(const QByteArray& comment) { return formatComment_impl(comment); } QString formatComment(const QString& comment) { return formatComment_impl(comment); } ParamIterator::~ParamIterator() = default; ParamIterator::ParamIterator( QString parens, QString source, int offset ) : d(new ParamIteratorPrivate) { d->m_source = source; d->m_parens = parens; d->m_cur = offset; d->m_curEnd = offset; d->m_end = d->m_source.length(); ///The whole search should be stopped when: A) The end-sign is found on the top-level B) A closing-brace of parameters was found int parenBegin = d->m_source.indexOf( parens[ 0 ], offset ); //Search for an interrupting end-sign that comes before the found paren-begin int foundEnd = -1; if( parens.length() > 2 ) { foundEnd = d->m_source.indexOf( parens[2], offset ); if( foundEnd > parenBegin && parenBegin != -1 ) foundEnd = -1; } if( foundEnd != -1 ) { //We have to stop the search, because we found an interrupting end-sign before the opening-paren d->m_prefix = d->m_source.mid( offset, foundEnd - offset ); d->m_curEnd = d->m_end = d->m_cur = foundEnd; } else { if( parenBegin != -1 ) { //We have a valid prefix before an opening-paren. Take the prefix, and start iterating parameters. d->m_prefix = d->m_source.mid( offset, parenBegin - offset ); d->m_cur = parenBegin + 1; d->m_curEnd = d->next(); if( d->m_curEnd == d->m_source.length() ) { //The paren was not closed. It might be an identifier like "operator<", so count everything as prefix. d->m_prefix = d->m_source.mid(offset); d->m_curEnd = d->m_end = d->m_cur = d->m_source.length(); } } else { //We have neither found an ending-character, nor an opening-paren, so take the whole input and end d->m_prefix = d->m_source.mid(offset); d->m_curEnd = d->m_end = d->m_cur = d->m_source.length(); } } } ParamIterator& ParamIterator::operator ++() { if( d->m_source[d->m_curEnd] == d->m_parens[1] ) { //We have reached the end-paren. Stop iterating. d->m_cur = d->m_end = d->m_curEnd + 1; } else { //Iterate on through parameters d->m_cur = d->m_curEnd + 1; if ( d->m_cur < ( int ) d->m_source.length() ) { d->m_curEnd = d->next(); } } return *this; } QString ParamIterator::operator *() { return d->m_source.mid( d->m_cur, d->m_curEnd - d->m_cur ).trimmed(); } ParamIterator::operator bool() const { return d->m_cur < ( int ) d->m_end; } QString ParamIterator::prefix() const { return d->m_prefix; } uint ParamIterator::position() const { return (uint)d->m_cur; } } diff --git a/kdevplatform/language/duchain/topducontextdynamicdata.cpp b/kdevplatform/language/duchain/topducontextdynamicdata.cpp index 5a26bfd3b0..698f4b2220 100644 --- a/kdevplatform/language/duchain/topducontextdynamicdata.cpp +++ b/kdevplatform/language/duchain/topducontextdynamicdata.cpp @@ -1,853 +1,854 @@ /* This is part of KDevelop Copyright 2014 Milian Wolff 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 "topducontextdynamicdata.h" #include #include #include #include "declaration.h" #include "declarationdata.h" #include "ducontext.h" #include "topducontext.h" #include "topducontextdata.h" #include "ducontextdata.h" #include "ducontextdynamicdata.h" #include "duchainregister.h" #include "serialization/itemrepository.h" #include "problem.h" #include //#define DEBUG_DATA_INFO //This might be problematic on some systems, because really many mmaps are created #define USE_MMAP using namespace KDevelop; namespace { /** * Serialize @p item into @p data and update @p totalDataOffset. * * If @p isSharedDataItem is true, then the item's internal data pointer is not updated * to point to the serialized data. Otherwise the dynamic data is deleted and the items * data will point to the constant serialized data. * * NOTE: The above is required to support serialization of shared-data such as from ProblemPointer. * If we'd set the data to point to the constant region, we'd get crashes due to use-after-free when * we unmap the data and a shared pointer outlives that. */ void saveDUChainItem(QVector& data, DUChainBase& item, uint& totalDataOffset, bool isSharedDataItem) { if(!item.d_func()->classId) { //If this triggers, you have probably created an own DUChainBase based class, but haven't called setClassId(this) in the constructor. qCritical() << "no class-id set for data attached to a declaration of type" << typeid(item).name(); Q_ASSERT(0); } int size = DUChainItemSystem::self().dynamicSize(*item.d_func()); if(data.back().array.size() - int(data.back().position) < size) //Create a new data item data.append({QByteArray(size > 10000 ? size : 10000, 0), 0u}); uint pos = data.back().position; data.back().position += size; totalDataOffset += size; DUChainBaseData& target(*(reinterpret_cast(data.back().array.data() + pos))); if(item.d_func()->isDynamic()) { //Change from dynamic data to constant data enableDUChainReferenceCounting(data.back().array.data(), data.back().array.size()); DUChainItemSystem::self().copy(*item.d_func(), target, true); Q_ASSERT(!target.isDynamic()); if (!isSharedDataItem) { item.setData(&target); } disableDUChainReferenceCounting(data.back().array.data()); }else{ //Just copy the data into another place, expensive copy constructors are not needed #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wclass-memaccess" #endif memcpy(&target, item.d_func(), size); #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic pop #endif if (!isSharedDataItem) { item.setData(&target, false); } } if (!isSharedDataItem) { Q_ASSERT(item.d_func() == &target); Q_ASSERT(!item.d_func()->isDynamic()); } } uint indexForParentContext(DUContext* context) { return LocalIndexedDUContext(context->parentContext()).localIndex(); } uint indexForParentContext(Declaration* declaration) { return LocalIndexedDUContext(declaration->context()).localIndex(); } uint indexForParentContext(const ProblemPointer& /*problem*/) { // always stored in the top context return 0; } #ifndef QT_NO_DEBUG void validateItem(const DUChainBaseData* const data, const uchar* const mappedData, const size_t mappedDataSize) { Q_ASSERT(!data->isDynamic()); if (mappedData) { Q_ASSERT(((size_t)data) < ((size_t)mappedData) || ((size_t)data) > ((size_t)mappedData) + mappedDataSize); } } #endif const char* pointerInData(const QVector& data, uint totalOffset) { for(int a = 0; a < data.size(); ++a) { if(totalOffset < data[a].position) return data[a].array.constData() + totalOffset; totalOffset -= data[a].position; } Q_ASSERT_X(false, Q_FUNC_INFO, "Offset doesn't exist in the data."); return nullptr; } void verifyDataInfo(const TopDUContextDynamicData::ItemDataInfo& info, const QVector& data) { Q_UNUSED(info); Q_UNUSED(data); #ifdef DEBUG_DATA_INFO DUChainBaseData* item = (DUChainBaseData*)(pointerInData(data, info.dataOffset)); int size = DUChainItemSystem::self().dynamicSize(*item); Q_ASSERT(size); #endif } QString basePath() { return globalItemRepositoryRegistry().path() + QLatin1String("/topcontexts/"); } QString pathForTopContext(const uint topContextIndex) { return basePath() + QString::number(topContextIndex); } enum LoadType { PartialLoad, ///< Only load the direct member data FullLoad ///< Load everything, including appended lists }; template void loadTopDUContextData(const uint topContextIndex, LoadType loadType, F callback) { QFile file(pathForTopContext(topContextIndex)); if (!file.open(QIODevice::ReadOnly)) { return; } uint readValue; file.read((char*)&readValue, sizeof(uint)); // now readValue is filled with the top-context data size Q_ASSERT(readValue >= sizeof(TopDUContextData)); const QByteArray data = file.read(loadType == FullLoad ? readValue : sizeof(TopDUContextData)); const TopDUContextData* topData = reinterpret_cast(data.constData()); callback(topData); } template struct PtrType; template struct PtrType { using value = T*; }; template struct PtrType> { using value = T*; }; template Q_DECL_CONSTEXPR bool isSharedDataItem() { return false; } template<> Q_DECL_CONSTEXPR bool isSharedDataItem() { return true; } } //BEGIN DUChainItemStorage template TopDUContextDynamicData::DUChainItemStorage::DUChainItemStorage(TopDUContextDynamicData* data) : data(data) { } template TopDUContextDynamicData::DUChainItemStorage::~DUChainItemStorage() { clearItems(); } template void TopDUContextDynamicData::DUChainItemStorage::clearItems() { //Due to template specialization it's possible that a declaration is not reachable through the normal context structure. //For that reason we have to check here, and delete all remaining declarations. qDeleteAll(temporaryItems); temporaryItems.clear(); qDeleteAll(items); items.clear(); } namespace KDevelop { template<> void TopDUContextDynamicData::DUChainItemStorage::clearItems() { // don't delete anything - the problem is shared items.clear(); } } template void TopDUContextDynamicData::DUChainItemStorage::clearItemIndex(const Item& item, const uint index) { if(!data->m_dataLoaded) data->loadData(); if (index < (0x0fffffff/2)) { if (index == 0 || index > uint(items.size())) { return; } else { const uint realIndex = index - 1; Q_ASSERT(items[realIndex] == item); items[realIndex] = nullptr; if (realIndex < (uint)offsets.size()) { offsets[realIndex] = ItemDataInfo(); } } } else { const uint realIndex = 0x0fffffff - index; //We always keep the highest bit at zero if (realIndex == 0 || realIndex > uint(temporaryItems.size())) { return; } else { Q_ASSERT(temporaryItems[realIndex-1] == item); temporaryItems[realIndex-1] = nullptr; } } Q_UNUSED(item); } template void TopDUContextDynamicData::DUChainItemStorage::storeData(uint& currentDataOffset, const QVector& oldData) { auto const oldOffsets = offsets; offsets.clear(); + offsets.reserve(items.size()); for (int a = 0; a < items.size(); ++a) { auto item = items[a]; if (!item) { if (oldOffsets.size() > a && oldOffsets[a].dataOffset) { //Directly copy the old data range into the new data const DUChainBaseData* itemData = nullptr; if (data->m_mappedData) { itemData = reinterpret_cast(data->m_mappedData + oldOffsets[a].dataOffset); } else { itemData = reinterpret_cast(::pointerInData(oldData, oldOffsets[a].dataOffset)); } offsets << data->writeDataInfo(oldOffsets[a], itemData, currentDataOffset); } else { offsets << ItemDataInfo(); } } else { offsets << ItemDataInfo{currentDataOffset, indexForParentContext(item)}; saveDUChainItem(data->m_data, *item, currentDataOffset, isSharedDataItem()); } } #ifndef QT_NO_DEBUG if (!isSharedDataItem()) { for (auto item : items) { if (item) { validateItem(item->d_func(), data->m_mappedData, data->m_mappedDataSize); } } } #endif } template bool TopDUContextDynamicData::DUChainItemStorage::itemsHaveChanged() const { for (auto item : items) { if (item && item->d_func()->m_dynamic) { return true; } } return false; } template uint TopDUContextDynamicData::DUChainItemStorage::allocateItemIndex(const Item& item, const bool temporary) { if (!data->m_dataLoaded) { data->loadData(); } if (!temporary) { items.append(item); return items.size(); } else { temporaryItems.append(item); return 0x0fffffff - temporaryItems.size(); //We always keep the highest bit at zero } } template bool TopDUContextDynamicData::DUChainItemStorage::isItemForIndexLoaded(uint index) const { if (!data->m_dataLoaded) { return false; } if (index < (0x0fffffff/2)) { if (index == 0 || index > uint(items.size())) { return false; } return items[index-1]; } else { // temporary item return true; } } template Item TopDUContextDynamicData::DUChainItemStorage::getItemForIndex(uint index) const { if (index >= (0x0fffffff/2)) { index = 0x0fffffff - index; //We always keep the highest bit at zero if(index == 0 || index > uint(temporaryItems.size())) return {}; else return temporaryItems.at(index-1); } if (index == 0 || index > static_cast(items.size())) { qCWarning(LANGUAGE) << "item index out of bounds:" << index << "count:" << items.size(); return {}; } - const uint realIndex = index - 1;; + const uint realIndex = index - 1; const auto& item = items.at(realIndex); if (item) { //Shortcut, because this is the most common case return item; } if (realIndex < (uint)offsets.size() && offsets[realIndex].dataOffset) { Q_ASSERT(!data->m_itemRetrievalForbidden); //Construct the context, and eventuall its parent first ///TODO: ugly, remove need for const_cast auto itemData = const_cast( reinterpret_cast(data->pointerInData(offsets[realIndex].dataOffset)) ); auto& item = items[realIndex]; item = dynamic_cast::value>(DUChainItemSystem::self().create(itemData)); if (!item) { //When this happens, the item has not been registered correctly. //We can stop here, because else we will get crashes later. qCritical() << "Failed to load item with identity" << itemData->classId; return {}; } if (isSharedDataItem()) { // NOTE: shared data must never point to mmapped data regions as otherwise we might end up with // use-after-free or double-deletions etc. pp. // thus, make the item always dynamic after deserialization item->makeDynamic(); } auto parent = data->getContextForIndex(offsets[realIndex].parentContext); Q_ASSERT_X(parent, Q_FUNC_INFO, "Could not find parent context for loaded item.\n" "Potentially, the context has been deleted without deleting its children."); item->rebuildDynamicData(parent, index); } else { qCWarning(LANGUAGE) << "invalid item for index" << index << offsets.size() << offsets.value(realIndex).dataOffset; } return item; } template void TopDUContextDynamicData::DUChainItemStorage::deleteOnDisk() { for (auto& item : items) { if (item) { item->makeDynamic(); } } } template void TopDUContextDynamicData::DUChainItemStorage::loadData(QFile* file) const { Q_ASSERT(offsets.isEmpty()); Q_ASSERT(items.isEmpty()); uint readValue; file->read((char*)&readValue, sizeof(uint)); offsets.resize(readValue); file->read((char*)offsets.data(), sizeof(ItemDataInfo) * offsets.size()); //Fill with zeroes for now, will be initialized on-demand items.resize(offsets.size()); } template void TopDUContextDynamicData::DUChainItemStorage::writeData(QFile* file) { uint writeValue = offsets.size(); file->write((char*)&writeValue, sizeof(uint)); file->write((char*)offsets.data(), sizeof(ItemDataInfo) * offsets.size()); } //END DUChainItemStorage const char* TopDUContextDynamicData::pointerInData(uint totalOffset) const { Q_ASSERT(!m_mappedData || m_data.isEmpty()); if(m_mappedData && m_mappedDataSize) return (char*)m_mappedData + totalOffset; return ::pointerInData(m_data, totalOffset); } TopDUContextDynamicData::TopDUContextDynamicData(TopDUContext* topContext) : m_deleting(false) , m_topContext(topContext) , m_contexts(this) , m_declarations(this) , m_problems(this) , m_onDisk(false) , m_dataLoaded(true) , m_mappedFile(nullptr) , m_mappedData(nullptr) , m_mappedDataSize(0) , m_itemRetrievalForbidden(false) { } void KDevelop::TopDUContextDynamicData::clear() { m_contexts.clearItems(); m_declarations.clearItems(); m_problems.clearItems(); } TopDUContextDynamicData::~TopDUContextDynamicData() { unmap(); } void KDevelop::TopDUContextDynamicData::unmap() { delete m_mappedFile; m_mappedFile = nullptr; m_mappedData = nullptr; m_mappedDataSize = 0; } bool TopDUContextDynamicData::fileExists(uint topContextIndex) { return QFile::exists(pathForTopContext(topContextIndex)); } QList TopDUContextDynamicData::loadImporters(uint topContextIndex) { QList ret; loadTopDUContextData(topContextIndex, FullLoad, [&ret] (const TopDUContextData* topData) { ret.reserve(topData->m_importersSize()); FOREACH_FUNCTION(const IndexedDUContext& importer, topData->m_importers) ret << importer; }); return ret; } QList TopDUContextDynamicData::loadImports(uint topContextIndex) { QList ret; loadTopDUContextData(topContextIndex, FullLoad, [&ret] (const TopDUContextData* topData) { ret.reserve(topData->m_importedContextsSize()); FOREACH_FUNCTION(const DUContext::Import& import, topData->m_importedContexts) ret << import.indexedContext(); }); return ret; } IndexedString TopDUContextDynamicData::loadUrl(uint topContextIndex) { IndexedString url; loadTopDUContextData(topContextIndex, PartialLoad, [&url] (const TopDUContextData* topData) { Q_ASSERT(topData->m_url.isEmpty() || topData->m_url.index() >> 16); url = topData->m_url; }); return url; } void TopDUContextDynamicData::loadData() const { //This function has to be protected by an additional mutex, since it can be triggered from multiple threads at the same time static QMutex mutex; QMutexLocker lock(&mutex); if(m_dataLoaded) return; Q_ASSERT(!m_dataLoaded); Q_ASSERT(m_data.isEmpty()); QFile* file = new QFile(pathForTopContext(m_topContext->ownIndex())); bool open = file->open(QIODevice::ReadOnly); Q_UNUSED(open); Q_ASSERT(open); Q_ASSERT(file->size()); //Skip the offsets, we're already read them //Skip top-context data uint readValue; file->read((char*)&readValue, sizeof(uint)); file->seek(readValue + file->pos()); m_contexts.loadData(file); m_declarations.loadData(file); m_problems.loadData(file); #ifdef USE_MMAP m_mappedData = file->map(file->pos(), file->size() - file->pos()); if(m_mappedData) { m_mappedFile = file; m_mappedDataSize = file->size() - file->pos(); file->close(); //Close the file, so there is less open file descriptors(May be problematic) }else{ qCDebug(LANGUAGE) << "Failed to map" << file->fileName(); } #endif if(!m_mappedFile) { QByteArray data = file->readAll(); m_data.append({data, (uint)data.size()}); delete file; } m_dataLoaded = true; } TopDUContext* TopDUContextDynamicData::load(uint topContextIndex) { QFile file(pathForTopContext(topContextIndex)); if(file.open(QIODevice::ReadOnly)) { if(file.size() == 0) { qCWarning(LANGUAGE) << "Top-context file is empty" << file.fileName(); return nullptr; } uint readValue; file.read((char*)&readValue, sizeof(uint)); //now readValue is filled with the top-context data size QByteArray topContextData = file.read(readValue); DUChainBaseData* topData = reinterpret_cast(topContextData.data()); TopDUContext* ret = dynamic_cast(DUChainItemSystem::self().create(topData)); if(!ret) { qCWarning(LANGUAGE) << "Cannot load a top-context from file" << file.fileName() << "- the required language-support for handling ID" << topData->classId << "is probably not loaded"; return nullptr; } TopDUContextDynamicData& target(*ret->m_dynamicData); target.m_data.clear(); target.m_dataLoaded = false; target.m_onDisk = true; ret->rebuildDynamicData(nullptr, topContextIndex); target.m_topContextData.append({topContextData, (uint)0}); return ret; }else{ return nullptr; } } bool TopDUContextDynamicData::isOnDisk() const { return m_onDisk; } void TopDUContextDynamicData::deleteOnDisk() { if(!isOnDisk()) return; qCDebug(LANGUAGE) << "deleting" << m_topContext->ownIndex() << m_topContext->url().str(); if(!m_dataLoaded) loadData(); m_contexts.deleteOnDisk(); m_declarations.deleteOnDisk(); m_problems.deleteOnDisk(); m_topContext->makeDynamic(); m_onDisk = false; bool successfullyRemoved = QFile::remove(filePath()); Q_UNUSED(successfullyRemoved); Q_ASSERT(successfullyRemoved); qCDebug(LANGUAGE) << "deletion ready"; } QString KDevelop::TopDUContextDynamicData::filePath() const { return pathForTopContext(m_topContext->ownIndex()); } bool TopDUContextDynamicData::hasChanged() const { return !m_onDisk || m_topContext->d_func()->m_dynamic || m_contexts.itemsHaveChanged() || m_declarations.itemsHaveChanged() || m_problems.itemsHaveChanged(); } void TopDUContextDynamicData::store() { // qCDebug(LANGUAGE) << "storing" << m_topContext->url().str() << m_topContext->ownIndex() << "import-count:" << m_topContext->importedParentContexts().size(); //Check if something has changed. If nothing has changed, don't store to disk. bool contentDataChanged = hasChanged(); if (!contentDataChanged) { return; } ///@todo Save the meta-data into a repository, and only the actual content data into a file. /// This will make saving+loading more efficient, and will reduce the disk-usage. /// Then we also won't need to load the data if only the meta-data changed. if(!m_dataLoaded) loadData(); ///If the data is mapped, and we re-write the file, we must make sure that the data is copied out of the map, ///even if only metadata is changed. ///@todo If we split up data and metadata, we don't need to do this if(m_mappedData) contentDataChanged = true; m_topContext->makeDynamic(); m_topContextData.clear(); Q_ASSERT(m_topContext->d_func()->m_ownIndex == m_topContext->ownIndex()); uint topContextDataSize = DUChainItemSystem::self().dynamicSize(*m_topContext->d_func()); m_topContextData.append({QByteArray(DUChainItemSystem::self().dynamicSize(*m_topContext->d_func()), topContextDataSize), 0u}); uint actualTopContextDataSize = 0; if (contentDataChanged) { //We don't need these structures any more, since we have loaded all the declarations/contexts, and m_data //will be reset which these structures pointed into //Load all lazy declarations/contexts const auto oldData = m_data; //Keep the old data alive until everything is stored into a new data structure m_data.clear(); uint newDataSize = 0; foreach(const ArrayWithPosition &array, oldData) newDataSize += array.position; newDataSize = std::max(newDataSize, 10000u); //We always put 1 byte to the front, so we don't have zero data-offsets, since those are used for "invalid". uint currentDataOffset = 1; m_data.append({QByteArray(newDataSize, 0), currentDataOffset}); m_itemRetrievalForbidden = true; m_contexts.storeData(currentDataOffset, oldData); m_declarations.storeData(currentDataOffset, oldData); m_problems.storeData(currentDataOffset, oldData); m_itemRetrievalForbidden = false; } saveDUChainItem(m_topContextData, *m_topContext, actualTopContextDataSize, false); Q_ASSERT(actualTopContextDataSize == topContextDataSize); Q_ASSERT(m_topContextData.size() == 1); Q_ASSERT(!m_topContext->d_func()->isDynamic()); unmap(); QDir().mkpath(basePath()); QFile file(filePath()); if(file.open(QIODevice::WriteOnly)) { file.resize(0); file.write((char*)&topContextDataSize, sizeof(uint)); foreach(const ArrayWithPosition& pos, m_topContextData) file.write(pos.array.constData(), pos.position); m_contexts.writeData(&file); m_declarations.writeData(&file); m_problems.writeData(&file); foreach(const ArrayWithPosition& pos, m_data) file.write(pos.array.constData(), pos.position); m_onDisk = true; if (file.size() == 0) { qCWarning(LANGUAGE) << "Saving zero size top ducontext data"; } file.close(); } else { qCWarning(LANGUAGE) << "Cannot open top-context for writing"; } // qCDebug(LANGUAGE) << "stored" << m_topContext->url().str() << m_topContext->ownIndex() << "import-count:" << m_topContext->importedParentContexts().size(); } TopDUContextDynamicData::ItemDataInfo TopDUContextDynamicData::writeDataInfo(const ItemDataInfo& info, const DUChainBaseData* data, uint& totalDataOffset) { ItemDataInfo ret(info); Q_ASSERT(info.dataOffset); const auto size = DUChainItemSystem::self().dynamicSize(*data); Q_ASSERT(size); if(m_data.back().array.size() - m_data.back().position < size) { //Create a new m_data item m_data.append({QByteArray(std::max(size, 10000u), 0), 0u}); } ret.dataOffset = totalDataOffset; uint pos = m_data.back().position; m_data.back().position += size; totalDataOffset += size; auto target = reinterpret_cast(m_data.back().array.data() + pos); #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wclass-memaccess" #endif memcpy(target, data, size); #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic pop #endif verifyDataInfo(ret, m_data); return ret; } uint TopDUContextDynamicData::allocateDeclarationIndex(Declaration* decl, bool temporary) { return m_declarations.allocateItemIndex(decl, temporary); } uint TopDUContextDynamicData::allocateContextIndex(DUContext* context, bool temporary) { return m_contexts.allocateItemIndex(context, temporary); } uint TopDUContextDynamicData::allocateProblemIndex(const ProblemPointer& problem) { return m_problems.allocateItemIndex(problem, false); } bool TopDUContextDynamicData::isDeclarationForIndexLoaded(uint index) const { return m_declarations.isItemForIndexLoaded(index); } bool TopDUContextDynamicData::isContextForIndexLoaded(uint index) const { return m_contexts.isItemForIndexLoaded(index); } bool TopDUContextDynamicData::isTemporaryContextIndex(uint index) const { return !(index < (0x0fffffff/2)); } bool TopDUContextDynamicData::isTemporaryDeclarationIndex(uint index) const { return !(index < (0x0fffffff/2)); } DUContext* TopDUContextDynamicData::getContextForIndex(uint index) const { if(!m_dataLoaded) loadData(); if (index == 0) { return m_topContext; } return m_contexts.getItemForIndex(index); } Declaration* TopDUContextDynamicData::getDeclarationForIndex(uint index) const { if(!m_dataLoaded) loadData(); return m_declarations.getItemForIndex(index); } ProblemPointer TopDUContextDynamicData::getProblemForIndex(uint index) const { if(!m_dataLoaded) loadData(); return m_problems.getItemForIndex(index); } void TopDUContextDynamicData::clearDeclarationIndex(Declaration* decl) { m_declarations.clearItemIndex(decl, decl->m_indexInTopContext); } void TopDUContextDynamicData::clearContextIndex(DUContext* context) { m_contexts.clearItemIndex(context, context->m_dynamicData->m_indexInTopContext); } void TopDUContextDynamicData::clearProblems() { m_problems.clearItems(); } diff --git a/kdevplatform/language/duchain/types/functiontype.cpp b/kdevplatform/language/duchain/types/functiontype.cpp index 06c34803cd..63765ec92e 100644 --- a/kdevplatform/language/duchain/types/functiontype.cpp +++ b/kdevplatform/language/duchain/types/functiontype.cpp @@ -1,197 +1,194 @@ /* This file is part of KDevelop Copyright 2006 Roberto Raggi Copyright 2006-2008 Hamish Rodda Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "functiontype.h" #include "typerepository.h" #include "typesystemdata.h" #include "typeregister.h" #include "typesystem.h" namespace KDevelop { REGISTER_TYPE(FunctionType); DEFINE_LIST_MEMBER_HASH(FunctionTypeData, m_arguments, IndexedType) FunctionType::FunctionType(const FunctionType& rhs) : AbstractType(copyData(*rhs.d_func())) { } FunctionType::FunctionType(FunctionTypeData& data) : AbstractType(data) { } AbstractType* FunctionType::clone() const { return new FunctionType(*this); } bool FunctionType::equals(const AbstractType* _rhs) const { if( this == _rhs ) return true; if (!AbstractType::equals(_rhs)) return false; Q_ASSERT(fastCast(_rhs)); const FunctionType* rhs = static_cast(_rhs); TYPE_D(FunctionType); if( d->m_argumentsSize() != rhs->d_func()->m_argumentsSize() ) return false; if( (bool)rhs->d_func()->m_returnType != (bool)d->m_returnType ) return false; if( d->m_returnType != rhs->d_func()->m_returnType ) return false; for(unsigned int a = 0; a < d->m_argumentsSize(); ++a) if(d->m_arguments()[a] != rhs->d_func()->m_arguments()[a]) return false; return true; } FunctionType::FunctionType() : AbstractType(createData()) { } FunctionType::~FunctionType() { } void FunctionType::addArgument(const AbstractType::Ptr& argument, int index) { if ( index == -1 ) d_func_dynamic()->m_argumentsList().append(IndexedType(argument)); else d_func_dynamic()->m_argumentsList().insert(index, IndexedType(argument)); } void FunctionType::removeArgument(int i) { d_func_dynamic()->m_argumentsList().remove(i); } void FunctionType::setReturnType(const AbstractType::Ptr& returnType) { d_func_dynamic()->m_returnType = IndexedType(returnType); } AbstractType::Ptr FunctionType::returnType () const { return d_func()->m_returnType.abstractType(); } QList FunctionType::arguments () const { ///@todo Don't do the conversion QList ret; + ret.reserve(d_func()->m_argumentsSize()); FOREACH_FUNCTION(const IndexedType& arg, d_func()->m_arguments) ret << arg.abstractType(); return ret; } const IndexedType* FunctionType::indexedArguments() const { return d_func()->m_arguments(); } uint FunctionType::indexedArgumentsSize() const { return d_func()->m_argumentsSize(); } void FunctionType::accept0 (TypeVisitor *v) const { TYPE_D(FunctionType); if (v->visit (this)) { acceptType (d->m_returnType.abstractType(), v); for (unsigned int i = 0; i < d->m_argumentsSize (); ++i) acceptType (d->m_arguments()[i].abstractType(), v); } v->endVisit (this); } void FunctionType::exchangeTypes( TypeExchanger* exchanger ) { TYPE_D_DYNAMIC(FunctionType); for (uint i = 0; i < d->m_argumentsSize (); ++i) d->m_argumentsList()[i] = IndexedType(exchanger->exchange(d->m_arguments()[i].abstractType())); d->m_returnType = IndexedType(exchanger->exchange(d->m_returnType.abstractType())); } QString FunctionType::partToString( SignaturePart sigPart ) const { QString args; TYPE_D(FunctionType); if( sigPart == SignatureArguments || sigPart == SignatureWhole ) { - args += QLatin1Char('('); - bool first = true; + QStringList types; + types.reserve(d->m_argumentsSize()); FOREACH_FUNCTION(const IndexedType& type, d->m_arguments) { - if (first) - first = false; - else - args.append(QLatin1String(", ")); - args.append(type ? type.abstractType()->toString() : QStringLiteral("")); + types.append(type ? type.abstractType()->toString() : QStringLiteral("")); } - args += QLatin1Char(')'); + args += QLatin1Char('(') + types.join(QLatin1String(", ")) + QLatin1Char(')'); } if( sigPart == SignatureArguments ) return args; else if( sigPart == SignatureWhole ) return QStringLiteral("function %1 %2").arg(returnType() ? returnType()->toString() : QStringLiteral(""), args); else if( sigPart == SignatureReturn ) return returnType() ? returnType()->toString() : QString(); return QStringLiteral("ERROR"); } QString FunctionType::toString() const { return partToString(SignatureWhole) + AbstractType::toString(true); } AbstractType::WhichType FunctionType::whichType() const { return TypeFunction; } uint FunctionType::hash() const { KDevHash kdevhash(AbstractType::hash()); kdevhash << d_func()->m_returnType.hash(); FOREACH_FUNCTION(const IndexedType& t, d_func()->m_arguments) { kdevhash << t.hash(); } return kdevhash; } } diff --git a/kdevplatform/language/duchain/types/unsuretype.cpp b/kdevplatform/language/duchain/types/unsuretype.cpp index 8afbdf2862..04258acc65 100644 --- a/kdevplatform/language/duchain/types/unsuretype.cpp +++ b/kdevplatform/language/duchain/types/unsuretype.cpp @@ -1,123 +1,119 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "unsuretype.h" #include "typeregister.h" #include "typesystem.h" namespace KDevelop { REGISTER_TYPE(UnsureType); DEFINE_LIST_MEMBER_HASH(UnsureTypeData, m_types, IndexedType) UnsureType::UnsureType(const KDevelop::UnsureType& rhs): AbstractType(copyData(*rhs.d_func())) { } UnsureType::UnsureType() : AbstractType(createData()) { } void UnsureType::accept0(KDevelop::TypeVisitor* v) const { FOREACH_FUNCTION(const IndexedType& type, d_func()->m_types) { AbstractType::Ptr t = type.abstractType(); v->visit(t.data()); } } KDevelop::AbstractType* UnsureType::clone() const { return new UnsureType(*this); } QString UnsureType::toString() const { - QString ret = QStringLiteral("unsure ("); - bool first = true; + QStringList typeNames; + typeNames.reserve(d_func()->m_typesSize()); FOREACH_FUNCTION(const IndexedType& type, d_func()->m_types) { - if(!first) - ret += QLatin1String(", "); - first = false; - AbstractType::Ptr t = type.abstractType(); if(t) - ret += t->toString(); + typeNames.append(t->toString()); else - ret += QLatin1String("none"); + typeNames.append(QLatin1String("none")); } - ret += QLatin1Char(')'); + QString ret = QLatin1String("unsure (") + typeNames.join(QLatin1String(", ")) + QLatin1Char(')'); return ret; } bool UnsureType::equals(const KDevelop::AbstractType* rhs) const { const UnsureType* rhsU = dynamic_cast(rhs); if(!rhsU) return false; if(d_func()->typeClassId != rhsU->d_func()->typeClassId) return false; if(d_func()->m_typesSize() != rhsU->d_func()->m_typesSize()) return false; for(uint a = 0; a < d_func()->m_typesSize(); ++a) if(d_func()->m_types()[a] != rhsU->d_func()->m_types()[a]) return false; return KDevelop::AbstractType::equals(rhs); } uint UnsureType::hash() const { KDevHash kdevhash(AbstractType::hash()); FOREACH_FUNCTION(const IndexedType& type, d_func()->m_types) kdevhash << type.hash(); return kdevhash << d_func()->m_typesSize(); } KDevelop::AbstractType::WhichType UnsureType::whichType() const { return TypeUnsure; } void UnsureType::exchangeTypes(KDevelop::TypeExchanger* exchanger) { for(uint a = 0; a < d_func()->m_typesSize(); ++a) { AbstractType::Ptr from = d_func()->m_types()[a].abstractType(); AbstractType::Ptr exchanged = exchanger->exchange(from); if(exchanged != from) d_func_dynamic()->m_typesList()[a] = exchanged->indexed(); } KDevelop::AbstractType::exchangeTypes(exchanger); } void UnsureType::addType(KDevelop::IndexedType type) { if ( !d_func_dynamic()->m_typesList().contains(type) ) { d_func_dynamic()->m_typesList().append(type); } } void UnsureType::removeType(KDevelop::IndexedType type) { d_func_dynamic()->m_typesList().removeOne(type); } const KDevelop::IndexedType* UnsureType::types() const { return d_func()->m_types(); } uint UnsureType::typesSize() const { return d_func()->m_typesSize(); } UnsureType::UnsureType(KDevelop::UnsureTypeData& data): AbstractType(data) { } } diff --git a/kdevplatform/language/editor/modificationrevisionset.cpp b/kdevplatform/language/editor/modificationrevisionset.cpp index 4195f87426..49454bb5d8 100644 --- a/kdevplatform/language/editor/modificationrevisionset.cpp +++ b/kdevplatform/language/editor/modificationrevisionset.cpp @@ -1,329 +1,324 @@ /* 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 "modificationrevisionset.h" #include #include #include #include //When uncommented, the reason for needed updates is printed // #define DEBUG_NEEDSUPDATE namespace KDevelop { QMutex modificationRevisionSetMutex(QMutex::Recursive); struct FileModificationPair { KDevelop::IndexedString file; KDevelop::ModificationRevision revision; FileModificationPair() { } FileModificationPair(KDevelop::IndexedString _file, KDevelop::ModificationRevision _revision) : file(_file), revision(_revision) { } unsigned int hash() const { return ((file.hash() + revision.modificationTime) * 17 + revision.revision) * 73; } unsigned short int itemSize() const { return sizeof(FileModificationPair); } bool operator==(const FileModificationPair& rhs) const { return file == rhs.file && revision == rhs.revision; } }; struct FileModificationPairRequest { FileModificationPairRequest(const FileModificationPair& data) : m_data(data) { } const FileModificationPair& m_data; enum { AverageSize = sizeof(FileModificationPair) }; unsigned int hash() const { return m_data.hash(); } uint itemSize() const { return m_data.itemSize(); } void createItem(FileModificationPair* item) const { new (item) FileModificationPair(m_data); } bool equals(const FileModificationPair* item) const { return *item == m_data; } static void destroy(FileModificationPair* item, KDevelop::AbstractItemRepository&) { item->~FileModificationPair(); } static bool persistent(const FileModificationPair* /*item*/) { return true; //Reference-counting is done implicitly using the set-repository } }; typedef KDevelop::ItemRepository FileModificationPairRepository; static FileModificationPairRepository& fileModificationPairRepository() { static FileModificationPairRepository rep(QStringLiteral("file modification repository")); rep.setMutex(&modificationRevisionSetMutex); return rep; } void initModificationRevisionSetRepository() { fileModificationPairRepository(); } QHash > needsUpdateCache; void ModificationRevisionSet::clearCache() { QMutexLocker lock(&modificationRevisionSetMutex); ///@todo More intelligent clearing. We actually need to watch the directory for changes, and if there are changes, clear the cache. needsUpdateCache.clear(); } struct FileModificationSetRepository : public Utils::BasicSetRepository { FileModificationSetRepository() : Utils::BasicSetRepository(QStringLiteral("file modification sets"), &globalItemRepositoryRegistry(), true) { } void itemRemovedFromSets(uint index) override; }; //FileModificationSetRepository fileModificationSetRepository; struct FileModificationSetRepositoryRepresenter { static FileModificationSetRepository& repository() { static FileModificationSetRepository fileModificationSetRepository; return fileModificationSetRepository; } }; ModificationRevisionSet::ModificationRevisionSet(unsigned int index) : m_index(index) { } uint ModificationRevisionSet::size() const { Utils::Set set = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); return set.count(); } void ModificationRevisionSet::clear() { QMutexLocker lock(&modificationRevisionSetMutex); if(m_index) { Utils::Set oldModificationTimes = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); oldModificationTimes.staticUnref(); m_index = 0; } } void ModificationRevisionSet::addModificationRevision(const IndexedString& url, const KDevelop::ModificationRevision& revision) { QMutexLocker lock(&modificationRevisionSetMutex); if(m_index == 0) { Utils::Set set = FileModificationSetRepositoryRepresenter::repository().createSet(fileModificationPairRepository().index(FileModificationPair(url, revision))); set.staticRef(); m_index = set.setIndex(); }else{ Utils::Set oldModificationTimes = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set newModificationTimes = oldModificationTimes; Utils::Set tempSet = FileModificationSetRepositoryRepresenter::repository().createSet(fileModificationPairRepository().index(FileModificationPair(url, revision))); tempSet.staticRef(); newModificationTimes += tempSet; newModificationTimes.staticRef(); oldModificationTimes.staticUnref(); tempSet.staticUnref(); m_index = newModificationTimes.setIndex(); } } bool ModificationRevisionSet::removeModificationRevision(const IndexedString& url, const KDevelop::ModificationRevision& revision) { QMutexLocker lock(&modificationRevisionSetMutex); if(!m_index) return false; Utils::Set oldModificationTimes = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set newModificationTimes = oldModificationTimes; Utils::Set tempSet = FileModificationSetRepositoryRepresenter::repository().createSet(fileModificationPairRepository().index(FileModificationPair(url, revision))); tempSet.staticRef(); newModificationTimes -= tempSet; newModificationTimes.staticRef(); oldModificationTimes.staticUnref(); tempSet.staticUnref(); m_index = newModificationTimes.setIndex(); return m_index != oldModificationTimes.setIndex(); } // const QMap ModificationRevisionSet::allModificationTimes() const { // QMap ret; // Utils::Set::Iterator it = m_allModificationTimes.iterator(); // while(it) { // const FileModificationPair* data = fileModificationPairRepository().itemFromIndex(*it); // ret[data->file] = data->revision; // ++it; // } // return ret; // } typedef Utils::VirtualSetNode, FileModificationSetRepositoryRepresenter> ModificationRevisionSetNode; // static bool (const Utils::SetNodeData* node) { // ModificationRevisionSetNode // if(!node) // return false; // } static bool nodeNeedsUpdate(uint index) { QMutexLocker lock(&modificationRevisionSetMutex); if(!index) return false; const auto currentTime = QDateTime::currentDateTime(); auto cached = needsUpdateCache.constFind(index); if(cached != needsUpdateCache.constEnd()) { if((*cached).first.secsTo(currentTime) < cacheModificationTimesForSeconds ) { return cached->second; } } bool result = false; const Utils::SetNodeData* nodeData = FileModificationSetRepositoryRepresenter::repository().nodeFromIndex(index); if(nodeData->contiguous()) { //Do the actual checking for(unsigned int a = nodeData->start(); a < nodeData->end(); ++a) { const FileModificationPair* data = fileModificationPairRepository().itemFromIndex(a); ModificationRevision revision = KDevelop::ModificationRevision::revisionForFile( data->file ); if( revision != data->revision ) { result = true; break; } } }else{ result = nodeNeedsUpdate(nodeData->leftNode()) || nodeNeedsUpdate(nodeData->rightNode()); } needsUpdateCache.insert(index, std::make_pair(currentTime, result)); return result; } QString ModificationRevisionSet::toString() const { QMutexLocker lock(&modificationRevisionSetMutex); - QString ret = QStringLiteral("["); // krazy:exclude=doublequote_chars Utils::Set set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set::Iterator it = set.iterator(); - bool first = true; + QStringList revisions; while(it) { - if(!first) - ret += QLatin1String(", "); - first = false; - const FileModificationPair* data = fileModificationPairRepository().itemFromIndex(*it); - ret += data->file.str() + QLatin1Char(':') + data->revision.toString(); + revisions.append(data->file.str() + QLatin1Char(':') + data->revision.toString()); ++it; } - ret += QLatin1Char(']'); + QString ret = QLatin1Char('[') + revisions.join(QLatin1String(", ")) + QLatin1Char(']'); return ret; } bool ModificationRevisionSet::needsUpdate() const { QMutexLocker lock(&modificationRevisionSetMutex); #ifdef DEBUG_NEEDSUPDATE Utils::Set set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set::Iterator it = set.iterator(); while(it) { const FileModificationPair* data = fileModificationPairRepository().itemFromIndex(*it); ModificationRevision revision = KDevelop::ModificationRevision::revisionForFile( data->file ); if( revision != data->revision ) { qCDebug(LANGUAGE) << "dependency" << data->file.str() << "has changed, stored stamp:" << data->revision << "new time:" << revision ; return true; } ++it; } return false; #else return nodeNeedsUpdate(m_index); #endif } ModificationRevisionSet& ModificationRevisionSet::operator+=(const ModificationRevisionSet& rhs) { QMutexLocker lock(&modificationRevisionSetMutex); Utils::Set oldModificationTimes = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set otherModificationTimes = Utils::Set(rhs.m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set newModificationTimes = oldModificationTimes; newModificationTimes += otherModificationTimes; newModificationTimes.staticRef(); oldModificationTimes.staticUnref(); m_index = newModificationTimes.setIndex(); return *this; } ModificationRevisionSet& ModificationRevisionSet::operator-=(const ModificationRevisionSet& rhs) { QMutexLocker lock(&modificationRevisionSetMutex); Utils::Set oldModificationTimes = Utils::Set(m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set otherModificationTimes = Utils::Set(rhs.m_index, &FileModificationSetRepositoryRepresenter::repository()); Utils::Set newModificationTimes = oldModificationTimes; newModificationTimes -= otherModificationTimes; newModificationTimes.staticRef(); oldModificationTimes.staticUnref(); m_index = newModificationTimes.setIndex(); return *this; } void FileModificationSetRepository::itemRemovedFromSets(uint index) { fileModificationPairRepository().deleteItem(index); needsUpdateCache.remove(index); } } diff --git a/kdevplatform/outputview/outputmodel.cpp b/kdevplatform/outputview/outputmodel.cpp index 5c06ee66e9..1ce206226f 100644 --- a/kdevplatform/outputview/outputmodel.cpp +++ b/kdevplatform/outputview/outputmodel.cpp @@ -1,471 +1,472 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright 2010 Aleix Pol Gonzalez * * Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com * * * * 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 "outputmodel.h" #include "filtereditem.h" #include "outputfilteringstrategies.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QVector) namespace KDevelop { /** * Number of lines that are processed in one go before we notify the GUI thread * about the result. It is generally faster to add multiple items to a model * in one go compared to adding each item independently. */ static const int BATCH_SIZE = 50; /** * Time in ms that we wait in the parse worker for new incoming lines before * actually processing them. If we already have enough for one batch though * we process immediately. */ static const int BATCH_AGGREGATE_TIME_DELAY = 50; class ParseWorker : public QObject { Q_OBJECT public: ParseWorker() : QObject(nullptr) , m_filter(new NoFilterStrategy) , m_timer(new QTimer(this)) { m_timer->setInterval(BATCH_AGGREGATE_TIME_DELAY); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &ParseWorker::process); } public Q_SLOTS: void changeFilterStrategy( KDevelop::IFilterStrategy* newFilterStrategy ) { m_filter = QSharedPointer( newFilterStrategy ); } void addLines( const QStringList& lines ) { m_cachedLines << lines; if (m_cachedLines.size() >= BATCH_SIZE) { // if enough lines were added, process immediately m_timer->stop(); process(); } else if (!m_timer->isActive()) { m_timer->start(); } } void flushBuffers() { m_timer->stop(); process(); emit allDone(); } Q_SIGNALS: void parsedBatch(const QVector& filteredItems); void progress(const KDevelop::IFilterStrategy::Progress& progress); void allDone(); private Q_SLOTS: /** * Process *all* cached lines, emit parsedBatch for each batch */ void process() { QVector filteredItems; filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); // apply pre-filtering functions std::transform(m_cachedLines.constBegin(), m_cachedLines.constEnd(), m_cachedLines.begin(), &KDevelop::stripAnsiSequences); // apply filtering strategy foreach(const QString& line, m_cachedLines) { FilteredItem item = m_filter->errorInLine(line); if( item.type == FilteredItem::InvalidItem ) { item = m_filter->actionInLine(line); } filteredItems << item; auto progress = m_filter->progressInLine(line); if (progress.percent >= 0 && m_progress.percent != progress.percent) { m_progress = progress; emit this->progress(m_progress); } if( filteredItems.size() == BATCH_SIZE ) { emit parsedBatch(filteredItems); filteredItems.clear(); filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); } } // Make sure to emit the rest as well if( !filteredItems.isEmpty() ) { emit parsedBatch(filteredItems); } m_cachedLines.clear(); } private: QSharedPointer m_filter; QStringList m_cachedLines; QTimer* m_timer; IFilterStrategy::Progress m_progress; }; class ParsingThread { public: ParsingThread() { m_thread.setObjectName(QStringLiteral("OutputFilterThread")); } virtual ~ParsingThread() { if (m_thread.isRunning()) { m_thread.quit(); m_thread.wait(); } } void addWorker(ParseWorker* worker) { if (!m_thread.isRunning()) { m_thread.start(); } worker->moveToThread(&m_thread); } private: QThread m_thread; }; Q_GLOBAL_STATIC(ParsingThread, s_parsingThread) class OutputModelPrivate { public: explicit OutputModelPrivate( OutputModel* model, const QUrl& builddir = QUrl() ); ~OutputModelPrivate(); bool isValidIndex( const QModelIndex&, int currentRowCount ) const; OutputModel* model; ParseWorker* worker; QVector m_filteredItems; // We use std::set because that is ordered std::set m_errorItems; // Indices of all items that we want to move to using previous and next QUrl m_buildDir; void linesParsed(const QVector& items) { model->beginInsertRows( QModelIndex(), model->rowCount(), model->rowCount() + items.size() - 1); + m_filteredItems.reserve(m_filteredItems.size() + items.size()); foreach( const FilteredItem& item, items ) { if( item.type == FilteredItem::ErrorItem ) { m_errorItems.insert(m_filteredItems.size()); } m_filteredItems << item; } model->endInsertRows(); } }; OutputModelPrivate::OutputModelPrivate( OutputModel* model_, const QUrl& builddir) : model(model_) , worker(new ParseWorker ) , m_buildDir( builddir ) { qRegisterMetaType >(); qRegisterMetaType(); qRegisterMetaType(); s_parsingThread->addWorker(worker); model->connect(worker, &ParseWorker::parsedBatch, model, [=] (const QVector& items) { linesParsed(items); }); model->connect(worker, &ParseWorker::allDone, model, &OutputModel::allDone); model->connect(worker, &ParseWorker::progress, model, &OutputModel::progress); } bool OutputModelPrivate::isValidIndex( const QModelIndex& idx, int currentRowCount ) const { return ( idx.isValid() && idx.row() >= 0 && idx.row() < currentRowCount && idx.column() == 0 ); } OutputModelPrivate::~OutputModelPrivate() { worker->deleteLater(); } OutputModel::OutputModel( const QUrl& builddir, QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this, builddir ) ) { } OutputModel::OutputModel( QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this ) ) { } OutputModel::~OutputModel() = default; QVariant OutputModel::data(const QModelIndex& idx , int role ) const { if( d->isValidIndex(idx, rowCount()) ) { switch( role ) { case Qt::DisplayRole: return d->m_filteredItems.at( idx.row() ).originalLine; break; case OutputModel::OutputItemTypeRole: return static_cast(d->m_filteredItems.at( idx.row() ).type); break; case Qt::FontRole: return QFontDatabase::systemFont(QFontDatabase::FixedFont); break; default: break; } } return QVariant(); } int OutputModel::rowCount( const QModelIndex& parent ) const { if( !parent.isValid() ) return d->m_filteredItems.count(); return 0; } QVariant OutputModel::headerData( int, Qt::Orientation, int ) const { return QVariant(); } void OutputModel::activate( const QModelIndex& index ) { if( index.model() != this || !d->isValidIndex(index, rowCount()) ) { return; } qCDebug(OUTPUTVIEW) << "Model activated" << index.row(); FilteredItem item = d->m_filteredItems.at( index.row() ); if( item.isActivatable ) { qCDebug(OUTPUTVIEW) << "activating:" << item.lineNo << item.url; KTextEditor::Cursor range( item.lineNo, item.columnNo ); KDevelop::IDocumentController *docCtrl = KDevelop::ICore::self()->documentController(); QUrl url = item.url; if (item.url.isEmpty()) { qCWarning(OUTPUTVIEW) << "trying to open empty url"; return; } if(url.isRelative()) { url = d->m_buildDir.resolved(url); } Q_ASSERT(!url.isRelative()); docCtrl->openDocument( url, range ); } else { qCDebug(OUTPUTVIEW) << "not an activateable item"; } } QModelIndex OutputModel::firstHighlightIndex() { if( !d->m_errorItems.empty() ) { return index( *d->m_errorItems.begin(), 0, QModelIndex() ); } for( int row = 0; row < rowCount(); ++row ) { if( d->m_filteredItems.at( row ).isActivatable ) { return index( row, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::nextHighlightIndex( const QModelIndex ¤tIdx ) { int startrow = d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() + 1 : 0; if( !d->m_errorItems.empty() ) { qCDebug(OUTPUTVIEW) << "searching next error"; // Jump to the next error item std::set< int >::const_iterator next = d->m_errorItems.lower_bound( startrow ); if( next == d->m_errorItems.end() ) next = d->m_errorItems.begin(); return index( *next, 0, QModelIndex() ); } for( int row = 0; row < rowCount(); ++row ) { int currow = (startrow + row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::previousHighlightIndex( const QModelIndex ¤tIdx ) { //We have to ensure that startrow is >= rowCount - 1 to get a positive value from the % operation. int startrow = rowCount() + (d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() : rowCount()) - 1; if(!d->m_errorItems.empty()) { qCDebug(OUTPUTVIEW) << "searching previous error"; // Jump to the previous error item std::set< int >::const_iterator previous = d->m_errorItems.lower_bound( currentIdx.row() ); if( previous == d->m_errorItems.begin() ) previous = d->m_errorItems.end(); --previous; return index( *previous, 0, QModelIndex() ); } for ( int row = 0; row < rowCount(); ++row ) { int currow = (startrow - row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::lastHighlightIndex() { if( !d->m_errorItems.empty() ) { return index( *d->m_errorItems.rbegin(), 0, QModelIndex() ); } for( int row = rowCount()-1; row >=0; --row ) { if( d->m_filteredItems.at( row ).isActivatable ) { return index( row, 0, QModelIndex() ); } } return QModelIndex(); } void OutputModel::setFilteringStrategy(const OutputFilterStrategy& currentStrategy) { // TODO: Turn into factory, decouple from OutputModel IFilterStrategy* filter = nullptr; switch( currentStrategy ) { case NoFilter: filter = new NoFilterStrategy; break; case CompilerFilter: filter = new CompilerFilterStrategy( d->m_buildDir ); break; case ScriptErrorFilter: filter = new ScriptErrorFilterStrategy; break; case NativeAppErrorFilter: filter = new NativeAppErrorFilterStrategy; break; case StaticAnalysisFilter: filter = new StaticAnalysisFilterStrategy; break; } if (!filter) { filter = new NoFilterStrategy; } QMetaObject::invokeMethod(d->worker, "changeFilterStrategy", Q_ARG(KDevelop::IFilterStrategy*, filter)); } void OutputModel::setFilteringStrategy(IFilterStrategy* filterStrategy) { QMetaObject::invokeMethod(d->worker, "changeFilterStrategy", Q_ARG(KDevelop::IFilterStrategy*, filterStrategy)); } void OutputModel::appendLines( const QStringList& lines ) { if( lines.isEmpty() ) return; QMetaObject::invokeMethod(d->worker, "addLines", Q_ARG(QStringList, lines)); } void OutputModel::appendLine( const QString& line ) { appendLines( QStringList() << line ); } void OutputModel::ensureAllDone() { QMetaObject::invokeMethod(d->worker, "flushBuffers"); } void OutputModel::clear() { ensureAllDone(); beginResetModel(); d->m_filteredItems.clear(); endResetModel(); } } #include "outputmodel.moc" #include "moc_outputmodel.cpp" diff --git a/kdevplatform/outputview/tests/test_filteringstrategy.cpp b/kdevplatform/outputview/tests/test_filteringstrategy.cpp index e6d76779bc..88ce65d890 100644 --- a/kdevplatform/outputview/tests/test_filteringstrategy.cpp +++ b/kdevplatform/outputview/tests/test_filteringstrategy.cpp @@ -1,554 +1,554 @@ /* This file is part of KDevelop Copyright 2012 Milian Wolff Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com 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, see . */ #include "test_filteringstrategy.h" #include "testlinebuilderfunctions.h" #include #include #include using namespace KDevelop; QTEST_GUILESS_MAIN(TestFilteringStrategy) namespace QTest { template<> inline char* toString(const FilteredItem::FilteredOutputItemType& type) { switch (type) { case FilteredItem::ActionItem: return qstrdup("ActionItem"); case FilteredItem::CustomItem: return qstrdup("CustomItem"); case FilteredItem::ErrorItem: return qstrdup("ErrorItem"); case FilteredItem::InformationItem: return qstrdup("InformationItem"); case FilteredItem::InvalidItem: return qstrdup("InvalidItem"); case FilteredItem::StandardItem: return qstrdup("StandardItem"); case FilteredItem::WarningItem: return qstrdup("WarningItem"); } return qstrdup("unknown"); } inline QTestData& newRowForPathType(const char *dataTag, TestPathType pathType) { switch (pathType) { case UnixFilePathNoSpaces: return QTest::newRow(QString(QLatin1String(dataTag)+QLatin1String("-unix-ns")).toUtf8().constData()); case UnixFilePathWithSpaces: return QTest::newRow(QString(QLatin1String(dataTag)+QLatin1String("-unix-ws")).toUtf8().constData()); case WindowsFilePathNoSpaces: return QTest::newRow(QString(QLatin1String(dataTag)+QLatin1String("-windows-ns")).toUtf8().constData()); case WindowsFilePathWithSpaces: return QTest::newRow(QString(QLatin1String(dataTag)+QLatin1String("-windows-ws")).toUtf8().constData()); } Q_UNREACHABLE(); } } void TestFilteringStrategy::testNoFilterStrategy_data() { QTest::addColumn("line"); QTest::addColumn("expected"); QTest::newRow("cppcheck-info-line") << buildCppCheckInformationLine() << FilteredItem::InvalidItem; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("cppcheck-error-line", pathType) << buildCppCheckErrorLine(pathType) << FilteredItem::InvalidItem; QTest::newRowForPathType("compiler-line", pathType) << buildCompilerLine(pathType) << FilteredItem::InvalidItem; QTest::newRowForPathType("compiler-error-line", pathType) << buildCompilerErrorLine(pathType) << FilteredItem::InvalidItem; } QTest::newRow("compiler-action-line") << buildCompilerActionLine() << FilteredItem::InvalidItem; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("compiler-information-line", pathType) << buildCompilerInformationLine(pathType) << FilteredItem::InvalidItem; QTest::newRowForPathType("python-error-line", pathType) << buildPythonErrorLine(pathType) << FilteredItem::InvalidItem; } } void TestFilteringStrategy::testNoFilterStrategy() { QFETCH(QString, line); QFETCH(FilteredItem::FilteredOutputItemType, expected); NoFilterStrategy testee; FilteredItem item1 = testee.errorInLine(line); QCOMPARE(item1.type, expected); item1 = testee.actionInLine(line); QCOMPARE(item1.type, expected); } void TestFilteringStrategy::testCompilerFilterStrategy_data() { QTest::addColumn("line"); QTest::addColumn("expectedError"); QTest::addColumn("expectedAction"); QTest::addColumn("pathType"); QTest::newRow("cppcheck-info-line") << buildCppCheckInformationLine() << FilteredItem::InvalidItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("cppcheck-error-line", pathType) << buildCppCheckErrorLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-line", pathType) << buildCompilerLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-error-line", pathType) << buildCompilerErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-information-line", pathType) << buildCompilerInformationLine(pathType) << FilteredItem::InformationItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-information-line2", pathType) << buildInfileIncludedFromFirstLine(pathType) << FilteredItem::InformationItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-information-line3", pathType) << buildInfileIncludedFromSecondLine(pathType) << FilteredItem::InformationItem << FilteredItem::InvalidItem << pathType; } QTest::newRow("cmake-error-line1") << "CMake Error at CMakeLists.txt:2 (cmake_minimum_required):" << FilteredItem::ErrorItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; QTest::newRow("cmake-error-multiline1") << "CMake Error: Error in cmake code at" << FilteredItem::InvalidItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("cmake-error-multiline2", pathType) << buildCmakeConfigureMultiLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; } QTest::newRow("cmake-warning-line") << "CMake Warning (dev) in CMakeLists.txt:" << FilteredItem::WarningItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; QTest::newRow("cmake-automoc-error") << "AUTOMOC: error: /foo/bar.cpp The file includes the moc file \"moc_bar1.cpp\"" << FilteredItem::ErrorItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; QTest::newRow("cmake-automoc4-error") << "automoc4: The file \"/foo/bar.cpp\" includes the moc file \"bar1.moc\"" << FilteredItem::InformationItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; QTest::newRow("cmake-autogen-error") << "AUTOGEN: error: /foo/bar.cpp The file includes the moc file \"moc_bar1.cpp\"" << FilteredItem::ErrorItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; QTest::newRow("linker-action-line") << "linking testCustombuild (g++)" << FilteredItem::InvalidItem << FilteredItem::ActionItem << UnixFilePathNoSpaces; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("linker-error-line", pathType) << buildLinkerErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("python-error-line", pathType) << buildPythonErrorLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; } } void TestFilteringStrategy::testCompilerFilterStrategy() { QFETCH(QString, line); QFETCH(FilteredItem::FilteredOutputItemType, expectedError); QFETCH(FilteredItem::FilteredOutputItemType, expectedAction); QFETCH(TestPathType, pathType); QUrl projecturl = QUrl::fromLocalFile( projectPath(pathType) ); CompilerFilterStrategy testee(projecturl); FilteredItem item1 = testee.errorInLine(line); QCOMPARE(item1.type, expectedError); item1 = testee.actionInLine(line); QCOMPARE(item1.type, expectedAction); } void TestFilteringStrategy::testCompilerFilterstrategyMultipleKeywords_data() { QTest::addColumn("line"); QTest::addColumn("expectedError"); QTest::addColumn("expectedAction"); QTest::newRow("warning-containing-error-word") << "RingBuffer.cpp:64:6: warning: unused parameter ‘errorItem’ [-Wunused-parameter]" << FilteredItem::WarningItem << FilteredItem::InvalidItem; QTest::newRow("error-containing-info-word") << "NodeSet.hpp:89:27: error: ‘Info’ was not declared in this scope" << FilteredItem::ErrorItem << FilteredItem::InvalidItem; QTest::newRow("warning-in-filename-containing-error-word") << "ErrorHandling.cpp:100:56: warning: unused parameter ‘item’ [-Wunused-parameter]" << FilteredItem::WarningItem << FilteredItem::InvalidItem; QTest::newRow("error-in-filename-containing-warning-word") << "WarningHandling.cpp:100:56: error: ‘Item’ was not declared in this scope" << FilteredItem::ErrorItem << FilteredItem::InvalidItem; } void TestFilteringStrategy::testCompilerFilterstrategyMultipleKeywords() { QFETCH(QString, line); QFETCH(FilteredItem::FilteredOutputItemType, expectedError); QFETCH(FilteredItem::FilteredOutputItemType, expectedAction); QUrl projecturl = QUrl::fromLocalFile( projectPath() ); CompilerFilterStrategy testee(projecturl); FilteredItem item1 = testee.errorInLine(line); QCOMPARE(item1.type, expectedError); item1 = testee.actionInLine(line); QCOMPARE(item1.type, expectedAction); } void TestFilteringStrategy::testScriptErrorFilterStrategy_data() { QTest::addColumn("line"); QTest::addColumn("expectedError"); QTest::addColumn("expectedAction"); QTest::newRow("cppcheck-info-line") << buildCppCheckInformationLine() << FilteredItem::InvalidItem << FilteredItem::InvalidItem; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("cppcheck-error-line", pathType) << buildCppCheckErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem; QTest::newRowForPathType("compiler-line", pathType) << buildCompilerLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem; QTest::newRowForPathType("compiler-error-line", pathType) << buildCompilerErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem; } QTest::newRow("compiler-action-line") << "linking testCustombuild (g++)" << FilteredItem::InvalidItem << FilteredItem::InvalidItem; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("python-error-line", pathType) << buildPythonErrorLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem; } } void TestFilteringStrategy::testScriptErrorFilterStrategy() { QFETCH(QString, line); QFETCH(FilteredItem::FilteredOutputItemType, expectedError); QFETCH(FilteredItem::FilteredOutputItemType, expectedAction); ScriptErrorFilterStrategy testee; FilteredItem item1 = testee.errorInLine(line); QCOMPARE(item1.type, expectedError); item1 = testee.actionInLine(line); QCOMPARE(item1.type, expectedAction); } void TestFilteringStrategy::testNativeAppErrorFilterStrategy_data() { QTest::addColumn("line"); QTest::addColumn("file"); QTest::addColumn("lineNo"); QTest::addColumn("column"); QTest::addColumn("itemtype"); // BEGIN: C++ QTest::newRow("cassert") << "a.out: /foo/bar/test.cpp:5: int main(): Assertion `false' failed." << "/foo/bar/test.cpp" << 4 << 0 << FilteredItem::ErrorItem; // END: C++ // BEGIN: Qt // TODO: qt-connect-* and friends shouldn't be error items but warnings items instead // this needs refactoring in outputfilteringstrategies, though... QTest::newRow("qt-connect-nosuch-slot") << "QObject::connect: No such slot Foo::bar() in /foo/bar.cpp:313" << "/foo/bar.cpp" << 312 << 0 << FilteredItem::ErrorItem; QTest::newRow("qt-connect-nosuch-signal") << "QObject::connect: No such signal Foo::bar() in /foo/bar.cpp:313" << "/foo/bar.cpp" << 312 << 0 << FilteredItem::ErrorItem; QTest::newRow("qt-connect-parentheses-slot") << "QObject::connect: Parentheses expected, slot Foo::bar() in /foo/bar.cpp:313" << "/foo/bar.cpp" << 312 << 0 << FilteredItem::ErrorItem; QTest::newRow("qt-connect-parentheses-signal") << "QObject::connect: Parentheses expected, signal Foo::bar() in /foo/bar.cpp:313" << "/foo/bar.cpp" << 312 << 0 << FilteredItem::ErrorItem; QTest::newRow("qt-assert") << "ASSERT: \"errors().isEmpty()\" in file /tmp/foo/bar.cpp, line 49" << "/tmp/foo/bar.cpp" << 48 << 0 << FilteredItem::ErrorItem; QTest::newRow("qttest-assert") << "QFATAL : FooTest::testBar() ASSERT: \"index.isValid()\" in file /foo/bar.cpp, line 32" << "/foo/bar.cpp" << 31 << 0 << FilteredItem::ErrorItem; QTest::newRow("qttest-loc") << " Loc: [/foo/bar.cpp(33)]" << "/foo/bar.cpp" << 32 << 0 << FilteredItem::ErrorItem; QTest::newRow("qttest-loc-nocatch") << " Loc: [Unknown file(0)]" << "" << -1 << -1 << FilteredItem::InvalidItem; QTest::newRow("qml-import-unix") << "file:///path/to/foo.qml:7:1: Bar is not a type" << "/path/to/foo.qml" << 6 << 0 << FilteredItem::ErrorItem; QTest::newRow("qml-import-unix1") << "file:///path/to/foo.qml:7:1: Bar is ambiguous. Found in A and in B" << "/path/to/foo.qml" << 6 << 0 << FilteredItem::ErrorItem; QTest::newRow("qml-import-unix2") << "file:///path/to/foo.qml:7:1: Bar is instantiated recursively" << "/path/to/foo.qml" << 6 << 0 << FilteredItem::ErrorItem; QTest::newRow("qml-typeerror") << "file:///path/to/foo.qml:7: TypeError: Cannot read property 'height' of null" << "/path/to/foo.qml" << 6 << 0 << FilteredItem::ErrorItem; QTest::newRow("qml-referenceerror") << "file:///path/to/foo.qml:7: ReferenceError: readOnly is not defined" << "/path/to/foo.qml" << 6 << 0 << FilteredItem::ErrorItem; QTest::newRow("qml-bindingloop") << "file:///path/to/foo.qml:7:5: QML Row: Binding loop detected for property \"height\"" << "/path/to/foo.qml" << 6 << 4 << FilteredItem::ErrorItem; // END: Qt } void TestFilteringStrategy::testNativeAppErrorFilterStrategy() { QFETCH(QString, line); QFETCH(QString, file); QFETCH(int, lineNo); QFETCH(int, column); QFETCH(FilteredItem::FilteredOutputItemType, itemtype); NativeAppErrorFilterStrategy testee; FilteredItem item = testee.errorInLine(line); QCOMPARE(item.url.path(), file); QCOMPARE(item.lineNo , lineNo); QCOMPARE(item.columnNo , column); QCOMPARE(item.type , itemtype); } void TestFilteringStrategy::testStaticAnalysisFilterStrategy_data() { QTest::addColumn("line"); QTest::addColumn("expectedError"); QTest::addColumn("expectedAction"); QTest::addColumn("pathType"); QTest::newRow("cppcheck-info-line") << buildCppCheckInformationLine() << FilteredItem::InvalidItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("cppcheck-error-line", pathType) - << buildCppCheckErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType;; + << buildCppCheckErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("krazy2-error-line", pathType) << buildKrazyErrorLine(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("krazy2-error-line-two-colons", pathType) << buildKrazyErrorLine2(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("krazy2-error-line-error-description", pathType) << buildKrazyErrorLine3(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("krazy2-error-line-wo-line-info", pathType) << buildKrazyErrorLineNoLineInfo(pathType) << FilteredItem::ErrorItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-line", pathType) << buildCompilerLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; QTest::newRowForPathType("compiler-error-line", pathType) << buildCompilerErrorLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; } QTest::newRow("compiler-action-line") << "linking testCustombuild (g++)" << FilteredItem::InvalidItem << FilteredItem::InvalidItem << UnixFilePathNoSpaces; for (TestPathType pathType : {UnixFilePathNoSpaces, UnixFilePathWithSpaces}) { QTest::newRowForPathType("python-error-line", pathType) << buildPythonErrorLine(pathType) << FilteredItem::InvalidItem << FilteredItem::InvalidItem << pathType; } } void TestFilteringStrategy::testStaticAnalysisFilterStrategy() { QFETCH(QString, line); QFETCH(FilteredItem::FilteredOutputItemType, expectedError); QFETCH(FilteredItem::FilteredOutputItemType, expectedAction); QFETCH(TestPathType, pathType); // Test that url's are extracted correctly as well QString referencePath = projectPath(pathType) + "main.cpp"; StaticAnalysisFilterStrategy testee; FilteredItem item1 = testee.errorInLine(line); QString extractedPath = item1.url.toLocalFile(); QVERIFY((item1.type != FilteredItem::ErrorItem) || ( extractedPath == referencePath)); QCOMPARE(item1.type, expectedError); item1 = testee.actionInLine(line); QCOMPARE(item1.type, expectedAction); } void TestFilteringStrategy::testCompilerFilterstrategyUrlFromAction_data() { QTest::addColumn("line"); QTest::addColumn("expectedLastDir"); QTest::addColumn("pathType"); for (TestPathType pathType : #ifdef Q_OS_WIN {WindowsFilePathNoSpaces, WindowsFilePathWithSpaces} #else {UnixFilePathNoSpaces, UnixFilePathWithSpaces} #endif ) { const QString basepath = projectPath(pathType); QTest::newRowForPathType("cmake-line1", pathType) << "[ 25%] Building CXX object path/to/one/CMakeFiles/file.o" << QString( basepath + "/path/to/one" ) << pathType; QTest::newRowForPathType("cmake-line2", pathType) << "[ 26%] Building CXX object path/to/two/CMakeFiles/file.o" << QString( basepath + "/path/to/two") << pathType; QTest::newRowForPathType("cmake-line3", pathType) << "[ 26%] Building CXX object path/to/three/CMakeFiles/file.o" << QString( basepath + "/path/to/three") << pathType; QTest::newRowForPathType("cmake-line4", pathType) << "[ 26%] Building CXX object path/to/four/CMakeFiles/file.o" << QString( basepath + "/path/to/four") << pathType; QTest::newRowForPathType("cmake-line5", pathType) << "[ 26%] Building CXX object path/to/two/CMakeFiles/file.o" << QString( basepath + "/path/to/two") << pathType; QTest::newRowForPathType("cd-line6", pathType) << QString("make[4]: Entering directory '" + basepath + "/path/to/one/'") << QString( basepath + "/path/to/one") << pathType; QTest::newRowForPathType("waf-cd", pathType) << QString("Waf: Entering directory `" + basepath + "/path/to/two/'") << QString( basepath + "/path/to/two") << pathType; QTest::newRowForPathType("cmake-line7", pathType) << QStringLiteral("[ 50%] Building CXX object CMakeFiles/testdeque.dir/RingBuffer.cpp.o") << QString( basepath) << pathType; QTest::newRowForPathType("cmake-cd-line8", pathType) << QString("> /usr/bin/cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Debug " + basepath) << QString( basepath ) << pathType; } } void TestFilteringStrategy::testCompilerFilterstrategyUrlFromAction() { QFETCH(QString, line); QFETCH(QString, expectedLastDir); QFETCH(TestPathType, pathType); QUrl projecturl = QUrl::fromLocalFile( projectPath(pathType) ); CompilerFilterStrategy testee(projecturl); FilteredItem item1 = testee.actionInLine(line); int last = testee.currentDirs().size() - 1; QCOMPARE(testee.currentDirs().at(last), expectedLastDir); } void TestFilteringStrategy::benchMarkCompilerFilterAction() { QString projecturl = projectPath(); QStringList outputlines; const int numLines(10000); int j(0), k(0), l(0), m(0); do { ++j; ++k; ++l; QString tmp; if(m % 2 == 0) { tmp = QStringLiteral( "[ 26%] Building CXX object /this/is/the/path/to/the/files/%1/%2/%3/CMakeFiles/file.o").arg( j ).arg( k ).arg( l ); } else { tmp = QString( "make[4]: Entering directory '" + projecturl + "/this/is/the/path/to/the/files/%1/%2/%3/").arg( j ).arg( k ).arg( l ); } outputlines << tmp; if(j % 6 == 0) { j = 0; ++m; } if(k % 9 == 0) { k = 0; ++m; } if(l % 13 == 0) { l = 0; ++m; } } while(outputlines.size() < numLines ); // gives us numLines (-ish) QElapsedTimer totalTime; totalTime.start(); static CompilerFilterStrategy testee(QUrl::fromLocalFile(projecturl)); FilteredItem item1(QStringLiteral("dummyline"), FilteredItem::InvalidItem); QBENCHMARK { for(int i = 0; i < outputlines.size(); ++i) { item1 = testee.actionInLine(outputlines.at(i)); } } const qint64 elapsed = totalTime.elapsed(); qDebug() << "ms elapsed to add directories: " << elapsed; qDebug() << "total number of directories: " << outputlines.count(); const double avgDirectoryInsertion = double(elapsed) / outputlines.count(); qDebug() << "average ms spend pr. dir: " << avgDirectoryInsertion; QVERIFY(avgDirectoryInsertion < 2); } void TestFilteringStrategy::testExtractionOfLineAndColumn_data() { QTest::addColumn("line"); QTest::addColumn("file"); QTest::addColumn("lineNr"); QTest::addColumn("column"); QTest::addColumn("itemtype"); #ifdef Q_OS_WIN QTest::newRow("msvc-compiler-error-line") << "Z:\\kderoot\\download\\git\\kcoreaddons\\src\\lib\\jobs\\kjob.cpp(3): error C2065: 'dadsads': undeclared identifier" << "Z:/kderoot/download/git/kcoreaddons/src/lib/jobs/kjob.cpp" << 2 << 0 << FilteredItem::ErrorItem; QTest::newRow("msvc-compiler-warning-line") << "c:\\program files\\microsoft visual studio 10.0\\vc\\include\\crtdefs.h(527): warning C4229: anachronism used : modifiers on data are ignored" << "c:/program files/microsoft visual studio 10.0/vc/include/crtdefs.h" << 526 << 0 << FilteredItem::WarningItem; #else QTest::newRow("gcc-with-col") << "/path/to/file.cpp:123:45: fatal error: ..." << "/path/to/file.cpp" << 122 << 44 << FilteredItem::ErrorItem; QTest::newRow("gcc-no-col") << "/path/to/file.cpp:123: error ..." << "/path/to/file.cpp" << 122 << 0 << FilteredItem::ErrorItem; QTest::newRow("gcc-app-gives-invalid-column") << "/path/to/file.h:60:0:\ warning: \"SOME_MACRO\" redefined" << "/path/to/file.h" << 59 << 0 << FilteredItem::WarningItem; QTest::newRow("fortcom") << "fortcom: Error: Ogive8.f90, line 123: ..." << QString(projectPath() + "/Ogive8.f90") << 122 << 0 << FilteredItem::ErrorItem; QTest::newRow("fortcomError") << "fortcom: Error: ./Ogive8.f90, line 123: ..." << QString(projectPath() + "/Ogive8.f90") << 122 << 0 << FilteredItem::ErrorItem; QTest::newRow("fortcomWarning") << "fortcom: Warning: /path/Ogive8.f90, line 123: ..." << "/path/Ogive8.f90" << 122 << 0 << FilteredItem::WarningItem; QTest::newRow("fortcomInfo") << "fortcom: Info: Ogive8.f90, line 123: ..." << QString(projectPath() + "/Ogive8.f90") << 122 << 0 << FilteredItem::InformationItem; QTest::newRow("libtool") << "libtool: link: warning: ..." << "" << -1 << 0 << FilteredItem::WarningItem; QTest::newRow("gfortranError1") << "/path/to/file.f90:123.456:Error: ...." << "/path/to/file.f90" << 122 << 455 << FilteredItem::ErrorItem; QTest::newRow("gfortranError2") << "/path/flib.f90:3567.22:" << "/path/flib.f90" << 3566 << 21 << FilteredItem::ErrorItem; QTest::newRow("ant-javac-Warning") << " [javac] /path/class.java:383: warning: [deprecation] ..." << "/path/class.java" << 382 << 0 << FilteredItem::WarningItem; QTest::newRow("ant-javac-Error") << " [javac] /path/class.java:447: error: cannot find symbol" << "/path/class.java" << 446 << 0 << FilteredItem::ErrorItem; QTest::newRow("cmake-error") << "CMake Error at somesubdir/CMakeLists.txt:214:" << "/some/path/to/a/somesubdir/CMakeLists.txt" << 213 << 0 << FilteredItem::ErrorItem; #endif } void TestFilteringStrategy::testExtractionOfLineAndColumn() { QFETCH(QString, line); QFETCH(QString, file); QFETCH(int, lineNr); QFETCH(int, column); QFETCH(FilteredItem::FilteredOutputItemType, itemtype); QUrl projecturl = QUrl::fromLocalFile( projectPath() ); CompilerFilterStrategy testee(projecturl); FilteredItem item1 = testee.errorInLine(line); QCOMPARE(item1.type , itemtype); QCOMPARE(KDevelop::toUrlOrLocalFile(item1.url), file); QCOMPARE(item1.lineNo , lineNr); QCOMPARE(item1.columnNo , column); } diff --git a/kdevplatform/project/builderjob.cpp b/kdevplatform/project/builderjob.cpp index 3ce1673fab..aea39eb334 100644 --- a/kdevplatform/project/builderjob.cpp +++ b/kdevplatform/project/builderjob.cpp @@ -1,265 +1,267 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "builderjob.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; struct SubJobData { BuilderJob::BuildType type; KJob* job; ProjectBaseItem* item; }; Q_DECLARE_TYPEINFO(SubJobData, Q_MOVABLE_TYPE); namespace KDevelop { class BuilderJobPrivate { public: explicit BuilderJobPrivate( BuilderJob* job ) : q(job) , failOnFirstError(true) { } BuilderJob* q; void addJob( BuilderJob::BuildType, ProjectBaseItem* ); bool failOnFirstError; QString buildTypeToString( BuilderJob::BuildType type ) const; bool hasJobForProject( BuilderJob::BuildType type, IProject* project ) const { foreach(const SubJobData& data, m_metadata) { if (data.type == type && data.item->project() == project) { return true; } } return false; } /** * a structure to keep metadata of all registered jobs */ QVector m_metadata; /** * get the subjob list and clear this composite job */ QVector takeJobList(); }; } QString BuilderJobPrivate::buildTypeToString(BuilderJob::BuildType type) const { switch( type ) { case BuilderJob::Build: return i18nc( "@info:status", "build" ); case BuilderJob::Clean: return i18nc( "@info:status", "clean" ); case BuilderJob::Configure: return i18nc( "@info:status", "configure" ); case BuilderJob::Install: return i18nc( "@info:status", "install" ); case BuilderJob::Prune: return i18nc( "@info:status", "prune" ); } return QString(); } void BuilderJobPrivate::addJob( BuilderJob::BuildType t, ProjectBaseItem* item ) { Q_ASSERT(item); qCDebug(PROJECT) << "adding build job for item:" << item->text(); Q_ASSERT(item->project()); qCDebug(PROJECT) << "project for item:" << item->project()->name(); Q_ASSERT(item->project()->projectItem()); qCDebug(PROJECT) << "project item for the project:" << item->project()->projectItem()->text(); if( !item->project()->buildSystemManager() ) { qCWarning(PROJECT) << "no buildsystem manager for:" << item->text() << item->project()->name(); return; } qCDebug(PROJECT) << "got build system manager"; Q_ASSERT(item->project()->buildSystemManager()->builder()); KJob* j = nullptr; switch( t ) { case BuilderJob::Build: j = item->project()->buildSystemManager()->builder()->build( item ); break; case BuilderJob::Clean: j = item->project()->buildSystemManager()->builder()->clean( item ); break; case BuilderJob::Install: j = item->project()->buildSystemManager()->builder()->install( item ); break; case BuilderJob::Prune: if (!hasJobForProject(t, item->project())) { j = item->project()->buildSystemManager()->builder()->prune( item->project() ); } break; case BuilderJob::Configure: if (!hasJobForProject(t, item->project())) { j = item->project()->buildSystemManager()->builder()->configure( item->project() ); } break; } if( j ) { q->addCustomJob( t, j, item ); } } BuilderJob::BuilderJob() : d( new BuilderJobPrivate( this ) ) { } BuilderJob::~BuilderJob() = default; void BuilderJob::addItems( BuildType t, const QList& items ) { foreach( ProjectBaseItem* item, items ) { d->addJob( t, item ); } } void BuilderJob::addProjects( BuildType t, const QList& projects ) { foreach( IProject* project, projects ) { d->addJob( t, project->projectItem() ); } } void BuilderJob::addItem( BuildType t, ProjectBaseItem* item ) { d->addJob( t, item ); } void BuilderJob::addCustomJob( BuilderJob::BuildType type, KJob* job, ProjectBaseItem* item ) { if( BuilderJob* builderJob = dynamic_cast( job ) ) { // If a subjob is a builder job itself, re-own its job list to avoid having recursive composite jobs. QVector subjobs = builderJob->d->takeJobList(); builderJob->deleteLater(); foreach( const SubJobData& subjob, subjobs ) { subjob.job->setParent(this); addSubjob( subjob.job ); } d->m_metadata << subjobs; } else { job->setParent(this); addSubjob( job ); SubJobData data; data.type = type; data.job = job; data.item = item; d->m_metadata << data; } } QVector< SubJobData > BuilderJobPrivate::takeJobList() { QVector< SubJobData > ret = m_metadata; m_metadata.clear(); q->clearSubjobs(); q->setObjectName( QString() ); return ret; } void BuilderJob::updateJobName() { // Which items are mentioned in the set // Make it a list to preserve ordering; search overhead (n^2) isn't too big QList< ProjectBaseItem* > registeredItems; // Which build types are mentioned in the set // (Same rationale applies) QList< BuildType > buildTypes; // Whether there are jobs without any specific item bool hasNullItems = false; foreach( const SubJobData& subjob, d->m_metadata ) { if( subjob.item ) { if( !registeredItems.contains( subjob.item ) ) { registeredItems.append( subjob.item ); } if( !buildTypes.contains( subjob.type ) ) { buildTypes.append( subjob.type ); } } else { hasNullItems = true; } } QString itemNames; if( !hasNullItems ) { QStringList itemNamesList; + itemNamesList.reserve(registeredItems.size()); foreach( ProjectBaseItem* item, registeredItems ) { itemNamesList << item->text(); } itemNames = itemNamesList.join(QStringLiteral(", ")); } else { itemNames = i18nc( "Unspecified set of build items (e. g. projects, targets)", "Various items" ); } QString methodNames; QStringList methodNamesList; + methodNamesList.reserve(buildTypes.size()); foreach( BuildType type, buildTypes ) { methodNamesList << d->buildTypeToString( type ); } methodNames = methodNamesList.join( QStringLiteral( ", " ) ); QString jobName = QStringLiteral( "%1: %2" ).arg( itemNames, methodNames ); setObjectName( jobName ); } void BuilderJob::start() { // Automatically save all documents before starting to build // might need an option to turn off at some point // Also should be moved into the builder and there try to find target(s) for the given item and then just save the documents of that target -> list?? if( ICore::self()->activeSession()->config()->group("Project Manager").readEntry( "Save All Documents Before Building", true ) ) { ICore::self()->documentController()->saveAllDocuments( IDocument::Silent ); } ExecuteCompositeJob::start(); } diff --git a/kdevplatform/project/projectbuildsetmodel.cpp b/kdevplatform/project/projectbuildsetmodel.cpp index 4c03451628..620f8d0e35 100644 --- a/kdevplatform/project/projectbuildsetmodel.cpp +++ b/kdevplatform/project/projectbuildsetmodel.cpp @@ -1,408 +1,411 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright 2009 Aleix Pol * * * * 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 "projectbuildsetmodel.h" #include #include #include #include #include #include #include #include "projectmodel.h" #include #include namespace KDevelop { BuildItem::BuildItem() { } BuildItem::BuildItem( const QStringList & itemPath ) : m_itemPath( itemPath ) { } BuildItem::BuildItem( KDevelop::ProjectBaseItem* item ) { initializeFromItem( item ); } BuildItem::BuildItem( const BuildItem& rhs ) : m_itemPath(rhs.itemPath()) { } void BuildItem::initializeFromItem( KDevelop::ProjectBaseItem* item ) { Q_ASSERT(item); KDevelop::ProjectModel* model=KDevelop::ICore::self()->projectController()->projectModel(); m_itemPath = model->pathFromIndex(item->index()); } QString BuildItem::itemName() const { return m_itemPath.last(); } QString BuildItem::itemProject() const { return m_itemPath.first(); } KDevelop::ProjectBaseItem* BuildItem::findItem() const { KDevelop::ProjectModel* model=KDevelop::ICore::self()->projectController()->projectModel(); QModelIndex idx = model->pathToIndex(m_itemPath); return model->itemFromIndex(idx); } bool operator==( const BuildItem& rhs, const BuildItem& lhs ) { return( rhs.itemPath() == lhs.itemPath() ); } BuildItem& BuildItem::operator=( const BuildItem& rhs ) { if( this == &rhs ) return *this; m_itemPath = rhs.itemPath(); return *this; } class ProjectBuildSetModelPrivate { public: QList items; QList orderingCache; }; ProjectBuildSetModel::ProjectBuildSetModel( QObject* parent ) : QAbstractTableModel( parent ) , d(new ProjectBuildSetModelPrivate) { } ProjectBuildSetModel::~ProjectBuildSetModel() = default; void ProjectBuildSetModel::loadFromSession( ISession* session ) { if (!session) { return; } // Load the item ordering cache KConfigGroup sessionBuildSetConfig = session->config()->group( "Buildset" ); QVariantList sessionBuildItems = KDevelop::stringToQVariant( sessionBuildSetConfig.readEntry( "BuildItems", QString() ) ).toList(); + d->orderingCache.reserve(d->orderingCache.size() + sessionBuildItems.size()); foreach( const QVariant& item, sessionBuildItems ) { d->orderingCache.append(item.toStringList()); } } void ProjectBuildSetModel::storeToSession( ISession* session ) { if (!session) { return; } // Store the item ordering cache QVariantList sessionBuildItems; + sessionBuildItems.reserve(d->orderingCache.size()); foreach (const QStringList& item, d->orderingCache) { sessionBuildItems.append( item ); } KConfigGroup sessionBuildSetConfig = session->config()->group( "Buildset" ); sessionBuildSetConfig.writeEntry("BuildItems", KDevelop::qvariantToString( QVariant( sessionBuildItems ) )); sessionBuildSetConfig.sync(); } int ProjectBuildSetModel::findInsertionPlace( const QStringList& itemPath ) { /* * The ordering cache list is a superset of the build set, and must be ordered in the same way. * Example: * (items) A - B ----- D --------- G * (orderingCache) A - B - C - D - E - F - G * * We scan orderingCache until we find the required item (absent in items: say, F). * In process of scanning we synchronize position in orderingCache with position in items; * so, when we reach F, we have D as last synchronization point and hence return it * as the insertion place (actually, we return the next item's index - here, index of G). * * If an item cannot be found in the ordering list, we append it to the list. */ int insertionIndex = 0; bool found = false; QList::iterator orderingCacheIterator = d->orderingCache.begin(); // Points to the item which is next to last synchronization point. QList::iterator nextItemIterator = d->items.begin(); while (orderingCacheIterator != d->orderingCache.end()) { if( itemPath == *orderingCacheIterator ) { found = true; break; } if (nextItemIterator != d->items.end() && nextItemIterator->itemPath() == *orderingCacheIterator ) { ++insertionIndex; ++nextItemIterator; } ++orderingCacheIterator; } // while if( !found ) { d->orderingCache.append(itemPath); } Q_ASSERT( insertionIndex >= 0 && insertionIndex <= d->items.size() ); return insertionIndex; } void ProjectBuildSetModel::removeItemsWithCache( const QList& itemIndices ) { /* * Removes the items with given indices from both the build set and the ordering cache. * List is given since removing many items together is more efficient than by one. * * Indices in the list shall be sorted. */ QList itemIndicesCopy = itemIndices; beginRemoveRows( QModelIndex(), itemIndices.first(), itemIndices.last() ); for (QList::iterator cacheIterator = d->orderingCache.end() - 1; cacheIterator >= d->orderingCache.begin() && !itemIndicesCopy.isEmpty();) { int index = itemIndicesCopy.back(); Q_ASSERT( index >= 0 && index < d->items.size() ); if (*cacheIterator == d->items.at(index).itemPath()) { cacheIterator = d->orderingCache.erase(cacheIterator); d->items.removeAt(index); itemIndicesCopy.removeLast(); } --cacheIterator; } // for endRemoveRows(); Q_ASSERT( itemIndicesCopy.isEmpty() ); } void ProjectBuildSetModel::insertItemWithCache( const BuildItem& item ) { int insertionPlace = findInsertionPlace( item.itemPath() ); beginInsertRows( QModelIndex(), insertionPlace, insertionPlace ); d->items.insert(insertionPlace, item); endInsertRows(); } void ProjectBuildSetModel::insertItemsOverrideCache( int index, const QList< BuildItem >& items ) { Q_ASSERT( index >= 0 && index <= d->items.size() ); if (index == d->items.size()) { beginInsertRows( QModelIndex(), index, index + items.size() - 1 ); d->items.append(items); + d->orderingCache.reserve(d->orderingCache.size() + items.size()); foreach( const BuildItem& item, items ) { d->orderingCache.append(item.itemPath()); } endInsertRows(); } else { int indexInCache = d->orderingCache.indexOf(d->items.at(index).itemPath()); Q_ASSERT( indexInCache >= 0 ); beginInsertRows( QModelIndex(), index, index + items.size() - 1 ); for( int i = 0; i < items.size(); ++i ) { const BuildItem& item = items.at( i ); d->items.insert(index + i, item); d->orderingCache.insert(indexInCache + i, item.itemPath()); } endInsertRows(); } } QVariant ProjectBuildSetModel::data( const QModelIndex& idx, int role ) const { if( !idx.isValid() || idx.row() < 0 || idx.column() < 0 || idx.row() >= rowCount() || idx.column() >= columnCount()) { return QVariant(); } if(role == Qt::DisplayRole) { switch( idx.column() ) { case 0: return d->items.at(idx.row()).itemName(); break; case 1: return KDevelop::joinWithEscaping(d->items.at(idx.row()).itemPath(), QLatin1Char('/'), QLatin1Char('\\')); break; } } else if(role == Qt::DecorationRole && idx.column()==0) { KDevelop::ProjectBaseItem* item = d->items.at(idx.row()).findItem(); if( item ) { return QIcon::fromTheme( item->iconName() ); } } return QVariant(); } QVariant ProjectBuildSetModel::headerData( int section, Qt::Orientation orientation, int role ) const { if( section < 0 || section >= columnCount() || orientation != Qt::Horizontal || role != Qt::DisplayRole ) return QVariant(); switch( section ) { case 0: return i18nc("@title:column buildset item name", "Name"); break; case 1: return i18nc("@title:column buildset item path", "Path"); break; } return QVariant(); } int ProjectBuildSetModel::rowCount( const QModelIndex& parent ) const { if( parent.isValid() ) return 0; return d->items.count(); } int ProjectBuildSetModel::columnCount( const QModelIndex& parent ) const { if( parent.isValid() ) return 0; return 2; } void ProjectBuildSetModel::addProjectItem( KDevelop::ProjectBaseItem* item ) { BuildItem buildItem( item ); if (d->items.contains(buildItem)) return; insertItemWithCache( buildItem ); } bool ProjectBuildSetModel::removeRows( int row, int count, const QModelIndex& parent ) { if( parent.isValid() || row > rowCount() || row < 0 || (row+count) > rowCount() || count <= 0 ) return false; QList itemsToRemove; itemsToRemove.reserve(count); for( int i = row; i < row+count; i++ ) { itemsToRemove.append( i ); } removeItemsWithCache( itemsToRemove ); return true; } QList ProjectBuildSetModel::items() { return d->items; } void ProjectBuildSetModel::projectClosed( KDevelop::IProject* project ) { for (int i = d->items.count() - 1; i >= 0; --i) { if (d->items.at(i).itemProject() == project->name()) { beginRemoveRows( QModelIndex(), i, i ); d->items.removeAt(i); endRemoveRows(); } } } void ProjectBuildSetModel::saveToProject( KDevelop::IProject* project ) const { QVariantList paths; foreach (const BuildItem& item, d->items) { if( item.itemProject() == project->name() ) paths.append(item.itemPath()); } KConfigGroup base = project->projectConfiguration()->group("Buildset"); base.writeEntry("BuildItems", KDevelop::qvariantToString( QVariant( paths ) )); base.sync(); } void ProjectBuildSetModel::loadFromProject( KDevelop::IProject* project ) { KConfigGroup base = project->projectConfiguration()->group("Buildset"); if (base.hasKey("BuildItems")) { QVariantList items = KDevelop::stringToQVariant(base.readEntry("BuildItems", QString())).toList(); foreach(const QVariant& path, items) { insertItemWithCache( BuildItem( path.toStringList() ) ); } } else { // Add project to buildset, but only if there is no configuration for this project yet. addProjectItem( project->projectItem() ); } } void ProjectBuildSetModel::moveRowsDown(int row, int count) { QList items = d->items.mid(row, count); removeRows( row, count ); insertItemsOverrideCache( row + 1, items ); } void ProjectBuildSetModel::moveRowsToBottom(int row, int count) { QList items = d->items.mid(row, count); removeRows( row, count ); insertItemsOverrideCache( rowCount(), items ); } void ProjectBuildSetModel::moveRowsUp(int row, int count) { QList items = d->items.mid(row, count); removeRows( row, count ); insertItemsOverrideCache( row - 1, items ); } void ProjectBuildSetModel::moveRowsToTop(int row, int count) { QList items = d->items.mid(row, count); removeRows( row, count ); insertItemsOverrideCache( 0, items ); } } diff --git a/kdevplatform/project/projectchangesmodel.cpp b/kdevplatform/project/projectchangesmodel.cpp index 7292615e2b..57ced0f71a 100644 --- a/kdevplatform/project/projectchangesmodel.cpp +++ b/kdevplatform/project/projectchangesmodel.cpp @@ -1,286 +1,286 @@ /* This file is part of KDevelop Copyright 2010 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectchangesmodel.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KDevelop::IProject*) using namespace KDevelop; ProjectChangesModel::ProjectChangesModel(QObject* parent) : VcsFileChangesModel(parent) { foreach(IProject* p, ICore::self()->projectController()->projects()) addProject(p); connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, &ProjectChangesModel::addProject); connect(ICore::self()->projectController(), &IProjectController::projectClosing, this, &ProjectChangesModel::removeProject); connect(ICore::self()->documentController(), &IDocumentController::documentSaved, this, &ProjectChangesModel::documentSaved); connect(ICore::self()->projectController()->projectModel(), &ProjectModel::rowsInserted, this, &ProjectChangesModel::itemsAdded); connect(ICore::self()->runController(), &IRunController::jobUnregistered, this, &ProjectChangesModel::jobUnregistered); } ProjectChangesModel::~ProjectChangesModel() {} void ProjectChangesModel::addProject(IProject* p) { QStandardItem* it = new QStandardItem(p->name()); it->setData(p->name(), ProjectChangesModel::ProjectNameRole); IPlugin* plugin = p->versionControlPlugin(); if(plugin) { IBasicVersionControl* vcs = plugin->extension(); auto info = ICore::self()->pluginController()->pluginInfo(plugin); it->setIcon(QIcon::fromTheme(info.iconName())); it->setToolTip(vcs->name()); IBranchingVersionControl* branchingExtension = plugin->extension(); if(branchingExtension) { const auto pathUrl = p->path().toUrl(); branchingExtension->registerRepositoryForCurrentBranchChanges(pathUrl); // can't use new signal slot syntax here, IBranchingVersionControl is not a QObject connect(plugin, SIGNAL(repositoryBranchChanged(QUrl)), this, SLOT(repositoryBranchChanged(QUrl))); repositoryBranchChanged(pathUrl); } else reload(QList() << p); } else { it->setEnabled(false); } appendRow(it); } void ProjectChangesModel::removeProject(IProject* p) { QStandardItem* it=projectItem(p); removeRow(it->row()); } QStandardItem* findItemChild(QStandardItem* parent, const QVariant& value, int role = Qt::DisplayRole) { for(int i=0; irowCount(); i++) { QStandardItem* curr=parent->child(i); if(curr->data(role) == value) return curr; } return nullptr; } QStandardItem* ProjectChangesModel::projectItem(IProject* p) const { return findItemChild(invisibleRootItem(), p->name(), ProjectChangesModel::ProjectNameRole); } void ProjectChangesModel::updateState(IProject* p, const KDevelop::VcsStatusInfo& status) { QStandardItem* pItem = projectItem(p); Q_ASSERT(pItem); VcsFileChangesModel::updateState(pItem, status); } void ProjectChangesModel::changes(IProject* project, const QList& urls, IBasicVersionControl::RecursionMode mode) { IPlugin* vcsplugin=project->versionControlPlugin(); IBasicVersionControl* vcs = vcsplugin ? vcsplugin->extension() : nullptr; if(vcs && vcs->isVersionControlled(urls.first())) { //TODO: filter? VcsJob* job=vcs->status(urls, mode); job->setProperty("urls", qVariantFromValue>(urls)); job->setProperty("mode", qVariantFromValue(mode)); job->setProperty("project", qVariantFromValue(project)); connect(job, &VcsJob::finished, this, &ProjectChangesModel::statusReady); ICore::self()->runController()->registerJob(job); } } void ProjectChangesModel::statusReady(KJob* job) { VcsJob* status=static_cast(job); QList states = status->fetchResults().toList(); IProject* project = job->property("project").value(); if(!project) return; QSet foundUrls; foundUrls.reserve(states.size()); foreach(const QVariant& state, states) { const VcsStatusInfo st = state.value(); foundUrls += st.url(); updateState(project, st); } QStandardItem* itProject = projectItem(project); if (!itProject) { qCDebug(PROJECT) << "Project no longer listed in model:" << project->name() << "- skipping update"; return; } IBasicVersionControl::RecursionMode mode = IBasicVersionControl::RecursionMode(job->property("mode").toInt()); QSet uncertainUrls = urls(itProject).toSet().subtract(foundUrls); QList sourceUrls = job->property("urls").value>(); foreach(const QUrl& url, sourceUrls) { if(url.isLocalFile() && QDir(url.toLocalFile()).exists()) { foreach(const QUrl& currentUrl, uncertainUrls) { if((mode == IBasicVersionControl::NonRecursive && currentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) == url.adjusted(QUrl::StripTrailingSlash)) || (mode == IBasicVersionControl::Recursive && url.isParentOf(currentUrl)) ) { removeUrl(currentUrl); } } } } } void ProjectChangesModel::documentSaved(KDevelop::IDocument* document) { reload({document->url()}); } void ProjectChangesModel::itemsAdded(const QModelIndex& parent, int start, int end) { ProjectModel* model=ICore::self()->projectController()->projectModel(); ProjectBaseItem* item=model->itemFromIndex(parent); if(!item) return; IProject* project=item->project(); if(!project) return; QList urls; for(int i=start; iindex(i, 0, parent); item=model->itemFromIndex(idx); if(item->type()==ProjectBaseItem::File || item->type()==ProjectBaseItem::Folder || item->type()==ProjectBaseItem::BuildFolder) urls += item->path().toUrl(); } if(!urls.isEmpty()) changes(project, urls, KDevelop::IBasicVersionControl::NonRecursive); } void ProjectChangesModel::reload(const QList& projects) { foreach(IProject* project, projects) changes(project, {project->path().toUrl()}, KDevelop::IBasicVersionControl::Recursive); } void ProjectChangesModel::reload(const QList& urls) { foreach(const QUrl& url, urls) { IProject* project=ICore::self()->projectController()->findProjectForUrl(url); if (project) { // FIXME: merge multiple urls of the same project changes(project, {url}, KDevelop::IBasicVersionControl::NonRecursive); } } } void ProjectChangesModel::reloadAll() { QList< IProject* > projects = ICore::self()->projectController()->projects(); reload(projects); } void ProjectChangesModel::jobUnregistered(KJob* job) { - static QList readOnly = QList() - << KDevelop::VcsJob::Add - << KDevelop::VcsJob::Remove - << KDevelop::VcsJob::Pull - << KDevelop::VcsJob::Commit - << KDevelop::VcsJob::Move - << KDevelop::VcsJob::Copy - << KDevelop::VcsJob::Revert - ; - + static const QList readOnly = QList{ + KDevelop::VcsJob::Add, + KDevelop::VcsJob::Remove, + KDevelop::VcsJob::Pull, + KDevelop::VcsJob::Commit, + KDevelop::VcsJob::Move, + KDevelop::VcsJob::Copy, + KDevelop::VcsJob::Revert, + }; + VcsJob* vcsjob=dynamic_cast(job); if(vcsjob && readOnly.contains(vcsjob->type())) { reloadAll(); } } void ProjectChangesModel::repositoryBranchChanged(const QUrl& url) { IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project) { IPlugin* v = project->versionControlPlugin(); Q_ASSERT(v); IBranchingVersionControl* branching = v->extension(); Q_ASSERT(branching); VcsJob* job = branching->currentBranch(url); connect(job, &VcsJob::resultsReady, this, &ProjectChangesModel::branchNameReady); job->setProperty("project", QVariant::fromValue(project)); ICore::self()->runController()->registerJob(job); } } void ProjectChangesModel::branchNameReady(VcsJob* job) { IProject* project = qobject_cast(job->property("project").value()); if(job->status()==VcsJob::JobSucceeded) { QString name = job->fetchResults().toString(); QString branchName = name.isEmpty() ? i18n("no branch") : name; projectItem(project)->setText(i18nc("project name (branch name)", "%1 (%2)", project->name(), branchName)); } else { projectItem(project)->setText(project->name()); } reload(QList() << project); } diff --git a/kdevplatform/serialization/itemrepository.h b/kdevplatform/serialization/itemrepository.h index e42903d70b..96e9f30ef2 100644 --- a/kdevplatform/serialization/itemrepository.h +++ b/kdevplatform/serialization/itemrepository.h @@ -1,2248 +1,2248 @@ /* 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_ITEMREPOSITORY_H #define KDEVPLATFORM_ITEMREPOSITORY_H #include #include #include #include #include #include "referencecounting.h" #include "abstractitemrepository.h" #include "repositorymanager.h" #include "itemrepositoryregistry.h" //#define DEBUG_MONSTERBUCKETS // #define DEBUG_ITEMREPOSITORY_LOADING // #define ifDebugInfiniteRecursion(x) x #define ifDebugInfiniteRecursion(x) // #define ifDebugLostSpace(x) x #define ifDebugLostSpace(x) // #define DEBUG_INCORRECT_DELETE //Makes sure that all items stay reachable through the basic hash // #define DEBUG_ITEM_REACHABILITY ///@todo Dynamic bucket hash size #ifdef DEBUG_ITEM_REACHABILITY #define ENSURE_REACHABLE(bucket) Q_ASSERT(allItemsReachable(bucket)); #define IF_ENSURE_REACHABLE(x) x #else #define ENSURE_REACHABLE(bucket) #define IF_ENSURE_REACHABLE(x) #endif #define ITEMREPOSITORY_USE_MMAP_LOADING //Assertion macro that prevents warnings if debugging is disabled //Only use it to verify values, it should not call any functions, since else the function will even be called in release mode #ifdef QT_NO_DEBUG #define VERIFY(X) if(!(X)) {qWarning() << "Failed to verify expression" << #X;} #else #define VERIFY(X) Q_ASSERT(X) #endif ///When this is uncommented, a 64-bit test-value is written behind the area an item is allowed to write into before ///createItem(..) is called, and an assertion triggers when it was changed during createItem(), which means createItem wrote too long. ///The problem: This temporarily overwrites valid data in the following item, so it will cause serious problems if that data is accessed ///during the call to createItem(). // #define DEBUG_WRITING_EXTENTS class TestItemRepository; namespace KDevelop { /** * This file implements a generic bucket-based indexing repository, that can be used for example to index strings. * * All you need to do is define your item type that you want to store into the repository, and create a request item * similar to ExampleItemRequest that compares and fills the defined item type. * * For example the string repository uses "unsigned short" as item-type, uses that actual value to store the length of the string, * and uses the space behind to store the actual string content. * * @see AbstractItemRepository * @see ItemRepository * * @see ExampleItem * @see ExampleItemRequest * * @see typerepository.h * @see stringrepository.h * @see indexedstring.h */ enum { ItemRepositoryBucketSize = 1<<16, ItemRepositoryBucketLimit = 1<<16 }; /** * Buckets are the memory-units that are used to store the data in an ItemRepository. * * About monster buckets: Normally a bucket has a size of 64kb, but when an item is * allocated that is larger than that, a "monster bucket" is allocated, which spans the * space of multiple buckets. */ template class Bucket { public: enum { AdditionalSpacePerItem = 2 }; enum { ObjectMapSize = ((ItemRepositoryBucketSize / ItemRequest::AverageSize) * 3) / 2 + 1, MaxFreeItemsForHide = 0, //When less than this count of free items in one buckets is reached, the bucket is removed from the global list of buckets with free items MaxFreeSizeForHide = fixedItemSize ? fixedItemSize : 0, //Only when the largest free size is smaller then this, the bucket is taken from the free list MinFreeItemsForReuse = 10,//When this count of free items in one bucket is reached, consider re-assigning them to new requests MinFreeSizeForReuse = ItemRepositoryBucketSize/20 //When the largest free item is bigger then this, the bucket is automatically added to the free list }; enum { NextBucketHashSize = ObjectMapSize, //Affects the average count of bucket-chains that need to be walked in ItemRepository::index. Must be a multiple of ObjectMapSize DataSize = sizeof(char) + sizeof(unsigned int) * 3 + ItemRepositoryBucketSize + sizeof(short unsigned int) * (ObjectMapSize + NextBucketHashSize + 1) }; enum { CheckStart = 0xff00ff1, CheckEnd = 0xfafcfb }; Bucket() { } ~Bucket() { if(m_data != m_mappedData) { delete[] m_data; delete[] m_nextBucketHash; delete[] m_objectMap; } } void initialize(int monsterBucketExtent) { if(!m_data) { m_monsterBucketExtent = monsterBucketExtent; m_available = ItemRepositoryBucketSize; m_data = new char[ItemRepositoryBucketSize + monsterBucketExtent * DataSize]; #ifndef QT_NO_DEBUG memset(m_data, 0, (ItemRepositoryBucketSize + monsterBucketExtent * DataSize) * sizeof(char)); #endif //The bigger we make the map, the lower the probability of a clash(and thus bad performance). However it increases memory usage. m_objectMap = new short unsigned int[ObjectMapSize]; memset(m_objectMap, 0, ObjectMapSize * sizeof(short unsigned int)); m_nextBucketHash = new short unsigned int[NextBucketHashSize]; memset(m_nextBucketHash, 0, NextBucketHashSize * sizeof(short unsigned int)); m_changed = true; m_dirty = false; m_lastUsed = 0; } } template void readValue(char*& from, T& to) { to = *reinterpret_cast(from); from += sizeof(T); } void initializeFromMap(char* current) { if(!m_data) { char* start = current; readValue(current, m_monsterBucketExtent); Q_ASSERT(current - start == 4); readValue(current, m_available); m_objectMap = reinterpret_cast(current); current += sizeof(short unsigned int) * ObjectMapSize; m_nextBucketHash = reinterpret_cast(current); current += sizeof(short unsigned int) * NextBucketHashSize; readValue(current, m_largestFreeItem); readValue(current, m_freeItemCount); readValue(current, m_dirty); m_data = current; m_mappedData = current; m_changed = false; m_lastUsed = 0; VERIFY(current - start == (DataSize - ItemRepositoryBucketSize)); } } void store(QFile* file, size_t offset) { if(!m_data) return; if(static_cast(file->size()) < offset + (1+m_monsterBucketExtent)*DataSize) file->resize(offset + (1+m_monsterBucketExtent)*DataSize); file->seek(offset); file->write((char*)&m_monsterBucketExtent, sizeof(unsigned int)); file->write((char*)&m_available, sizeof(unsigned int)); file->write((char*)m_objectMap, sizeof(short unsigned int) * ObjectMapSize); file->write((char*)m_nextBucketHash, sizeof(short unsigned int) * NextBucketHashSize); file->write((char*)&m_largestFreeItem, sizeof(short unsigned int)); file->write((char*)&m_freeItemCount, sizeof(unsigned int)); file->write((char*)&m_dirty, sizeof(bool)); file->write(m_data, ItemRepositoryBucketSize + m_monsterBucketExtent * DataSize); if(static_cast(file->pos()) != offset + (1+m_monsterBucketExtent)*DataSize) { KMessageBox::error(nullptr, i18n("Failed writing to %1, probably the disk is full", file->fileName())); abort(); } m_changed = false; #ifdef DEBUG_ITEMREPOSITORY_LOADING { file->flush(); file->seek(offset); uint available, freeItemCount, monsterBucketExtent; short unsigned int largestFree; bool dirty; short unsigned int* m = new short unsigned int[ObjectMapSize]; short unsigned int* h = new short unsigned int[NextBucketHashSize]; file->read((char*)&monsterBucketExtent, sizeof(unsigned int)); char* d = new char[ItemRepositoryBucketSize + monsterBucketExtent * DataSize]; file->read((char*)&available, sizeof(unsigned int)); file->read((char*)m, sizeof(short unsigned int) * ObjectMapSize); file->read((char*)h, sizeof(short unsigned int) * NextBucketHashSize); file->read((char*)&largestFree, sizeof(short unsigned int)); file->read((char*)&freeItemCount, sizeof(unsigned int)); file->read((char*)&dirty, sizeof(bool)); file->read(d, ItemRepositoryBucketSize); Q_ASSERT(monsterBucketExtent == m_monsterBucketExtent); Q_ASSERT(available == m_available); Q_ASSERT(memcmp(d, m_data, ItemRepositoryBucketSize + monsterBucketExtent * DataSize) == 0); Q_ASSERT(memcmp(m, m_objectMap, sizeof(short unsigned int) * ObjectMapSize) == 0); Q_ASSERT(memcmp(h, m_nextBucketHash, sizeof(short unsigned int) * NextBucketHashSize) == 0); Q_ASSERT(m_largestFreeItem == largestFree); Q_ASSERT(m_freeItemCount == freeItemCount); Q_ASSERT(m_dirty == dirty); Q_ASSERT(static_cast(file->pos()) == offset + DataSize + m_monsterBucketExtent * DataSize); delete[] d; delete[] m; delete[] h; } #endif } inline char* data() { return m_data; } inline uint dataSize() const { return ItemRepositoryBucketSize + m_monsterBucketExtent * DataSize; } //Tries to find the index this item has in this bucket, or returns zero if the item isn't there yet. unsigned short findIndex(const ItemRequest& request) const { m_lastUsed = 0; unsigned short localHash = request.hash() % ObjectMapSize; unsigned short index = m_objectMap[localHash]; unsigned short follower = 0; //Walk the chain of items with the same local hash while(index && (follower = followerIndex(index)) && !(request.equals(itemFromIndex(index)))) index = follower; if(index && request.equals(itemFromIndex(index))) { return index; //We have found the item } return 0; } //Tries to get the index within this bucket, or returns zero. Will put the item into the bucket if there is room. //Created indices will never begin with 0xffff____, so you can use that index-range for own purposes. unsigned short index(const ItemRequest& request, unsigned int itemSize) { m_lastUsed = 0; unsigned short localHash = request.hash() % ObjectMapSize; unsigned short index = m_objectMap[localHash]; unsigned short insertedAt = 0; unsigned short follower = 0; //Walk the chain of items with the same local hash while(index && (follower = followerIndex(index)) && !(request.equals(itemFromIndex(index)))) index = follower; if(index && request.equals(itemFromIndex(index))) return index; //We have found the item ifDebugLostSpace( Q_ASSERT(!lostSpace()); ) prepareChange(); unsigned int totalSize = itemSize + AdditionalSpacePerItem; if(m_monsterBucketExtent) { ///This is a monster-bucket. Other rules are applied here. Only one item can be allocated, and that must be bigger than the bucket data Q_ASSERT(totalSize > ItemRepositoryBucketSize); Q_ASSERT(m_available); m_available = 0; insertedAt = AdditionalSpacePerItem; setFollowerIndex(insertedAt, 0); Q_ASSERT(m_objectMap[localHash] == 0); m_objectMap[localHash] = insertedAt; if(markForReferenceCounting) enableDUChainReferenceCounting(m_data, dataSize()); request.createItem(reinterpret_cast(m_data + insertedAt)); if(markForReferenceCounting) disableDUChainReferenceCounting(m_data); return insertedAt; } //The second condition is needed, else we can get problems with zero-length items and an overflow in insertedAt to zero if(totalSize > m_available || (!itemSize && totalSize == m_available)) { //Try finding the smallest freed item that can hold the data unsigned short currentIndex = m_largestFreeItem; unsigned short previousIndex = 0; unsigned short freeChunkSize = 0; ///@todo Achieve this without full iteration while(currentIndex && freeSize(currentIndex) > itemSize) { unsigned short follower = followerIndex(currentIndex); if(follower && freeSize(follower) >= itemSize) { //The item also fits into the smaller follower, so use that one previousIndex = currentIndex; currentIndex = follower; }else{ //The item fits into currentIndex, but not into the follower. So use currentIndex freeChunkSize = freeSize(currentIndex) - itemSize; //We need 2 bytes to store the free size if(freeChunkSize != 0 && freeChunkSize < AdditionalSpacePerItem+2) { //we can not manage the resulting free chunk as a separate item, so we cannot use this position. //Just pick the biggest free item, because there we can be sure that //either we can manage the split, or we cannot do anything at all in this bucket. freeChunkSize = freeSize(m_largestFreeItem) - itemSize; if(freeChunkSize == 0 || freeChunkSize >= AdditionalSpacePerItem+2) { previousIndex = 0; currentIndex = m_largestFreeItem; }else{ currentIndex = 0; } } break; } } if(!currentIndex || freeSize(currentIndex) < (totalSize-AdditionalSpacePerItem)) return 0; if(previousIndex) setFollowerIndex(previousIndex, followerIndex(currentIndex)); else m_largestFreeItem = followerIndex(currentIndex); --m_freeItemCount; //Took one free item out of the chain ifDebugLostSpace( Q_ASSERT((uint)lostSpace() == (uint)(freeSize(currentIndex) + AdditionalSpacePerItem)); ) if(freeChunkSize) { Q_ASSERT(freeChunkSize >= AdditionalSpacePerItem+2); unsigned short freeItemSize = freeChunkSize - AdditionalSpacePerItem; unsigned short freeItemPosition; //Insert the resulting free chunk into the list of free items, so we don't lose it if(isBehindFreeSpace(currentIndex)) { //Create the free item at the beginning of currentIndex, so it can be merged with the free space in front freeItemPosition = currentIndex; currentIndex += freeItemSize + AdditionalSpacePerItem; }else{ //Create the free item behind currentIndex freeItemPosition = currentIndex + itemSize + AdditionalSpacePerItem; } setFreeSize(freeItemPosition, freeItemSize); insertFreeItem(freeItemPosition); } insertedAt = currentIndex; Q_ASSERT((bool)m_freeItemCount == (bool)m_largestFreeItem); }else{ //We have to insert the item insertedAt = ItemRepositoryBucketSize - m_available; insertedAt += AdditionalSpacePerItem; //Room for the prepended follower-index m_available -= totalSize; } ifDebugLostSpace( Q_ASSERT(lostSpace() == totalSize); ) Q_ASSERT(!index || !followerIndex(index)); Q_ASSERT(!m_objectMap[localHash] || index); if(index) setFollowerIndex(index, insertedAt); setFollowerIndex(insertedAt, 0); if(m_objectMap[localHash] == 0) m_objectMap[localHash] = insertedAt; #ifdef DEBUG_CREATEITEM_EXTENTS char* borderBehind = m_data + insertedAt + (totalSize-AdditionalSpacePerItem); quint64 oldValueBehind = 0; if(m_available >= 8) { oldValueBehind = *(quint64*)borderBehind; *((quint64*)borderBehind) = 0xfafafafafafafafaLLU; } #endif //Last thing we do, because createItem may recursively do even more transformation of the repository if(markForReferenceCounting) enableDUChainReferenceCounting(m_data, dataSize()); request.createItem(reinterpret_cast(m_data + insertedAt)); if(markForReferenceCounting) disableDUChainReferenceCounting(m_data); #ifdef DEBUG_CREATEITEM_EXTENTS if(m_available >= 8) { //If this assertion triggers, then the item writes a bigger range than it advertised in Q_ASSERT(*((quint64*)borderBehind) == 0xfafafafafafafafaLLU); *((quint64*)borderBehind) = oldValueBehind; } #endif Q_ASSERT(itemFromIndex(insertedAt)->hash() == request.hash()); Q_ASSERT(itemFromIndex(insertedAt)->itemSize() == itemSize); ifDebugLostSpace( if(lostSpace()) qDebug() << "lost space:" << lostSpace(); Q_ASSERT(!lostSpace()); ) return insertedAt; } /// @param modulo Returns whether this bucket contains an item with (hash % modulo) == (item.hash % modulo) /// The default-parameter is the size of the next-bucket hash that is used by setNextBucketForHash and nextBucketForHash /// @note modulo MUST be a multiple of ObjectMapSize, because (b-a) | (x * h1) => (b-a) | h2, where a|b means a is a multiple of b. /// This this allows efficiently computing the clashes using the local object map hash. bool hasClashingItem(uint hash, uint modulo) { Q_ASSERT(modulo % ObjectMapSize == 0); m_lastUsed = 0; uint hashMod = hash % modulo; unsigned short localHash = hash % ObjectMapSize; unsigned short currentIndex = m_objectMap[localHash]; if(currentIndex == 0) return false; while(currentIndex) { uint currentHash = itemFromIndex(currentIndex)->hash(); Q_ASSERT(currentHash % ObjectMapSize == localHash); if(currentHash % modulo == hashMod) return true; //Clash currentIndex = followerIndex(currentIndex); } return false; } void countFollowerIndexLengths(uint& usedSlots, uint& lengths, uint& slotCount, uint& longestInBucketFollowerChain) { for(uint a = 0; a < ObjectMapSize; ++a) { unsigned short currentIndex = m_objectMap[a]; ++slotCount; uint length = 0; if(currentIndex) { ++usedSlots; while(currentIndex) { ++length; ++lengths; currentIndex = followerIndex(currentIndex); } if(length > longestInBucketFollowerChain) { // qDebug() << "follower-chain at" << a << ":" << length; longestInBucketFollowerChain = length; } } } } //Returns whether the given item is reachabe within this bucket, through its hash. bool itemReachable(const Item* item, uint hash) const { unsigned short localHash = hash % ObjectMapSize; unsigned short currentIndex = m_objectMap[localHash]; while(currentIndex) { if(itemFromIndex(currentIndex) == item) return true; currentIndex = followerIndex(currentIndex); } return false; } template void deleteItem(unsigned short index, unsigned int hash, Repository& repository) { ifDebugLostSpace( Q_ASSERT(!lostSpace()); ) m_lastUsed = 0; prepareChange(); unsigned int size = itemFromIndex(index)->itemSize(); //Step 1: Remove the item from the data-structures that allow finding it: m_objectMap unsigned short localHash = hash % ObjectMapSize; unsigned short currentIndex = m_objectMap[localHash]; unsigned short previousIndex = 0; //Fix the follower-link by setting the follower of the previous item to the next one, or updating m_objectMap while(currentIndex != index) { previousIndex = currentIndex; currentIndex = followerIndex(currentIndex); //If this assertion triggers, the deleted item was not registered under the given hash Q_ASSERT(currentIndex); } Q_ASSERT(currentIndex == index); if(!previousIndex) //The item was directly in the object map m_objectMap[localHash] = followerIndex(index); else setFollowerIndex(previousIndex, followerIndex(index)); Item* item = const_cast(itemFromIndex(index)); if(markForReferenceCounting) enableDUChainReferenceCounting(m_data, dataSize()); ItemRequest::destroy(item, repository); if(markForReferenceCounting) disableDUChainReferenceCounting(m_data); #ifndef QT_NO_DEBUG #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wclass-memaccess" #endif memset(item, 0, size); //For debugging, so we notice the data is wrong #if defined(__GNUC__) && !defined(__INTEL_COMPILER) && (((__GNUC__ * 100) + __GNUC_MINOR__) >= 800) #pragma GCC diagnostic pop #endif #endif if(m_monsterBucketExtent) { ///This is a monster-bucket. Make it completely empty again. Q_ASSERT(!m_available); m_available = ItemRepositoryBucketSize; //Items are always inserted into monster-buckets at a fixed position Q_ASSERT(currentIndex == AdditionalSpacePerItem); Q_ASSERT(m_objectMap[localHash] == 0); }else{ ///Put the space into the free-set setFreeSize(index, size); //Try merging the created free item to other free items around, else add it into the free list insertFreeItem(index); if(m_freeItemCount == 1 && freeSize(m_largestFreeItem) + m_available == ItemRepositoryBucketSize) { //Everything has been deleted, there is only free space left. Make the bucket empty again, //so it can later also be used as a monster-bucket. m_available = ItemRepositoryBucketSize; m_freeItemCount = 0; m_largestFreeItem = 0; } } Q_ASSERT((bool)m_freeItemCount == (bool)m_largestFreeItem); ifDebugLostSpace( Q_ASSERT(!lostSpace()); ) #ifdef DEBUG_INCORRECT_DELETE //Make sure the item cannot be found any more { unsigned short localHash = hash % ObjectMapSize; unsigned short currentIndex = m_objectMap[localHash]; while(currentIndex && currentIndex != index) { previousIndex = currentIndex; currentIndex = followerIndex(currentIndex); } Q_ASSERT(!currentIndex); //The item must not be found } #endif // Q_ASSERT(canAllocateItem(size)); } ///@warning The returned item may be in write-protected memory, so never try doing a const_cast and changing some data /// If you need to change something, use dynamicItemFromIndex ///@warning When using multi-threading, mutex() must be locked as long as you use the returned data inline const Item* itemFromIndex(unsigned short index) const { m_lastUsed = 0; return reinterpret_cast(m_data+index); } bool isEmpty() const { return m_available == ItemRepositoryBucketSize; } ///Returns true if this bucket has no nextBucketForHash links bool noNextBuckets() const { for(int a = 0; a < NextBucketHashSize; ++a) if(m_nextBucketHash[a]) return false; return true; } uint available() const { return m_available; } uint usedMemory() const { return ItemRepositoryBucketSize - m_available; } template bool visitAllItems(Visitor& visitor) const { m_lastUsed = 0; for(uint a = 0; a < ObjectMapSize; ++a) { uint currentIndex = m_objectMap[a]; while(currentIndex) { //Get the follower early, so there is no problems when the current //index is removed if(!visitor(reinterpret_cast(m_data+currentIndex))) return false; currentIndex = followerIndex(currentIndex); } } return true; } ///Returns whether something was changed template int finalCleanup(Repository& repository) { int changed = 0; while(m_dirty) { m_dirty = false; for(uint a = 0; a < ObjectMapSize; ++a) { uint currentIndex = m_objectMap[a]; while(currentIndex) { //Get the follower early, so there is no problems when the current //index is removed const Item* item = reinterpret_cast(m_data+currentIndex); if(!ItemRequest::persistent(item)) { changed += item->itemSize(); deleteItem(currentIndex, item->hash(), repository); m_dirty = true; //Set to dirty so we re-iterate break; } currentIndex = followerIndex(currentIndex); } } } return changed; } unsigned short nextBucketForHash(uint hash) const { m_lastUsed = 0; return m_nextBucketHash[hash % NextBucketHashSize]; } void setNextBucketForHash(unsigned int hash, unsigned short bucket) { m_lastUsed = 0; prepareChange(); m_nextBucketHash[hash % NextBucketHashSize] = bucket; } uint freeItemCount() const { return m_freeItemCount; } short unsigned int totalFreeItemsSize() const { short unsigned int ret = 0; short unsigned int currentIndex = m_largestFreeItem; while(currentIndex) { ret += freeSize(currentIndex); currentIndex = followerIndex(currentIndex); } return ret; } //Size of the largest item that could be inserted into this bucket short unsigned int largestFreeSize() const { short unsigned int ret = 0; if(m_largestFreeItem) ret = freeSize(m_largestFreeItem); if(m_available > (uint)(AdditionalSpacePerItem + (uint)ret)) { ret = m_available - AdditionalSpacePerItem; Q_ASSERT(ret == (m_available - AdditionalSpacePerItem)); } return ret; } bool canAllocateItem(unsigned int size) const { short unsigned int currentIndex = m_largestFreeItem; while(currentIndex) { short unsigned int currentFree = freeSize(currentIndex); if(currentFree < size) return false; //Either we need an exact match, or 2 additional bytes to manage the resulting gap if(size == currentFree || currentFree - size >= AdditionalSpacePerItem + 2) return true; currentIndex = followerIndex(currentIndex); } return false; } void tick() const { ++m_lastUsed; } //How many ticks ago the item was last used int lastUsed() const { return m_lastUsed; } //Whether this bucket was changed since it was last stored bool changed() const { return m_changed; } void prepareChange() { m_changed = true; m_dirty = true; makeDataPrivate(); } bool dirty() const { return m_dirty; } ///Returns the count of following buckets that were merged onto this buckets data array int monsterBucketExtent() const { return m_monsterBucketExtent; } //Counts together the space that is neither accessible through m_objectMap nor through the free items uint lostSpace() { if(m_monsterBucketExtent) return 0; uint need = ItemRepositoryBucketSize - m_available; uint found = 0; for(uint a = 0; a < ObjectMapSize; ++a) { uint currentIndex = m_objectMap[a]; while(currentIndex) { found += reinterpret_cast(m_data+currentIndex)->itemSize() + AdditionalSpacePerItem; currentIndex = followerIndex(currentIndex); } } uint currentIndex = m_largestFreeItem; while(currentIndex) { found += freeSize(currentIndex) + AdditionalSpacePerItem; currentIndex = followerIndex(currentIndex); } return need-found; } private: void makeDataPrivate() { if(m_mappedData == m_data) { short unsigned int* oldObjectMap = m_objectMap; short unsigned int* oldNextBucketHash = m_nextBucketHash; m_data = new char[ItemRepositoryBucketSize + m_monsterBucketExtent * DataSize]; m_objectMap = new short unsigned int[ObjectMapSize]; m_nextBucketHash = new short unsigned int[NextBucketHashSize]; memcpy(m_data, m_mappedData, ItemRepositoryBucketSize + m_monsterBucketExtent * DataSize); memcpy(m_objectMap, oldObjectMap, ObjectMapSize * sizeof(short unsigned int)); memcpy(m_nextBucketHash, oldNextBucketHash, NextBucketHashSize * sizeof(short unsigned int)); } } ///Merges the given index item, which must have a freeSize() set, to surrounding free items, and inserts the result. ///The given index itself should not be in the free items chain yet. ///Returns whether the item was inserted somewhere. void insertFreeItem(unsigned short index) { //If the item-size is fixed, we don't need to do any management. Just keep a list of free items. Items of other size will never be requested. if(!fixedItemSize) { unsigned short currentIndex = m_largestFreeItem; unsigned short previousIndex = 0; while(currentIndex) { Q_ASSERT(currentIndex != index); #ifndef QT_NO_DEBUG unsigned short currentFreeSize = freeSize(currentIndex); #endif ///@todo Achieve this without iterating through all items in the bucket(This is very slow) //Merge behind index if(currentIndex == index + freeSize(index) + AdditionalSpacePerItem) { //Remove currentIndex from the free chain, since it's merged backwards into index if(previousIndex && followerIndex(currentIndex)) Q_ASSERT(freeSize(previousIndex) >= freeSize(followerIndex(currentIndex))); if(previousIndex) setFollowerIndex(previousIndex, followerIndex(currentIndex)); else m_largestFreeItem = followerIndex(currentIndex); --m_freeItemCount; //One was removed //currentIndex is directly behind index, touching its space. Merge them. setFreeSize(index, freeSize(index) + AdditionalSpacePerItem + freeSize(currentIndex)); //Recurse to do even more merging insertFreeItem(index); return; } //Merge before index if(index == currentIndex + freeSize(currentIndex) + AdditionalSpacePerItem) { if(previousIndex && followerIndex(currentIndex)) Q_ASSERT(freeSize(previousIndex) >= freeSize(followerIndex(currentIndex))); //Remove currentIndex from the free chain, since insertFreeItem wants //it not to be in the chain, and it will be inserted in another place if(previousIndex) setFollowerIndex(previousIndex, followerIndex(currentIndex)); else m_largestFreeItem = followerIndex(currentIndex); --m_freeItemCount; //One was removed //index is directly behind currentIndex, touching its space. Merge them. setFreeSize(currentIndex, freeSize(currentIndex) + AdditionalSpacePerItem + freeSize(index)); //Recurse to do even more merging insertFreeItem(currentIndex); return; } previousIndex = currentIndex; currentIndex = followerIndex(currentIndex); #ifndef QT_NO_DEBUG if(currentIndex) Q_ASSERT(freeSize(currentIndex) <= currentFreeSize); #endif } } insertToFreeChain(index); } ///Only inserts the item in the correct position into the free chain. index must not be in the chain yet. void insertToFreeChain(unsigned short index) { if(!fixedItemSize) { ///@todo Use some kind of tree to find the correct position in the chain(This is very slow) //Insert the free item into the chain opened by m_largestFreeItem unsigned short currentIndex = m_largestFreeItem; unsigned short previousIndex = 0; unsigned short size = freeSize(index); while(currentIndex && freeSize(currentIndex) > size) { Q_ASSERT(currentIndex != index); //must not be in the chain yet previousIndex = currentIndex; currentIndex = followerIndex(currentIndex); } if(currentIndex) Q_ASSERT(freeSize(currentIndex) <= size); setFollowerIndex(index, currentIndex); if(previousIndex) { Q_ASSERT(freeSize(previousIndex) >= size); setFollowerIndex(previousIndex, index); } else //This item is larger than all already registered free items, or there are none. m_largestFreeItem = index; }else{ Q_ASSERT(freeSize(index) == fixedItemSize); //When all items have the same size, just prepent to the front. setFollowerIndex(index, m_largestFreeItem); m_largestFreeItem = index; } ++m_freeItemCount; } /// Returns true if the given index is right behind free space, and thus can be merged to the free space. bool isBehindFreeSpace(unsigned short index) const { // TODO: Without iteration! unsigned short currentIndex = m_largestFreeItem; while(currentIndex) { if(index == currentIndex + freeSize(currentIndex) + AdditionalSpacePerItem) return true; currentIndex = followerIndex(currentIndex); } return false; } /// @param index the index of an item @return The index of the next item in the chain of items with a same local hash, or zero inline unsigned short followerIndex(unsigned short index) const { Q_ASSERT(index >= 2); return *reinterpret_cast(m_data+(index-2)); } void setFollowerIndex(unsigned short index, unsigned short follower) { Q_ASSERT(index >= 2); *reinterpret_cast(m_data+(index-2)) = follower; } // Only returns the current value if the item is actually free inline unsigned short freeSize(unsigned short index) const { return *reinterpret_cast(m_data+index); } //Convenience function to set the free-size, only for freed items void setFreeSize(unsigned short index, unsigned short size) { *reinterpret_cast(m_data+index) = size; } int m_monsterBucketExtent = 0; //If this is a monster-bucket, this contains the count of follower-buckets that belong to this one unsigned int m_available = 0; char* m_data = nullptr; //Structure of the data: (2 byte), (item.size() byte) char* m_mappedData = nullptr; //Read-only memory-mapped data. If this equals m_data, m_data must not be written short unsigned int* m_objectMap = nullptr; //Points to the first object in m_data with (hash % ObjectMapSize) == index. Points to the item itself, so subtract 1 to get the pointer to the next item with same local hash. short unsigned int m_largestFreeItem = 0; //Points to the largest item that is currently marked as free, or zero. That one points to the next largest one through followerIndex unsigned int m_freeItemCount = 0; unsigned short* m_nextBucketHash = nullptr; bool m_dirty = false; //Whether the data was changed since the last finalCleanup bool m_changed = false; //Whether this bucket was changed since it was last stored to disk mutable int m_lastUsed = 0; //How many ticks ago this bucket was last accessed }; template struct Locker { //This is a dummy that does nothing template explicit Locker(const T& /*t*/) { } }; template<> struct Locker { explicit Locker(QMutex* mutex) : m_mutex(mutex) { m_mutex->lock(); } ~Locker() { m_mutex->unlock(); } QMutex* m_mutex; }; ///This object needs to be kept alive as long as you change the contents of an item ///stored in the repository. It is needed to correctly track the reference counting ///within disk-storage. ///@warning You can not freely copy this around, when you create a copy, the copy source /// becomes invalid template class DynamicItem { public: DynamicItem(Item* i, void* start, uint size) : m_item(i), m_start(start) { if(markForReferenceCounting) enableDUChainReferenceCounting(m_start, size); // qDebug() << "enabling" << i << "to" << (void*)(((char*)i)+size); } ~DynamicItem() { if(m_start) { // qDebug() << "destructor-disabling" << m_item; if(markForReferenceCounting) disableDUChainReferenceCounting(m_start); } } DynamicItem(const DynamicItem& rhs) : m_item(rhs.m_item), m_start(rhs.m_start) { // qDebug() << "stealing" << m_item; Q_ASSERT(rhs.m_start); rhs.m_start = nullptr; } Item* operator->() { return m_item; } Item* m_item; private: mutable void* m_start; DynamicItem& operator=(const DynamicItem&); }; ///@tparam Item See ExampleItem ///@tparam ItemRequest See ExampleReqestItem ///@tparam fixedItemSize When this is true, all inserted items must have the same size. /// This greatly simplifies and speeds up the task of managing free items within the buckets. ///@tparam markForReferenceCounting Whether the data within the repository should be marked for reference-counting. /// This costs a bit of performance, but must be enabled if there may be data in the repository /// that does on-disk reference counting, like IndexedString, IndexedIdentifier, etc. ///@tparam threadSafe Whether class access should be thread-safe. Disabling this is dangerous when you do multi-threading. /// You have to make sure that mutex() is locked whenever the repository is accessed. template class ItemRepository : public AbstractItemRepository { typedef Locker ThisLocker; typedef Bucket MyBucket; enum { //Must be a multiple of Bucket::ObjectMapSize, so Bucket::hasClashingItem can be computed //Must also be a multiple of Bucket::NextBucketHashSize, for the same reason.(Currently those are same) bucketHashSize = (targetBucketHashSize / MyBucket::ObjectMapSize) * MyBucket::ObjectMapSize }; enum { BucketStartOffset = sizeof(uint) * 7 + sizeof(short unsigned int) * bucketHashSize //Position in the data where the bucket array starts }; public: ///@param registry May be zero, then the repository will not be registered at all. Else, the repository will register itself to that registry. /// If this is zero, you have to care about storing the data using store() and/or close() by yourself. It does not happen automatically. /// For the global standard registry, the storing/loading is triggered from within duchain, so you don't need to care about it. explicit ItemRepository(const QString& repositoryName, ItemRepositoryRegistry* registry = &globalItemRepositoryRegistry(), uint repositoryVersion = 1, AbstractRepositoryManager* manager = nullptr) : m_ownMutex(QMutex::Recursive) , m_mutex(&m_ownMutex) , m_repositoryName(repositoryName) , m_registry(registry) , m_file(nullptr) , m_dynamicFile(nullptr) , m_repositoryVersion(repositoryVersion) , m_manager(manager) { m_unloadingEnabled = true; m_metaDataChanged = true; m_buckets.resize(10); m_buckets.fill(nullptr); memset(m_firstBucketForHash, 0, bucketHashSize * sizeof(short unsigned int)); m_statBucketHashClashes = m_statItemCount = 0; m_currentBucket = 1; //Skip the first bucket, we won't use it so we have the zero indices for special purposes if(m_registry) m_registry->registerRepository(this, m_manager); } ~ItemRepository() override { if(m_registry) m_registry->unRegisterRepository(this); close(); } ///Unloading of buckets is enabled by default. Use this to disable it. When unloading is enabled, the data ///gotten from must only itemFromIndex must not be used for a long time. void setUnloadingEnabled(bool enabled) { m_unloadingEnabled = enabled; } ///Returns the index for the given item. If the item is not in the repository yet, it is inserted. ///The index can never be zero. Zero is reserved for your own usage as invalid ///@param request Item to retrieve the index from unsigned int index(const ItemRequest& request) { ThisLocker lock(m_mutex); const uint hash = request.hash(); const uint size = request.itemSize(); // Bucket indexes tracked while walking the bucket chain for this request hash unsigned short bucketInChainWithSpace = 0; unsigned short lastBucketWalked = 0; const ushort foundIndexInBucket = walkBucketChain(hash, [&](ushort bucketIdx, const MyBucket* bucketPtr) { lastBucketWalked = bucketIdx; const ushort found = bucketPtr->findIndex(request); if (!found && !bucketInChainWithSpace && bucketPtr->canAllocateItem(size)) { bucketInChainWithSpace = bucketIdx; } return found; }); if (foundIndexInBucket) { // 'request' is already present, return the existing index return createIndex(lastBucketWalked, foundIndexInBucket); } /* * Disclaimer: Writer of comment != writer of code, believe with caution * * Requested item does not yet exist. Need to find a place to put it... * * First choice is to place it in an existing bucket in the chain for the request hash * Second choice is to find an existing bucket anywhere with enough space * Otherwise use m_currentBucket (the latest unused bucket) * * If the chosen bucket fails to allocate the item, merge buckets to create a monster (dragon?) * * Finally, if the first option failed or the selected bucket failed to allocate, add the * bucket which the item ended up in to the chain of buckets for the request's hash */ m_metaDataChanged = true; const bool pickedBucketInChain = bucketInChainWithSpace; int useBucket = bucketInChainWithSpace; int reOrderFreeSpaceBucketIndex = -1; if (!pickedBucketInChain) { //Try finding an existing bucket with deleted space to store the data into for(int a = 0; a < m_freeSpaceBuckets.size(); ++a) { MyBucket* bucketPtr = bucketForIndex(m_freeSpaceBuckets[a]); Q_ASSERT(bucketPtr->largestFreeSize()); if(bucketPtr->canAllocateItem(size)) { //The item fits into the bucket. useBucket = m_freeSpaceBuckets[a]; reOrderFreeSpaceBucketIndex = a; break; } } if (!useBucket) { useBucket = m_currentBucket; } } else { reOrderFreeSpaceBucketIndex = m_freeSpaceBuckets.indexOf(useBucket); } //The item isn't in the repository yet, find a new bucket for it while(1) { if(useBucket >= m_buckets.size()) { if(m_buckets.size() >= 0xfffe) { //We have reserved the last bucket index 0xffff for special purposes //the repository has overflown. qWarning() << "Found no room for an item in" << m_repositoryName << "size of the item:" << request.itemSize(); return 0; }else{ //Allocate new buckets m_buckets.resize(m_buckets.size() + 10); } } MyBucket* bucketPtr = m_buckets.at(useBucket); if(!bucketPtr) { initializeBucket(useBucket); bucketPtr = m_buckets.at(useBucket); } ENSURE_REACHABLE(useBucket); Q_ASSERT_X(!bucketPtr->findIndex(request), Q_FUNC_INFO, "found item in unexpected bucket, ensure your ItemRequest::equals method is correct. Note: For custom AbstractType's e.g. ensure you have a proper equals() override"); unsigned short indexInBucket = bucketPtr->index(request, size); //If we could not allocate the item in an empty bucket, then we need to create a monster-bucket that //can hold the data. if(bucketPtr->isEmpty() && !indexInBucket) { ///@todo Move this compound statement into an own function uint totalSize = size + MyBucket::AdditionalSpacePerItem; Q_ASSERT((totalSize > ItemRepositoryBucketSize)); useBucket = 0; //The item did not fit in, we need a monster-bucket(Merge consecutive buckets) ///Step one: Search whether we can merge multiple empty buckets in the free-list into one monster-bucket int rangeStart = -1; int rangeEnd = -1; for(int a = 0; a < m_freeSpaceBuckets.size(); ++a) { MyBucket* bucketPtr = bucketForIndex(m_freeSpaceBuckets[a]); if(bucketPtr->isEmpty()) { //This bucket is a candidate for monster-bucket merging int index = (int)m_freeSpaceBuckets[a]; if(rangeEnd != index) { rangeStart = index; rangeEnd = index+1; }else{ ++rangeEnd; } if(rangeStart != rangeEnd) { uint extent = rangeEnd - rangeStart - 1; uint totalAvailableSpace = bucketForIndex(rangeStart)->available() + MyBucket::DataSize * (rangeEnd - rangeStart - 1); if(totalAvailableSpace > totalSize) { Q_ASSERT(extent); ///We can merge these buckets into one monster-bucket that can hold the data Q_ASSERT((uint)m_freeSpaceBuckets[a-extent] == (uint)rangeStart); m_freeSpaceBuckets.remove(a-extent, extent+1); useBucket = rangeStart; convertMonsterBucket(rangeStart, extent); break; } } } } if(!useBucket) { //Create a new monster-bucket at the end of the data int needMonsterExtent = (totalSize - ItemRepositoryBucketSize) / MyBucket::DataSize + 1; Q_ASSERT(needMonsterExtent); if(m_currentBucket + needMonsterExtent + 1 > m_buckets.size()) { m_buckets.resize(m_buckets.size() + 10 + needMonsterExtent + 1); } useBucket = m_currentBucket; convertMonsterBucket(useBucket, needMonsterExtent); m_currentBucket += 1 + needMonsterExtent; Q_ASSERT(m_currentBucket < ItemRepositoryBucketLimit); Q_ASSERT(m_buckets[m_currentBucket - 1 - needMonsterExtent] && m_buckets[m_currentBucket - 1 - needMonsterExtent]->monsterBucketExtent() == needMonsterExtent); } Q_ASSERT(useBucket); bucketPtr = bucketForIndex(useBucket); indexInBucket = bucketPtr->index(request, size); Q_ASSERT(indexInBucket); } if(indexInBucket) { ++m_statItemCount; const int previousBucketNumber = lastBucketWalked; unsigned short* const bucketHashPosition = m_firstBucketForHash + (hash % bucketHashSize); if(!(*bucketHashPosition)) { Q_ASSERT(!previousBucketNumber); (*bucketHashPosition) = useBucket; ENSURE_REACHABLE(useBucket); } else if(!pickedBucketInChain && previousBucketNumber && previousBucketNumber != useBucket) { //Should happen rarely ++m_statBucketHashClashes; ///Debug: Detect infinite recursion ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, previousBucketNumber));) //Find the position where the paths of useBucket and *bucketHashPosition intersect, and insert useBucket //there. That way, we don't create loops. QPair intersect = hashChainIntersection(*bucketHashPosition, useBucket, hash); Q_ASSERT(m_buckets[previousBucketNumber]->nextBucketForHash(hash) == 0); if(!intersect.second) { ifDebugInfiniteRecursion(Q_ASSERT(!walkBucketLinks(*bucketHashPosition, hash, useBucket));) ifDebugInfiniteRecursion(Q_ASSERT(!walkBucketLinks(useBucket, hash, previousBucketNumber));) Q_ASSERT(m_buckets[previousBucketNumber]->nextBucketForHash(hash) == 0); m_buckets[previousBucketNumber]->setNextBucketForHash(hash, useBucket); ENSURE_REACHABLE(useBucket); ENSURE_REACHABLE(previousBucketNumber); ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, useBucket));) } else if(intersect.first) { ifDebugInfiniteRecursion(Q_ASSERT(bucketForIndex(intersect.first)->nextBucketForHash(hash) == intersect.second);) ifDebugInfiniteRecursion(Q_ASSERT(!walkBucketLinks(*bucketHashPosition, hash, useBucket));) ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, intersect.second));) ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, intersect.first));) ifDebugInfiniteRecursion(Q_ASSERT(bucketForIndex(intersect.first)->nextBucketForHash(hash) == intersect.second);) ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(useBucket, hash, intersect.second));) ifDebugInfiniteRecursion(Q_ASSERT(!walkBucketLinks(useBucket, hash, intersect.first));) bucketForIndex(intersect.first)->setNextBucketForHash(hash, useBucket); ENSURE_REACHABLE(useBucket); ENSURE_REACHABLE(intersect.second); ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, useBucket));) ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(*bucketHashPosition, hash, intersect.second));) } else { //State: intersect.first == 0 && intersect.second != 0. This means that whole compleet //chain opened by *bucketHashPosition with the hash-value is also following useBucket, //so useBucket can just be inserted at the top ifDebugInfiniteRecursion(Q_ASSERT(!walkBucketLinks(*bucketHashPosition, hash, useBucket));) ifDebugInfiniteRecursion(Q_ASSERT(walkBucketLinks(useBucket, hash, *bucketHashPosition));) unsigned short oldStart = *bucketHashPosition; *bucketHashPosition = useBucket; ENSURE_REACHABLE(useBucket); ENSURE_REACHABLE(oldStart); Q_UNUSED(oldStart); } } if(reOrderFreeSpaceBucketIndex != -1) updateFreeSpaceOrder(reOrderFreeSpaceBucketIndex); return createIndex(useBucket, indexInBucket); }else{ //This should never happen when we picked a bucket for re-use Q_ASSERT(!pickedBucketInChain); Q_ASSERT(reOrderFreeSpaceBucketIndex == -1); Q_ASSERT(useBucket == m_currentBucket); if(!bucketForIndex(useBucket)->isEmpty()) putIntoFreeList(useBucket, bucketPtr); ++m_currentBucket; Q_ASSERT(m_currentBucket < ItemRepositoryBucketLimit); useBucket = m_currentBucket; } } //We haven't found a bucket that already contains the item, so append it to the current bucket qWarning() << "Found no bucket for an item in" << m_repositoryName; return 0; } ///Returns zero if the item is not in the repository yet unsigned int findIndex(const ItemRequest& request) { ThisLocker lock(m_mutex); return walkBucketChain(request.hash(), [this, &request](ushort bucketIdx, const MyBucket* bucketPtr) { const ushort indexInBucket = bucketPtr->findIndex(request); return indexInBucket ? createIndex(bucketIdx, indexInBucket) : 0u; }); } ///Deletes the item from the repository. void deleteItem(unsigned int index) { verifyIndex(index); ThisLocker lock(m_mutex); m_metaDataChanged = true; const uint hash = itemFromIndex(index)->hash(); const ushort bucket = (index >> 16); //Apart from removing the item itself, we may have to recreate the nextBucketForHash link, so we need the previous bucket MyBucket* previousBucketPtr = nullptr; MyBucket* const bucketPtr = walkBucketChain(hash, [bucket, &previousBucketPtr](ushort bucketIdx, MyBucket* bucketPtr) -> MyBucket* { if (bucket != bucketIdx) { previousBucketPtr = bucketPtr; return nullptr; } return bucketPtr; // found bucket, stop looking }); //Make sure the index was reachable through the hash chain Q_ASSERT(bucketPtr); --m_statItemCount; bucketPtr->deleteItem(index, hash, *this); /** * Now check whether the link root/previousBucketNumber -> bucket is still needed. */ if (!previousBucketPtr) { // This bucket is linked in the m_firstBucketForHash array, find the next clashing bucket in the chain // There may be items in the chain that clash only with MyBucket::NextBucketHashSize, skipped here m_firstBucketForHash[hash % bucketHashSize] = walkBucketChain(hash, [hash](ushort bucketIdx, MyBucket *bucketPtr){ if (bucketPtr->hasClashingItem(hash, bucketHashSize)) { return bucketIdx; } return static_cast(0); }); } else if(!bucketPtr->hasClashingItem(hash, MyBucket::NextBucketHashSize)) { // TODO: Skip clashing items reachable from m_firstBucketForHash // (see note in usePermissiveModuloWhenRemovingClashLinks() test) ENSURE_REACHABLE(bucket); previousBucketPtr->setNextBucketForHash(hash, bucketPtr->nextBucketForHash(hash)); Q_ASSERT(m_buckets[bucketPtr->nextBucketForHash(hash)] != previousBucketPtr); } ENSURE_REACHABLE(bucket); if(bucketPtr->monsterBucketExtent()) { //Convert the monster-bucket back to multiple normal buckets, and put them into the free list uint newBuckets = bucketPtr->monsterBucketExtent()+1; Q_ASSERT(bucketPtr->isEmpty()); if (!previousBucketPtr) { // see https://bugs.kde.org/show_bug.cgi?id=272408 // the monster bucket will be deleted and new smaller ones created // the next bucket for this hash is invalid anyways as done above // but calling the below unconditionally leads to other issues... bucketPtr->setNextBucketForHash(hash, 0); } convertMonsterBucket(bucket, 0); for(uint created = bucket; created < bucket + newBuckets; ++created) { putIntoFreeList(created, bucketForIndex(created)); #ifdef DEBUG_MONSTERBUCKETS Q_ASSERT(m_freeSpaceBuckets.indexOf(created) != -1); #endif } }else{ putIntoFreeList(bucket, bucketPtr); } } typedef DynamicItem MyDynamicItem; ///This returns an editable version of the item. @warning: Never change an entry that affects the hash, ///or the equals(..) function. That would completely destroy the consistency. ///@param index The index. It must be valid(match an existing item), and nonzero. ///@warning If you use this, make sure you lock mutex() before calling, /// and hold it until you're ready using/changing the data.. MyDynamicItem dynamicItemFromIndex(unsigned int index) { verifyIndex(index); ThisLocker lock(m_mutex); unsigned short bucket = (index >> 16); MyBucket* bucketPtr = m_buckets.at(bucket); if(!bucketPtr) { initializeBucket(bucket); bucketPtr = m_buckets.at(bucket); } bucketPtr->prepareChange(); unsigned short indexInBucket = index & 0xffff; return MyDynamicItem(const_cast(bucketPtr->itemFromIndex(indexInBucket)), bucketPtr->data(), bucketPtr->dataSize()); } ///This returns an editable version of the item. @warning: Never change an entry that affects the hash, ///or the equals(..) function. That would completely destroy the consistency. ///@param index The index. It must be valid(match an existing item), and nonzero. ///@warning If you use this, make sure you lock mutex() before calling, /// and hold it until you're ready using/changing the data.. ///@warning If you change contained complex items that depend on reference-counting, you /// must use dynamicItemFromIndex(..) instead of dynamicItemFromIndexSimple(..) Item* dynamicItemFromIndexSimple(unsigned int index) { verifyIndex(index); ThisLocker lock(m_mutex); unsigned short bucket = (index >> 16); MyBucket* bucketPtr = m_buckets.at(bucket); if(!bucketPtr) { initializeBucket(bucket); bucketPtr = m_buckets.at(bucket); } bucketPtr->prepareChange(); unsigned short indexInBucket = index & 0xffff; return const_cast(bucketPtr->itemFromIndex(indexInBucket)); } ///@param index The index. It must be valid(match an existing item), and nonzero. const Item* itemFromIndex(unsigned int index) const { verifyIndex(index); ThisLocker lock(m_mutex); unsigned short bucket = (index >> 16); const MyBucket* bucketPtr = m_buckets.at(bucket); if(!bucketPtr) { initializeBucket(bucket); bucketPtr = m_buckets.at(bucket); } unsigned short indexInBucket = index & 0xffff; return bucketPtr->itemFromIndex(indexInBucket); } struct Statistics { Statistics() { } uint loadedBuckets = -1; uint currentBucket = -1; uint usedMemory = -1; uint loadedMonsterBuckets = -1; uint usedSpaceForBuckets = -1; uint freeSpaceInBuckets = -1; uint lostSpace = -1; uint freeUnreachableSpace = -1; uint hashClashedItems = -1; uint totalItems = -1; uint emptyBuckets; uint hashSize = -1; //How big the hash is uint hashUse = -1; //How many slots in the hash are used uint averageInBucketHashSize = -1; uint averageInBucketUsedSlotCount = -1; float averageInBucketSlotChainLength = -1; uint longestInBucketChain = -1; uint longestNextBucketChain = -1; uint totalBucketFollowerSlots = -1; //Total count of used slots in the nextBucketForHash structure float averageNextBucketForHashSequenceLength = -1; //Average sequence length of a nextBucketForHash sequence(If not empty) QString print() const { QString ret; ret += QStringLiteral("loaded buckets: %1 current bucket: %2 used memory: %3 loaded monster buckets: %4").arg(loadedBuckets).arg(currentBucket).arg(usedMemory).arg(loadedMonsterBuckets); ret += QStringLiteral("\nbucket hash clashed items: %1 total items: %2").arg(hashClashedItems).arg(totalItems); ret += QStringLiteral("\nused space for buckets: %1 free space in buckets: %2 lost space: %3").arg(usedSpaceForBuckets).arg(freeSpaceInBuckets).arg(lostSpace); ret += QStringLiteral("\nfree unreachable space: %1 empty buckets: %2").arg(freeUnreachableSpace).arg(emptyBuckets); ret += QStringLiteral("\nhash size: %1 hash slots used: %2").arg(hashSize).arg(hashUse); ret += QStringLiteral("\naverage in-bucket hash size: %1 average in-bucket used hash slot count: %2 average in-bucket slot chain length: %3 longest in-bucket follower chain: %4").arg(averageInBucketHashSize).arg(averageInBucketUsedSlotCount).arg(averageInBucketSlotChainLength).arg(longestInBucketChain); ret += QStringLiteral("\ntotal count of used next-bucket-for-hash slots: %1 average next-bucket-for-hash sequence length: %2 longest next-bucket chain: %3").arg(totalBucketFollowerSlots).arg(averageNextBucketForHashSequenceLength).arg(longestNextBucketChain); return ret; } operator QString() const { return print(); } }; QString printStatistics() const override { return statistics().print(); } Statistics statistics() const { Statistics ret; uint loadedBuckets = 0; for(int a = 0; a < m_buckets.size(); ++a) if(m_buckets[a]) ++loadedBuckets; #ifdef DEBUG_MONSTERBUCKETS for(int a = 0; a < m_freeSpaceBuckets.size(); ++a) { if(a > 0) { uint prev = a-1; uint prevLargestFree = bucketForIndex(m_freeSpaceBuckets[prev])->largestFreeSize(); uint largestFree = bucketForIndex(m_freeSpaceBuckets[a])->largestFreeSize(); Q_ASSERT( (prevLargestFree < largestFree) || (prevLargestFree == largestFree && m_freeSpaceBuckets[prev] < m_freeSpaceBuckets[a]) ); } } #endif ret.hashSize = bucketHashSize; ret.hashUse = 0; for(uint a = 0; a < bucketHashSize; ++a) if(m_firstBucketForHash[a]) ++ret.hashUse; ret.emptyBuckets = 0; uint loadedMonsterBuckets = 0; for(int a = 0; a < m_buckets.size(); ++a) if(m_buckets[a] && m_buckets[a]->monsterBucketExtent()) loadedMonsterBuckets += m_buckets[a]->monsterBucketExtent()+1; uint usedBucketSpace = MyBucket::DataSize * m_currentBucket; uint freeBucketSpace = 0, freeUnreachableSpace = 0; uint lostSpace = 0; //Should be zero, else something is wrong uint totalInBucketHashSize = 0, totalInBucketUsedSlotCount = 0, totalInBucketChainLengths = 0; uint bucketCount = 0; ret.totalBucketFollowerSlots = 0; ret.averageNextBucketForHashSequenceLength = 0; ret.longestNextBucketChain = 0; ret.longestInBucketChain = 0; for(int a = 1; a < m_currentBucket+1; ++a) { MyBucket* bucket = bucketForIndex(a); if(bucket) { ++bucketCount; bucket->countFollowerIndexLengths(totalInBucketUsedSlotCount, totalInBucketChainLengths, totalInBucketHashSize, ret.longestInBucketChain); for(uint aa = 0; aa < MyBucket::NextBucketHashSize; ++aa) { uint length = 0; uint next = bucket->nextBucketForHash(aa); if(next) { ++ret.totalBucketFollowerSlots; while(next) { ++length; ++ret.averageNextBucketForHashSequenceLength; next = bucketForIndex(next)->nextBucketForHash(aa); } } if(length > ret.longestNextBucketChain) { // qDebug() << "next-bucket-chain in" << a << aa << ":" << length; ret.longestNextBucketChain = length; } } uint bucketFreeSpace = bucket->totalFreeItemsSize() + bucket->available(); freeBucketSpace += bucketFreeSpace; if(m_freeSpaceBuckets.indexOf(a) == -1) freeUnreachableSpace += bucketFreeSpace; if(bucket->isEmpty()) { ++ret.emptyBuckets; Q_ASSERT(!bucket->monsterBucketExtent()); #ifdef DEBUG_MONSTERBUCKETS Q_ASSERT(m_freeSpaceBuckets.contains(a)); #endif } lostSpace += bucket->lostSpace(); a += bucket->monsterBucketExtent(); } } if(ret.totalBucketFollowerSlots) ret.averageNextBucketForHashSequenceLength /= ret.totalBucketFollowerSlots; ret.loadedBuckets = loadedBuckets; ret.currentBucket = m_currentBucket; ret.usedMemory = usedMemory(); ret.loadedMonsterBuckets = loadedMonsterBuckets; ret.hashClashedItems = m_statBucketHashClashes; ret.totalItems = m_statItemCount; ret.usedSpaceForBuckets = usedBucketSpace; ret.freeSpaceInBuckets = freeBucketSpace; ret.lostSpace = lostSpace; ret.freeUnreachableSpace = freeUnreachableSpace; ret.averageInBucketHashSize = totalInBucketHashSize / bucketCount; ret.averageInBucketUsedSlotCount = totalInBucketUsedSlotCount / bucketCount; ret.averageInBucketSlotChainLength = float(totalInBucketChainLengths) / totalInBucketUsedSlotCount; //If m_statBucketHashClashes is high, the bucket-hash needs to be bigger return ret; } uint usedMemory() const { uint used = 0; for(int a = 0; a < m_buckets.size(); ++a) { if(m_buckets[a]) { used += m_buckets[a]->usedMemory(); } } return used; } ///This can be used to safely iterate through all items in the repository ///@param visitor Should be an instance of a class that has a bool operator()(const Item*) member function, /// that returns whether more items are wanted. ///@param onlyInMemory If this is true, only items are visited that are currently in memory. template void visitAllItems(Visitor& visitor, bool onlyInMemory = false) const { ThisLocker lock(m_mutex); for(int a = 1; a <= m_currentBucket; ++a) { if(!onlyInMemory || m_buckets.at(a)) { if(bucketForIndex(a) && !bucketForIndex(a)->visitAllItems(visitor)) return; } } } ///Synchronizes the state on disk to the one in memory, and does some memory-management. ///Should be called on a regular basis. Can be called centrally from the global item repository registry. void store() override { QMutexLocker lock(m_mutex); if(m_file) { if(!m_file->open( QFile::ReadWrite ) || !m_dynamicFile->open( QFile::ReadWrite )) { qFatal("cannot re-open repository file for storing"); return; } for(int a = 0; a < m_buckets.size(); ++a) { if(m_buckets[a]) { if(m_buckets[a]->changed()) { storeBucket(a); } if(m_unloadingEnabled) { const int unloadAfterTicks = 2; if(m_buckets[a]->lastUsed() > unloadAfterTicks) { delete m_buckets[a]; m_buckets[a] = nullptr; }else{ m_buckets[a]->tick(); } } } } if(m_metaDataChanged) { Q_ASSERT(m_dynamicFile); m_file->seek(0); m_file->write((char*)&m_repositoryVersion, sizeof(uint)); uint hashSize = bucketHashSize; m_file->write((char*)&hashSize, sizeof(uint)); uint itemRepositoryVersion = staticItemRepositoryVersion(); m_file->write((char*)&itemRepositoryVersion, sizeof(uint)); m_file->write((char*)&m_statBucketHashClashes, sizeof(uint)); m_file->write((char*)&m_statItemCount, sizeof(uint)); const uint bucketCount = static_cast(m_buckets.size()); m_file->write((char*)&bucketCount, sizeof(uint)); m_file->write((char*)&m_currentBucket, sizeof(uint)); m_file->write((char*)m_firstBucketForHash, sizeof(short unsigned int) * bucketHashSize); Q_ASSERT(m_file->pos() == BucketStartOffset); m_dynamicFile->seek(0); const uint freeSpaceBucketsSize = static_cast(m_freeSpaceBuckets.size()); m_dynamicFile->write((char*)&freeSpaceBucketsSize, sizeof(uint)); m_dynamicFile->write((char*)m_freeSpaceBuckets.data(), sizeof(uint) * freeSpaceBucketsSize); } //To protect us from inconsistency due to crashes. flush() is not enough. We need to close. m_file->close(); m_dynamicFile->close(); Q_ASSERT(!m_file->isOpen()); Q_ASSERT(!m_dynamicFile->isOpen()); } } ///This mutex is used for the thread-safe locking when threadSafe is true. Even if threadSafe is false, it is ///always locked before storing to or loading from disk. ///@warning If threadSafe is false, and you sometimes call store() from within another thread(As happens in duchain), /// you must always make sure that this mutex is locked before you access this repository. /// Else you will get crashes and inconsistencies. /// In KDevelop This means: Make sure you _always_ lock this mutex before accessing the repository. QMutex* mutex() const { return m_mutex; } ///With this, you can replace the internal mutex with another one. void setMutex(QMutex* mutex) { m_mutex = mutex; } QString repositoryName() const override { return m_repositoryName; } private: uint createIndex(ushort bucketIndex, ushort indexInBucket) { //Combine the index in the bucket, and the bucket number into one index const uint index = (bucketIndex << 16) + indexInBucket; verifyIndex(index); return index; } /** * Walks through all buckets clashing with @p hash * * Will return the value returned by the lambda, returning early if truthy */ template auto walkBucketChain(unsigned int hash, const Visitor& visitor) const -> decltype(visitor(0, nullptr)) { unsigned short bucketIndex = m_firstBucketForHash[hash % bucketHashSize]; while (bucketIndex) { auto* bucketPtr = m_buckets.at(bucketIndex); if (!bucketPtr) { initializeBucket(bucketIndex); bucketPtr = m_buckets.at(bucketIndex); } if (auto visitResult = visitor(bucketIndex, bucketPtr)) { return visitResult; } bucketIndex = bucketPtr->nextBucketForHash(hash); } return {}; // clazy:exclude=returning-void-expression } ///Makes sure the order within m_freeSpaceBuckets is correct, after largestFreeSize has been changed for m_freeSpaceBuckets[index]. ///If too few space is free within the given bucket, it is removed from m_freeSpaceBuckets. void updateFreeSpaceOrder(uint index) { m_metaDataChanged = true; unsigned int* freeSpaceBuckets = m_freeSpaceBuckets.data(); Q_ASSERT(index < static_cast(m_freeSpaceBuckets.size())); MyBucket* bucketPtr = bucketForIndex(freeSpaceBuckets[index]); unsigned short largestFreeSize = bucketPtr->largestFreeSize(); if(largestFreeSize == 0 || (bucketPtr->freeItemCount() <= MyBucket::MaxFreeItemsForHide && largestFreeSize <= MyBucket::MaxFreeSizeForHide)) { //Remove the item from freeSpaceBuckets m_freeSpaceBuckets.remove(index); }else{ while(1) { int prev = index-1; int next = index+1; if(prev >= 0 && (bucketForIndex(freeSpaceBuckets[prev])->largestFreeSize() > largestFreeSize || (bucketForIndex(freeSpaceBuckets[prev])->largestFreeSize() == largestFreeSize && freeSpaceBuckets[index] < freeSpaceBuckets[prev])) ) { //This item should be behind the successor, either because it has a lower largestFreeSize, or because the index is lower uint oldPrevValue = freeSpaceBuckets[prev]; freeSpaceBuckets[prev] = freeSpaceBuckets[index]; freeSpaceBuckets[index] = oldPrevValue; index = prev; }else if(next < m_freeSpaceBuckets.size() && (bucketForIndex(freeSpaceBuckets[next])->largestFreeSize() < largestFreeSize || (bucketForIndex(freeSpaceBuckets[next])->largestFreeSize() == largestFreeSize && freeSpaceBuckets[index] > freeSpaceBuckets[next]))) { //This item should be behind the successor, either because it has a higher largestFreeSize, or because the index is higher uint oldNextValue = freeSpaceBuckets[next]; freeSpaceBuckets[next] = freeSpaceBuckets[index]; freeSpaceBuckets[index] = oldNextValue; index = next; }else { break; } } } } ///Does conversion from monster-bucket to normal bucket and from normal bucket to monster-bucket ///The bucket @param bucketNumber must already be loaded and empty. the "extent" buckets behind must also be loaded, ///and also be empty. ///The created buckets are not registered anywhere. When converting from monster-bucket to normal bucket, ///oldExtent+1 normal buckets are created, that must be registered somewhere. ///@warning During conversion, all the touched buckets are deleted and re-created ///@param extent When this is zero, the bucket is converted from monster-bucket to normal bucket. /// When it is nonzero, it is converted to a monster-bucket. MyBucket* convertMonsterBucket(int bucketNumber, int extent) { Q_ASSERT(bucketNumber); MyBucket* bucketPtr = m_buckets.at(bucketNumber); if(!bucketPtr) { initializeBucket(bucketNumber); bucketPtr = m_buckets.at(bucketNumber); } if(extent) { //Convert to monster-bucket #ifdef DEBUG_MONSTERBUCKETS for(int index = bucketNumber; index < bucketNumber + 1 + extent; ++index) { Q_ASSERT(bucketPtr->isEmpty()); Q_ASSERT(!bucketPtr->monsterBucketExtent()); Q_ASSERT(m_freeSpaceBuckets.indexOf(index) == -1); } #endif for(int index = bucketNumber; index < bucketNumber + 1 + extent; ++index) deleteBucket(index); m_buckets[bucketNumber] = new MyBucket(); m_buckets[bucketNumber]->initialize(extent); #ifdef DEBUG_MONSTERBUCKETS for(uint index = bucketNumber+1; index < bucketNumber + 1 + extent; ++index) { Q_ASSERT(!m_buckets[index]); } #endif }else{ Q_ASSERT(bucketPtr->monsterBucketExtent()); Q_ASSERT(bucketPtr->isEmpty()); const int oldExtent = bucketPtr->monsterBucketExtent(); deleteBucket(bucketNumber); //Delete the monster-bucket for(int index = bucketNumber; index < bucketNumber + 1 + oldExtent; ++index) { Q_ASSERT(!m_buckets[index]); m_buckets[index] = new MyBucket(); m_buckets[index]->initialize(0); Q_ASSERT(!m_buckets[index]->monsterBucketExtent()); } } return m_buckets[bucketNumber]; } MyBucket* bucketForIndex(short unsigned int index) const { MyBucket* bucketPtr = m_buckets.at(index); if(!bucketPtr) { initializeBucket(index); bucketPtr = m_buckets.at(index); } return bucketPtr; } bool open(const QString& path) override { QMutexLocker lock(m_mutex); close(); //qDebug() << "opening repository" << m_repositoryName << "at" << path; QDir dir(path); m_file = new QFile(dir.absoluteFilePath( m_repositoryName )); m_dynamicFile = new QFile(dir.absoluteFilePath( m_repositoryName + QLatin1String("_dynamic") )); if(!m_file->open( QFile::ReadWrite ) || !m_dynamicFile->open( QFile::ReadWrite ) ) { delete m_file; m_file = nullptr; delete m_dynamicFile; m_dynamicFile = nullptr; return false; } m_metaDataChanged = true; if(m_file->size() == 0) { m_file->resize(0); m_file->write((char*)&m_repositoryVersion, sizeof(uint)); uint hashSize = bucketHashSize; m_file->write((char*)&hashSize, sizeof(uint)); uint itemRepositoryVersion = staticItemRepositoryVersion(); m_file->write((char*)&itemRepositoryVersion, sizeof(uint)); m_statBucketHashClashes = m_statItemCount = 0; m_file->write((char*)&m_statBucketHashClashes, sizeof(uint)); m_file->write((char*)&m_statItemCount, sizeof(uint)); m_buckets.resize(10); m_buckets.fill(nullptr); uint bucketCount = m_buckets.size(); m_file->write((char*)&bucketCount, sizeof(uint)); memset(m_firstBucketForHash, 0, bucketHashSize * sizeof(short unsigned int)); m_currentBucket = 1; //Skip the first bucket, we won't use it so we have the zero indices for special purposes m_file->write((char*)&m_currentBucket, sizeof(uint)); m_file->write((char*)m_firstBucketForHash, sizeof(short unsigned int) * bucketHashSize); //We have completely initialized the file now if(m_file->pos() != BucketStartOffset) { KMessageBox::error(nullptr, i18n("Failed writing to %1, probably the disk is full", m_file->fileName())); abort(); } const uint freeSpaceBucketsSize = 0; m_dynamicFile->write((char*)&freeSpaceBucketsSize, sizeof(uint)); m_freeSpaceBuckets.clear(); }else{ m_file->close(); bool res = m_file->open( QFile::ReadOnly ); //Re-open in read-only mode, so we create a read-only m_fileMap VERIFY(res); //Check that the version is correct uint storedVersion = 0, hashSize = 0, itemRepositoryVersion = 0; m_file->read((char*)&storedVersion, sizeof(uint)); m_file->read((char*)&hashSize, sizeof(uint)); m_file->read((char*)&itemRepositoryVersion, sizeof(uint)); m_file->read((char*)&m_statBucketHashClashes, sizeof(uint)); m_file->read((char*)&m_statItemCount, sizeof(uint)); if(storedVersion != m_repositoryVersion || hashSize != bucketHashSize || itemRepositoryVersion != staticItemRepositoryVersion()) { qDebug() << "repository" << m_repositoryName << "version mismatch in" << m_file->fileName() << ", stored: version " << storedVersion << "hashsize" << hashSize << "repository-version" << itemRepositoryVersion << " current: version" << m_repositoryVersion << "hashsize" << bucketHashSize << "repository-version" << staticItemRepositoryVersion(); delete m_file; m_file = nullptr; delete m_dynamicFile; m_dynamicFile = nullptr; return false; } m_metaDataChanged = false; uint bucketCount = 0; m_file->read((char*)&bucketCount, sizeof(uint)); m_buckets.resize(bucketCount); m_file->read((char*)&m_currentBucket, sizeof(uint)); m_file->read((char*)m_firstBucketForHash, sizeof(short unsigned int) * bucketHashSize); Q_ASSERT(m_file->pos() == BucketStartOffset); uint freeSpaceBucketsSize = 0; m_dynamicFile->read((char*)&freeSpaceBucketsSize, sizeof(uint)); m_freeSpaceBuckets.resize(freeSpaceBucketsSize); m_dynamicFile->read((char*)m_freeSpaceBuckets.data(), sizeof(uint) * freeSpaceBucketsSize); } m_fileMapSize = 0; m_fileMap = nullptr; #ifdef ITEMREPOSITORY_USE_MMAP_LOADING if(m_file->size() > BucketStartOffset){ m_fileMap = m_file->map(BucketStartOffset, m_file->size() - BucketStartOffset); Q_ASSERT(m_file->isOpen()); Q_ASSERT(m_file->size() >= BucketStartOffset); if(m_fileMap){ m_fileMapSize = m_file->size() - BucketStartOffset; }else{ qWarning() << "mapping" << m_file->fileName() << "FAILED!"; } } #endif //To protect us from inconsistency due to crashes. flush() is not enough. m_file->close(); m_dynamicFile->close(); return true; } ///@warning by default, this does not store the current state to disk. void close(bool doStore = false) override { if(doStore) store(); if(m_file) m_file->close(); delete m_file; m_file = nullptr; m_fileMap = nullptr; m_fileMapSize = 0; if(m_dynamicFile) m_dynamicFile->close(); delete m_dynamicFile; m_dynamicFile = nullptr; qDeleteAll(m_buckets); m_buckets.clear(); memset(m_firstBucketForHash, 0, bucketHashSize * sizeof(short unsigned int)); } struct AllItemsReachableVisitor { explicit AllItemsReachableVisitor(ItemRepository* rep) : repository(rep) { } bool operator()(const Item* item) { return repository->itemReachable(item); } ItemRepository* repository; }; //Returns whether the given item is reachable through its hash bool itemReachable(const Item* item) const { const uint hash = item->hash(); return walkBucketChain(hash, [=](ushort /*bucketIndex*/, const MyBucket* bucketPtr) { return bucketPtr->itemReachable(item, hash); }); } //Returns true if all items in the given bucket are reachable through their hashes bool allItemsReachable(unsigned short bucket) { if(!bucket) return true; MyBucket* bucketPtr = bucketForIndex(bucket); AllItemsReachableVisitor visitor(this); return bucketPtr->visitAllItems(visitor); } int finalCleanup() override { ThisLocker lock(m_mutex); int changed = 0; for(int a = 1; a <= m_currentBucket; ++a) { MyBucket* bucket = bucketForIndex(a); if(bucket && bucket->dirty()) { ///@todo Faster dirty check, without loading bucket changed += bucket->finalCleanup(*this); } a += bucket->monsterBucketExtent(); //Skip buckets that are attached as tail to monster-buckets } return changed; } inline void initializeBucket(int bucketNumber) const { Q_ASSERT(bucketNumber); #ifdef DEBUG_MONSTERBUCKETS for(uint offset = 1; offset < 5; ++offset) { int test = bucketNumber - offset; if(test >= 0 && m_buckets[test]) { Q_ASSERT(m_buckets[test]->monsterBucketExtent() < offset); } } #endif if(!m_buckets[bucketNumber]) { m_buckets[bucketNumber] = new MyBucket(); bool doMMapLoading = (bool)m_fileMap; uint offset = ((bucketNumber-1) * MyBucket::DataSize); if(m_file && offset < m_fileMapSize && doMMapLoading && *reinterpret_cast(m_fileMap + offset) == 0) { // qDebug() << "loading bucket mmap:" << bucketNumber; m_buckets[bucketNumber]->initializeFromMap(reinterpret_cast(m_fileMap + offset)); } else if(m_file) { //Either memory-mapping is disabled, or the item is not in the existing memory-map, //so we have to load it the classical way. bool res = m_file->open( QFile::ReadOnly ); if(offset + BucketStartOffset < m_file->size()) { VERIFY(res); offset += BucketStartOffset; m_file->seek(offset); uint monsterBucketExtent; - m_file->read((char*)(&monsterBucketExtent), sizeof(unsigned int));; + m_file->read((char*)(&monsterBucketExtent), sizeof(unsigned int)); m_file->seek(offset); ///FIXME: use the data here instead of copying it again in prepareChange QByteArray data = m_file->read((1+monsterBucketExtent) * MyBucket::DataSize); m_buckets[bucketNumber]->initializeFromMap(data.data()); m_buckets[bucketNumber]->prepareChange(); }else{ m_buckets[bucketNumber]->initialize(0); } m_file->close(); }else{ m_buckets[bucketNumber]->initialize(0); } }else{ m_buckets[bucketNumber]->initialize(0); } } ///Can only be called on empty buckets void deleteBucket(int bucketNumber) { Q_ASSERT(bucketForIndex(bucketNumber)->isEmpty()); Q_ASSERT(bucketForIndex(bucketNumber)->noNextBuckets()); delete m_buckets[bucketNumber]; m_buckets[bucketNumber] = nullptr; } //m_file must be opened void storeBucket(int bucketNumber) const { if(m_file && m_buckets[bucketNumber]) { m_buckets[bucketNumber]->store(m_file, BucketStartOffset + (bucketNumber-1) * MyBucket::DataSize); } } /// If mustFindBucket is zero, the whole chain is just walked. This is good for debugging for infinite recursion. /// @return whether @p mustFindBucket was found bool walkBucketLinks(uint checkBucket, uint hash, uint mustFindBucket = 0) const { bool found = false; while(checkBucket) { if(checkBucket == mustFindBucket) found = true; checkBucket = bucketForIndex(checkBucket)->nextBucketForHash(hash); } return found || (mustFindBucket == 0); } /// Computes the bucket where the chains opened by the buckets @p mainHead and @p intersectorHead /// with hash @p hash meet each other. /// @return QPair hashChainIntersection(uint mainHead, uint intersectorHead, uint hash) const { uint previous = 0; uint current = mainHead; while(current) { ///@todo Make this more efficient if(walkBucketLinks(intersectorHead, hash, current)) return qMakePair(previous, current); previous = current; current = bucketForIndex(current)->nextBucketForHash(hash); } return qMakePair(0u, 0u); } void putIntoFreeList(unsigned short bucket, MyBucket* bucketPtr) { Q_ASSERT(!bucketPtr->monsterBucketExtent()); int indexInFree = m_freeSpaceBuckets.indexOf(bucket); if(indexInFree == -1 && (bucketPtr->freeItemCount() >= MyBucket::MinFreeItemsForReuse || bucketPtr->largestFreeSize() >= MyBucket::MinFreeSizeForReuse)) { //Add the bucket to the list of buckets from where to re-assign free space //We only do it when a specific threshold of empty items is reached, because that way items can stay "somewhat" semantically ordered. Q_ASSERT(bucketPtr->largestFreeSize()); int insertPos; for(insertPos = 0; insertPos < m_freeSpaceBuckets.size(); ++insertPos) { if(bucketForIndex(m_freeSpaceBuckets[insertPos])->largestFreeSize() > bucketPtr->largestFreeSize()) break; } m_freeSpaceBuckets.insert(insertPos, bucket); updateFreeSpaceOrder(insertPos); }else if(indexInFree != -1) { ///Re-order so the order in m_freeSpaceBuckets is correct(sorted by largest free item size) updateFreeSpaceOrder(indexInFree); } #ifdef DEBUG_MONSTERBUCKETS if(bucketPtr->isEmpty()) { Q_ASSERT(m_freeSpaceBuckets.contains(bucket)); } #endif } void verifyIndex(uint index) const { // We don't use zero indices Q_ASSERT(index); int bucket = (index >> 16); // nor zero buckets Q_ASSERT(bucket); Q_ASSERT_X(bucket < m_buckets.size(), Q_FUNC_INFO, qPrintable(QStringLiteral("index %1 gives invalid bucket number %2, current count is: %3") .arg(index) .arg(bucket) .arg(m_buckets.size()))); // don't trigger compile warnings in release mode Q_UNUSED(bucket); Q_UNUSED(index); } bool m_metaDataChanged; mutable QMutex m_ownMutex; mutable QMutex* m_mutex; QString m_repositoryName; mutable int m_currentBucket; //List of buckets that have free space available that can be assigned. Sorted by size: Smallest space first. Second order sorting: Bucket index QVector m_freeSpaceBuckets; mutable QVector m_buckets; uint m_statBucketHashClashes, m_statItemCount; //Maps hash-values modulo 1< Copyright 2002 Bernd Gehrmann Copyright 2003 Roberto Raggi Copyright 2003-2008 Hamish Rodda Copyright 2003 Harald Fernengel Copyright 2003 Jens Dagerbo Copyright 2005 Adam Treat Copyright 2004-2007 Alexander Dymo Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "documentcontroller.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 "core.h" #include "mainwindow.h" #include "textdocument.h" #include "uicontroller.h" #include "partcontroller.h" #include "savedialog.h" #include "debug.h" #include #include #define EMPTY_DOCUMENT_URL i18n("Untitled") using namespace KDevelop; class KDevelop::DocumentControllerPrivate { public: struct OpenFileResult { QList urls; QString encoding; }; explicit DocumentControllerPrivate(DocumentController* c) : controller(c) , fileOpenRecent(nullptr) , globalTextEditorInstance(nullptr) { } ~DocumentControllerPrivate() = default; // used to map urls to open docs QHash< QUrl, IDocument* > documents; QHash< QString, IDocumentFactory* > factories; struct HistoryEntry { HistoryEntry() {} HistoryEntry( const QUrl & u, const KTextEditor::Cursor& cursor ); QUrl url; KTextEditor::Cursor cursor; int id; }; void removeDocument(Sublime::Document *doc) { QList urlsForDoc = documents.keys(dynamic_cast(doc)); foreach (const QUrl &url, urlsForDoc) { qCDebug(SHELL) << "destroying document" << doc; documents.remove(url); } } OpenFileResult showOpenFile() const { QUrl dir; if ( controller->activeDocument() ) { dir = controller->activeDocument()->url().adjusted(QUrl::RemoveFilename); } else { const auto cfg = KSharedConfig::openConfig()->group("Open File"); dir = cfg.readEntry( "Last Open File Directory", Core::self()->projectController()->projectsBaseDirectory() ); } const auto caption = i18n("Open File"); const auto filter = i18n("*|Text File\n"); auto parent = Core::self()->uiControllerInternal()->defaultMainWindow(); // use special dialogs in a KDE session, native dialogs elsewhere if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) { const auto result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), dir, filter, parent, caption); return {result.URLs, result.encoding}; } // note: can't just filter on text files using the native dialog, just display all files // see https://phabricator.kde.org/D622#11679 const auto urls = QFileDialog::getOpenFileUrls(parent, caption, dir); return {urls, QString()}; } void chooseDocument() { const auto res = showOpenFile(); if( !res.urls.isEmpty() ) { QString encoding = res.encoding; foreach( const QUrl& u, res.urls ) { openDocumentInternal(u, QString(), KTextEditor::Range::invalid(), encoding ); } } } void changeDocumentUrl(KDevelop::IDocument* document) { QMutableHashIterator it = documents; while (it.hasNext()) { if (it.next().value() == document) { if (documents.contains(document->url())) { // Weird situation (saving as a file that is aready open) IDocument* origDoc = documents[document->url()]; if (origDoc->state() & IDocument::Modified) { // given that the file has been saved, close the saved file as the other instance will become conflicted on disk document->close(); controller->activateDocument( origDoc ); break; } // Otherwise close the original document origDoc->close(); } else { // Remove the original document it.remove(); } documents.insert(document->url(), document); if (!controller->isEmptyDocumentUrl(document->url())) { fileOpenRecent->addUrl(document->url()); } break; } } } KDevelop::IDocument* findBuddyDocument(const QUrl &url, IBuddyDocumentFinder* finder) { QList allDocs = controller->openDocuments(); foreach( KDevelop::IDocument* doc, allDocs ) { if(finder->areBuddies(url, doc->url())) { return doc; } } return nullptr; } static bool fileExists(const QUrl& url) { if (url.isLocalFile()) { return QFile::exists(url.toLocalFile()); } else { auto job = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); return job->exec(); } }; IDocument* openDocumentInternal( const QUrl & inputUrl, const QString& prefName = QString(), const KTextEditor::Range& range = KTextEditor::Range::invalid(), const QString& encoding = QString(), DocumentController::DocumentActivationParams activationParams = {}, IDocument* buddy = nullptr) { Q_ASSERT(!inputUrl.isRelative()); Q_ASSERT(!inputUrl.fileName().isEmpty() || !inputUrl.isLocalFile()); QString _encoding = encoding; QUrl url = inputUrl; if ( url.isEmpty() && (!activationParams.testFlag(IDocumentController::DoNotCreateView)) ) { const auto res = showOpenFile(); if( !res.urls.isEmpty() ) url = res.urls.first(); _encoding = res.encoding; if ( url.isEmpty() ) //still no url return nullptr; } KSharedConfig::openConfig()->group("Open File").writeEntry( "Last Open File Directory", url.adjusted(QUrl::RemoveFilename) ); // clean it and resolve possible symlink url = url.adjusted( QUrl::NormalizePathSegments ); if ( url.isLocalFile() ) { QString path = QFileInfo( url.toLocalFile() ).canonicalFilePath(); if ( !path.isEmpty() ) url = QUrl::fromLocalFile( path ); } //get a part document IDocument* doc = documents.value(url); if (!doc) { QMimeType mimeType; if (DocumentController::isEmptyDocumentUrl(url)) { mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } else if (!url.isValid()) { // Exit if the url is invalid (should not happen) // If the url is valid and the file does not already exist, // kate creates the file and gives a message saying so qCDebug(SHELL) << "invalid URL:" << url.url(); return nullptr; } else if (KProtocolInfo::isKnownProtocol(url.scheme()) && !fileExists(url)) { //Don't create a new file if we are not in the code mode. if (ICore::self()->uiController()->activeArea()->objectName() != QLatin1String("code")) { return nullptr; } // enfore text mime type in order to create a kate part editor which then can be used to create the file // otherwise we could end up opening e.g. okteta which then crashes, see: https://bugs.kde.org/id=326434 mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } else { mimeType = QMimeDatabase().mimeTypeForUrl(url); if(!url.isLocalFile() && mimeType.isDefault()) { // fall back to text/plain, for remote files without extension, i.e. COPYING, LICENSE, ... // using a synchronous KIO::MimetypeJob is hazardous and may lead to repeated calls to // this function without it having returned in the first place // and this function is *not* reentrant, see assert below: // Q_ASSERT(!documents.contains(url) || documents[url]==doc); mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); } } // is the URL pointing to a directory? if (mimeType.inherits(QStringLiteral("inode/directory"))) { qCDebug(SHELL) << "cannot open directory:" << url.url(); return nullptr; } if( prefName.isEmpty() ) { // Try to find a plugin that handles this mimetype QVariantMap constraints; constraints.insert(QStringLiteral("X-KDevelop-SupportedMimeTypes"), mimeType.name()); Core::self()->pluginController()->pluginForExtension(QString(), QString(), constraints); } if( IDocumentFactory* factory = factories.value(mimeType.name())) { doc = factory->create(url, Core::self()); } if(!doc) { if( !prefName.isEmpty() ) { doc = new PartDocument(url, Core::self(), prefName); } else if ( Core::self()->partControllerInternal()->isTextType(mimeType)) { doc = new TextDocument(url, Core::self(), _encoding); } else if( Core::self()->partControllerInternal()->canCreatePart(url) ) { doc = new PartDocument(url, Core::self()); } else { int openAsText = KMessageBox::questionYesNo(nullptr, i18n("KDevelop could not find the editor for file '%1' of type %2.\nDo you want to open it as plain text?", url.fileName(), mimeType.name()), i18nc("@title:window", "Could Not Find Editor"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("AskOpenWithTextEditor")); if (openAsText == KMessageBox::Yes) doc = new TextDocument(url, Core::self(), _encoding); else return nullptr; } } } // The url in the document must equal the current url, else the housekeeping will get broken Q_ASSERT(!doc || doc->url() == url); if(doc && openDocumentInternal(doc, range, activationParams, buddy)) return doc; else return nullptr; } bool openDocumentInternal(IDocument* doc, const KTextEditor::Range& range, DocumentController::DocumentActivationParams activationParams, IDocument* buddy = nullptr) { IDocument* previousActiveDocument = controller->activeDocument(); KTextEditor::View* previousActiveTextView = ICore::self()->documentController()->activeTextDocumentView(); KTextEditor::Cursor previousActivePosition; if(previousActiveTextView) previousActivePosition = previousActiveTextView->cursorPosition(); QUrl url=doc->url(); UiController *uiController = Core::self()->uiControllerInternal(); Sublime::Area *area = uiController->activeArea(); //We can't have the same url in many documents //so we check it's already the same if it exists //contains=>it's the same Q_ASSERT(!documents.contains(url) || documents[url]==doc); Sublime::Document *sdoc = dynamic_cast(doc); if( !sdoc ) { documents.remove(url); delete doc; return false; } //react on document deletion - we need to cleanup controller structures QObject::connect(sdoc, &Sublime::Document::aboutToDelete, controller, &DocumentController::notifyDocumentClosed); //We check if it was already opened before bool emitOpened = !documents.contains(url); if(emitOpened) documents[url]=doc; if (!activationParams.testFlag(IDocumentController::DoNotCreateView)) { //find a view if there's one already opened in this area Sublime::View *partView = nullptr; Sublime::AreaIndex* activeViewIdx = area->indexOf(uiController->activeSublimeWindow()->activeView()); foreach (Sublime::View *view, sdoc->views()) { Sublime::AreaIndex* areaIdx = area->indexOf(view); if (areaIdx && areaIdx == activeViewIdx) { partView = view; break; } } bool addView = false; if (!partView) { //no view currently shown for this url partView = sdoc->createView(); addView = true; } if(addView) { // This code is never executed when restoring session on startup, // only when opening a file manually Sublime::View* buddyView = nullptr; bool placeAfterBuddy = true; if(Core::self()->uiControllerInternal()->arrangeBuddies() && !buddy && doc->mimeType().isValid()) { // If buddy is not set, look for a (usually) plugin which handles this URL's mimetype // and use its IBuddyDocumentFinder, if available, to find a buddy document QString mime = doc->mimeType().name(); IBuddyDocumentFinder* buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime); if(buddyFinder) { buddy = findBuddyDocument(url, buddyFinder); if(buddy) { placeAfterBuddy = buddyFinder->buddyOrder(buddy->url(), doc->url()); } } } if(buddy) { Sublime::Document* sublimeDocBuddy = dynamic_cast(buddy); if(sublimeDocBuddy) { Sublime::AreaIndex *pActiveViewIndex = area->indexOf(uiController->activeSublimeWindow()->activeView()); if(pActiveViewIndex) { // try to find existing View of buddy document in current active view's tab foreach (Sublime::View *pView, pActiveViewIndex->views()) { if(sublimeDocBuddy->views().contains(pView)) { buddyView = pView; break; } } } } } // add view to the area if(buddyView && area->indexOf(buddyView)) { if(placeAfterBuddy) { // Adding new view after buddy view, simple case area->addView(partView, area->indexOf(buddyView), buddyView); } else { // First new view, then buddy view area->addView(partView, area->indexOf(buddyView), buddyView); // move buddyView tab after the new document area->removeView(buddyView); area->addView(buddyView, area->indexOf(partView), partView); } } else { // no buddy found for new document / plugin does not support buddies / buddy feature disabled Sublime::View *activeView = uiController->activeSublimeWindow()->activeView(); Sublime::UrlDocument *activeDoc = nullptr; IBuddyDocumentFinder *buddyFinder = nullptr; if(activeView) activeDoc = dynamic_cast(activeView->document()); if(activeDoc && Core::self()->uiControllerInternal()->arrangeBuddies()) { QString mime = QMimeDatabase().mimeTypeForUrl(activeDoc->url()).name(); buddyFinder = IBuddyDocumentFinder::finderForMimeType(mime); } if(Core::self()->uiControllerInternal()->openAfterCurrent() && Core::self()->uiControllerInternal()->arrangeBuddies() && buddyFinder) { // Check if active document's buddy is directly next to it. // For example, we have the already-open tabs | *foo.h* | foo.cpp | , foo.h is active. // When we open a new document here (and the buddy feature is enabled), // we do not want to separate foo.h and foo.cpp, so we take care and avoid this. Sublime::AreaIndex *activeAreaIndex = area->indexOf(activeView); int pos = activeAreaIndex->views().indexOf(activeView); Sublime::View *afterActiveView = activeAreaIndex->views().value(pos+1, nullptr); Sublime::UrlDocument *activeDoc = nullptr, *afterActiveDoc = nullptr; if(activeView && afterActiveView) { activeDoc = dynamic_cast(activeView->document()); afterActiveDoc = dynamic_cast(afterActiveView->document()); } if(activeDoc && afterActiveDoc && buddyFinder->areBuddies(activeDoc->url(), afterActiveDoc->url())) { // don't insert in between of two buddies, but after them area->addView(partView, activeAreaIndex, afterActiveView); } else { // The active document's buddy is not directly after it // => no ploblem, insert after active document area->addView(partView, activeView); } } else { // Opening as last tab won't disturb our buddies // Same, if buddies are disabled, we needn't care about them. // this method places the tab according to openAfterCurrent() area->addView(partView, activeView); } } } if (!activationParams.testFlag(IDocumentController::DoNotActivate)) { uiController->activeSublimeWindow()->activateView( partView, !activationParams.testFlag(IDocumentController::DoNotFocus)); } if (!activationParams.testFlag(IDocumentController::DoNotAddToRecentOpen) && !controller->isEmptyDocumentUrl(url)) { fileOpenRecent->addUrl( url ); } if( range.isValid() ) { if (range.isEmpty()) doc->setCursorPosition( range.start() ); else doc->setTextSelection( range ); } } // Deferred signals, wait until it's all ready first if( emitOpened ) { emit controller->documentOpened( doc ); } if (!activationParams.testFlag(IDocumentController::DoNotActivate) && doc != controller->activeDocument()) emit controller->documentActivated( doc ); saveAll->setEnabled(true); revertAll->setEnabled(true); close->setEnabled(true); closeAll->setEnabled(true); closeAllOthers->setEnabled(true); KTextEditor::Cursor activePosition; if(range.isValid()) activePosition = range.start(); else if(KTextEditor::View* v = doc->activeTextView()) activePosition = v->cursorPosition(); if (doc != previousActiveDocument || activePosition != previousActivePosition) emit controller->documentJumpPerformed(doc, activePosition, previousActiveDocument, previousActivePosition); return true; } DocumentController* controller; QVector backHistory; QVector forwardHistory; bool isJumping; QPointer saveAll; QPointer revertAll; QPointer close; QPointer closeAll; QPointer closeAllOthers; KRecentFilesAction* fileOpenRecent; KTextEditor::Document* globalTextEditorInstance; }; Q_DECLARE_TYPEINFO(KDevelop::DocumentControllerPrivate::HistoryEntry, Q_MOVABLE_TYPE); DocumentController::DocumentController( QObject *parent ) : IDocumentController( parent ) , d(new DocumentControllerPrivate(this)) { setObjectName(QStringLiteral("DocumentController")); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/DocumentController"), this, QDBusConnection::ExportScriptableSlots ); connect(this, &DocumentController::documentUrlChanged, this, [&] (IDocument* document) { d->changeDocumentUrl(document); }); if(!(Core::self()->setupFlags() & Core::NoUi)) setupActions(); } void DocumentController::initialize() { } void DocumentController::cleanup() { if (d->fileOpenRecent) d->fileOpenRecent->saveEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) ); // Close all documents without checking if they should be saved. // This is because the user gets a chance to save them during MainWindow::queryClose. foreach (IDocument* doc, openDocuments()) doc->close(IDocument::Discard); } DocumentController::~DocumentController() = default; void DocumentController::setupActions() { KActionCollection* ac = Core::self()->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction* action; action = ac->addAction( QStringLiteral("file_open") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_O ); action->setText(i18n( "&Open..." ) ); connect( action, &QAction::triggered, this, [&] { d->chooseDocument(); } ); action->setToolTip( i18n( "Open file" ) ); action->setWhatsThis( i18n( "Opens a file for editing." ) ); d->fileOpenRecent = KStandardAction::openRecent(this, SLOT(slotOpenDocument(QUrl)), ac); d->fileOpenRecent->setWhatsThis(i18n("This lists files which you have opened recently, and allows you to easily open them again.")); d->fileOpenRecent->loadEntries( KConfigGroup(KSharedConfig::openConfig(), "Recent Files" ) ); action = d->saveAll = ac->addAction( QStringLiteral("file_save_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); action->setText(i18n( "Save Al&l" ) ); connect( action, &QAction::triggered, this, &DocumentController::slotSaveAllDocuments ); action->setToolTip( i18n( "Save all open documents" ) ); action->setWhatsThis( i18n( "Save all open documents, prompting for additional information when necessary." ) ); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_L) ); action->setEnabled(false); action = d->revertAll = ac->addAction( QStringLiteral("file_revert_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); action->setText(i18n( "Reload All" ) ); connect( action, &QAction::triggered, this, &DocumentController::reloadAllDocuments ); action->setToolTip( i18n( "Revert all open documents" ) ); action->setWhatsThis( i18n( "Revert all open documents, returning to the previously saved state." ) ); action->setEnabled(false); action = d->close = ac->addAction( QStringLiteral("file_close") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_W ); action->setText( i18n( "&Close" ) ); connect( action, &QAction::triggered, this, &DocumentController::fileClose ); action->setToolTip( i18n( "Close file" ) ); action->setWhatsThis( i18n( "Closes current file." ) ); action->setEnabled(false); action = d->closeAll = ac->addAction( QStringLiteral("file_close_all") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); action->setText(i18n( "Clos&e All" ) ); connect( action, &QAction::triggered, this, &DocumentController::closeAllDocuments ); action->setToolTip( i18n( "Close all open documents" ) ); action->setWhatsThis( i18n( "Close all open documents, prompting for additional information when necessary." ) ); action->setEnabled(false); action = d->closeAllOthers = ac->addAction( QStringLiteral("file_closeother") ); action->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); ac->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_W ); action->setText(i18n( "Close All Ot&hers" ) ); connect( action, &QAction::triggered, this, &DocumentController::closeAllOtherDocuments ); action->setToolTip( i18n( "Close all other documents" ) ); action->setWhatsThis( i18n( "Close all open documents, with the exception of the currently active document." ) ); action->setEnabled(false); action = ac->addAction( QStringLiteral("vcsannotate_current_document") ); connect( action, &QAction::triggered, this, &DocumentController::vcsAnnotateCurrentDocument ); action->setText( i18n( "Show Annotate on current document") ); action->setIconText( i18n( "Annotate" ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("user-properties")) ); } void DocumentController::slotOpenDocument(const QUrl &url) { openDocument(url); } IDocument* DocumentController::openDocumentFromText( const QString& data ) { IDocument* d = openDocument(nextEmptyDocumentUrl()); Q_ASSERT(d->textDocument()); d->textDocument()->setText( data ); return d; } bool DocumentController::openDocumentFromTextSimple( QString text ) { return (bool)openDocumentFromText( text ); } bool DocumentController::openDocumentSimple( QString url, int line, int column ) { return (bool)openDocument( QUrl::fromUserInput(url), KTextEditor::Cursor( line, column ) ); } IDocument* DocumentController::openDocument( const QUrl& inputUrl, const QString& prefName ) { return d->openDocumentInternal( inputUrl, prefName ); } IDocument* DocumentController::openDocument( const QUrl & inputUrl, const KTextEditor::Range& range, DocumentActivationParams activationParams, const QString& encoding, IDocument* buddy) { return d->openDocumentInternal(inputUrl, QString(), range, encoding, activationParams, buddy); } bool DocumentController::openDocument(IDocument* doc, const KTextEditor::Range& range, DocumentActivationParams activationParams, IDocument* buddy) { return d->openDocumentInternal( doc, range, activationParams, buddy); } void DocumentController::fileClose() { IDocument *activeDoc = activeDocument(); if (activeDoc) { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::View *activeView = uiController->activeSublimeWindow()->activeView(); uiController->activeArea()->closeView(activeView); } } bool DocumentController::closeDocument( const QUrl &url ) { if( !d->documents.contains(url) ) return false; //this will remove all views and after the last view is removed, the //document will be self-destructed and removeDocument() slot will catch that //and clean up internal data structures d->documents[url]->close(); return true; } void DocumentController::notifyDocumentClosed(Sublime::Document* doc_) { IDocument* doc = dynamic_cast(doc_); Q_ASSERT(doc); d->removeDocument(doc_); if (d->documents.isEmpty()) { if (d->saveAll) d->saveAll->setEnabled(false); if (d->revertAll) d->revertAll->setEnabled(false); if (d->close) d->close->setEnabled(false); if (d->closeAll) d->closeAll->setEnabled(false); if (d->closeAllOthers) d->closeAllOthers->setEnabled(false); } emit documentClosed(doc); } IDocument * DocumentController::documentForUrl( const QUrl & dirtyUrl ) const { if (dirtyUrl.isEmpty()) { return nullptr; } Q_ASSERT(!dirtyUrl.isRelative()); Q_ASSERT(!dirtyUrl.fileName().isEmpty() || !dirtyUrl.isLocalFile()); //Fix urls that might not be normalized return d->documents.value( dirtyUrl.adjusted( QUrl::NormalizePathSegments ), nullptr ); } QList DocumentController::openDocuments() const { QList opened; foreach (IDocument *doc, d->documents) { Sublime::Document *sdoc = dynamic_cast(doc); if( !sdoc ) { continue; } if (!sdoc->views().isEmpty()) opened << doc; } return opened; } void DocumentController::activateDocument( IDocument * document, const KTextEditor::Range& range ) { // TODO avoid some code in openDocument? Q_ASSERT(document); openDocument(document->url(), range, IDocumentController::DoNotAddToRecentOpen); } void DocumentController::slotSaveAllDocuments() { saveAllDocuments(IDocument::Silent); } bool DocumentController::saveAllDocuments(IDocument::DocumentSaveMode mode) { return saveSomeDocuments(openDocuments(), mode); } bool KDevelop::DocumentController::saveSomeDocuments(const QList< IDocument * > & list, IDocument::DocumentSaveMode mode) { if (mode & IDocument::Silent) { foreach (IDocument* doc, modifiedDocuments(list)) { if( !DocumentController::isEmptyDocumentUrl(doc->url()) && !doc->save(mode) ) { if( doc ) qCWarning(SHELL) << "!! Could not save document:" << doc->url(); else qCWarning(SHELL) << "!! Could not save document as its NULL"; } // TODO if (!ret) showErrorDialog() ? } } else { // Ask the user which documents to save QList checkSave = modifiedDocuments(list); if (!checkSave.isEmpty()) { ScopedDialog dialog(checkSave, qApp->activeWindow()); return dialog->exec(); } } return true; } QList< IDocument * > KDevelop::DocumentController::visibleDocumentsInWindow(MainWindow * mw) const { // Gather a list of all documents which do have a view in the given main window // Does not find documents which are open in inactive areas QList list; foreach (IDocument* doc, openDocuments()) { if (Sublime::Document* sdoc = dynamic_cast(doc)) { foreach (Sublime::View* view, sdoc->views()) { if (view->hasWidget() && view->widget()->window() == mw) { list.append(doc); break; } } } } return list; } QList< IDocument * > KDevelop::DocumentController::documentsExclusivelyInWindow(MainWindow * mw, bool currentAreaOnly) const { // Gather a list of all documents which have views only in the given main window QList checkSave; foreach (IDocument* doc, openDocuments()) { if (Sublime::Document* sdoc = dynamic_cast(doc)) { bool inOtherWindow = false; foreach (Sublime::View* view, sdoc->views()) { foreach(Sublime::MainWindow* window, Core::self()->uiControllerInternal()->mainWindows()) if(window->containsView(view) && (window != mw || (currentAreaOnly && window == mw && !mw->area()->views().contains(view)))) inOtherWindow = true; } if (!inOtherWindow) checkSave.append(doc); } } return checkSave; } QList< IDocument * > KDevelop::DocumentController::modifiedDocuments(const QList< IDocument * > & list) const { QList< IDocument * > ret; foreach (IDocument* doc, list) if (doc->state() == IDocument::Modified || doc->state() == IDocument::DirtyAndModified) ret.append(doc); return ret; } bool DocumentController::saveAllDocumentsForWindow(KParts::MainWindow* mw, KDevelop::IDocument::DocumentSaveMode mode, bool currentAreaOnly) { QList checkSave = documentsExclusivelyInWindow(dynamic_cast(mw), currentAreaOnly); return saveSomeDocuments(checkSave, mode); } void DocumentController::reloadAllDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { QList views = visibleDocumentsInWindow(dynamic_cast(mw)); if (!saveSomeDocuments(views, IDocument::Default)) // User cancelled or other error return; foreach (IDocument* doc, views) if(!isEmptyDocumentUrl(doc->url())) doc->reload(); } } bool DocumentController::closeAllDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { QList views = visibleDocumentsInWindow(dynamic_cast(mw)); if (!saveSomeDocuments(views, IDocument::Default)) // User cancelled or other error return false; foreach (IDocument* doc, views) doc->close(IDocument::Discard); } return true; } void DocumentController::closeAllOtherDocuments() { if (Sublime::MainWindow* mw = Core::self()->uiControllerInternal()->activeSublimeWindow()) { Sublime::View* activeView = mw->activeView(); if (!activeView) { qCWarning(SHELL) << "Shouldn't there always be an active view when this function is called?"; return; } // Deal with saving unsaved solo views QList soloViews = documentsExclusivelyInWindow(dynamic_cast(mw)); soloViews.removeAll(dynamic_cast(activeView->document())); if (!saveSomeDocuments(soloViews, IDocument::Default)) // User cancelled or other error return; foreach (Sublime::View* view, mw->area()->views()) { if (view != activeView) mw->area()->closeView(view); } activeView->widget()->setFocus(); } } IDocument* DocumentController::activeDocument() const { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::MainWindow* mw = uiController->activeSublimeWindow(); if( !mw || !mw->activeView() ) return nullptr; return dynamic_cast(mw->activeView()->document()); } KTextEditor::View* DocumentController::activeTextDocumentView() const { UiController *uiController = Core::self()->uiControllerInternal(); Sublime::MainWindow* mw = uiController->activeSublimeWindow(); if( !mw || !mw->activeView() ) return nullptr; TextView* view = qobject_cast(mw->activeView()); if(!view) return nullptr; return view->textView(); } QString DocumentController::activeDocumentPath( const QString& target ) const { if(!target.isEmpty()) { foreach(IProject* project, Core::self()->projectController()->projects()) { if(project->name().startsWith(target, Qt::CaseInsensitive)) { return project->path().pathOrUrl() + QLatin1String("/."); } } } IDocument* doc = activeDocument(); if(!doc || target == QStringLiteral("[selection]")) { Context* selection = ICore::self()->selectionController()->currentSelection(); if(selection && selection->type() == Context::ProjectItemContext && !static_cast(selection)->items().isEmpty()) { QString ret = static_cast(selection)->items().at(0)->path().pathOrUrl(); if(static_cast(selection)->items().at(0)->folder()) ret += QStringLiteral("/."); return ret; } return QString(); } return doc->url().toString(); } QStringList DocumentController::activeDocumentPaths() const { UiController *uiController = Core::self()->uiControllerInternal(); if( !uiController->activeSublimeWindow() ) return QStringList(); QSet documents; foreach(Sublime::View* view, uiController->activeSublimeWindow()->area()->views()) documents.insert(view->document()->documentSpecifier()); return documents.toList(); } void DocumentController::registerDocumentForMimetype( const QString& mimetype, KDevelop::IDocumentFactory* factory ) { if( !d->factories.contains( mimetype ) ) d->factories[mimetype] = factory; } QStringList DocumentController::documentTypes() const { return QStringList() << QStringLiteral("Text"); } static const QRegularExpression& emptyDocumentPattern() { static const QRegularExpression pattern(QStringLiteral("^/%1(?:\\s\\((\\d+)\\))?$").arg(EMPTY_DOCUMENT_URL)); return pattern; } bool DocumentController::isEmptyDocumentUrl(const QUrl &url) { return emptyDocumentPattern().match(url.toDisplayString(QUrl::PreferLocalFile)).hasMatch(); } QUrl DocumentController::nextEmptyDocumentUrl() { int nextEmptyDocNumber = 0; const auto& pattern = emptyDocumentPattern(); foreach (IDocument *doc, Core::self()->documentControllerInternal()->openDocuments()) { if (DocumentController::isEmptyDocumentUrl(doc->url())) { const auto match = pattern.match(doc->url().toDisplayString(QUrl::PreferLocalFile)); if (match.hasMatch()) { const int num = match.captured(1).toInt(); nextEmptyDocNumber = qMax(nextEmptyDocNumber, num + 1); } else { nextEmptyDocNumber = qMax(nextEmptyDocNumber, 1); } } } QUrl url; if (nextEmptyDocNumber > 0) url = QUrl::fromLocalFile(QStringLiteral("/%1 (%2)").arg(EMPTY_DOCUMENT_URL).arg(nextEmptyDocNumber)); else url = QUrl::fromLocalFile(QLatin1Char('/') + EMPTY_DOCUMENT_URL); return url; } IDocumentFactory* DocumentController::factory(const QString& mime) const { return d->factories.value(mime); } KTextEditor::Document* DocumentController::globalTextEditorInstance() { if(!d->globalTextEditorInstance) d->globalTextEditorInstance = Core::self()->partControllerInternal()->createTextPart(); return d->globalTextEditorInstance; } bool DocumentController::openDocumentsSimple( QStringList urls ) { Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea(); Sublime::AreaIndex* areaIndex = area->rootIndex(); QList topViews = static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->topViews(); if(Sublime::View* activeView = Core::self()->uiControllerInternal()->activeSublimeWindow()->activeView()) areaIndex = area->indexOf(activeView); qCDebug(SHELL) << "opening " << urls << " to area " << area << " index " << areaIndex << " with children " << areaIndex->first() << " " << areaIndex->second(); bool isFirstView = true; bool ret = openDocumentsWithSplitSeparators( areaIndex, urls, isFirstView ); qCDebug(SHELL) << "area arch. after opening: " << areaIndex->print(); // Required because sublime sometimes doesn't update correctly when the area-index contents has been changed // (especially when views have been moved to other indices, through unsplit, split, etc.) static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->reconstructViews(topViews); return ret; } bool DocumentController::openDocumentsWithSplitSeparators( Sublime::AreaIndex* index, QStringList urlsWithSeparators, bool& isFirstView ) { qCDebug(SHELL) << "opening " << urlsWithSeparators << " index " << index << " with children " << index->first() << " " << index->second() << " view-count " << index->viewCount(); if(urlsWithSeparators.isEmpty()) return true; Sublime::Area* area = Core::self()->uiControllerInternal()->activeArea(); QList topLevelSeparators; // Indices of the top-level separators (with groups skipped) - QStringList separators = QStringList() << QStringLiteral("/") << QStringLiteral("-"); + const QStringList separators {QStringLiteral("/"), QStringLiteral("-")}; QList groups; bool ret = true; { int parenDepth = 0; int groupStart = 0; for(int pos = 0; pos < urlsWithSeparators.size(); ++pos) { QString item = urlsWithSeparators[pos]; if(separators.contains(item)) { if(parenDepth == 0) topLevelSeparators << pos; }else if(item == QLatin1String("[")) { if(parenDepth == 0) groupStart = pos+1; ++parenDepth; } else if(item == QLatin1String("]")) { if(parenDepth > 0) { --parenDepth; if(parenDepth == 0) groups << urlsWithSeparators.mid(groupStart, pos-groupStart); } else{ qCDebug(SHELL) << "syntax error in " << urlsWithSeparators << ": parens do not match"; ret = false; } }else if(parenDepth == 0) { groups << (QStringList() << item); } } } if(topLevelSeparators.isEmpty()) { if(urlsWithSeparators.size() > 1) { foreach(const QStringList& group, groups) ret &= openDocumentsWithSplitSeparators( index, group, isFirstView ); }else{ while(index->isSplit()) index = index->first(); // Simply open the document into the area index IDocument* doc = Core::self()->documentControllerInternal()->openDocument(QUrl::fromUserInput(urlsWithSeparators.front()), KTextEditor::Cursor::invalid(), IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView); Sublime::Document *sublimeDoc = dynamic_cast(doc); if (sublimeDoc) { Sublime::View* view = sublimeDoc->createView(); area->addView(view, index); if(isFirstView) { static_cast(Core::self()->uiControllerInternal()->activeMainWindow())->activateView(view); isFirstView = false; } }else{ ret = false; } } return ret; } // Pick a separator in the middle int pickSeparator = topLevelSeparators[topLevelSeparators.size()/2]; bool activeViewToSecondChild = false; if(pickSeparator == urlsWithSeparators.size()-1) { // There is no right child group, so the right side should be filled with the currently active views activeViewToSecondChild = true; }else{ QStringList separatorsAndParens = separators; separatorsAndParens << QStringLiteral("[") << QStringLiteral("]"); // Check if the second child-set contains an unterminated separator, which means that the active views should end up there for(int pos = pickSeparator+1; pos < urlsWithSeparators.size(); ++pos) if( separators.contains(urlsWithSeparators[pos]) && (pos == urlsWithSeparators.size()-1 || separatorsAndParens.contains(urlsWithSeparators[pos-1])) ) activeViewToSecondChild = true; } Qt::Orientation orientation = urlsWithSeparators[pickSeparator] == QLatin1String("/") ? Qt::Horizontal : Qt::Vertical; if(!index->isSplit()) { qCDebug(SHELL) << "splitting " << index << "orientation" << orientation << "to second" << activeViewToSecondChild; index->split(orientation, activeViewToSecondChild); }else{ index->setOrientation(orientation); qCDebug(SHELL) << "WARNING: Area is already split (shouldn't be)" << urlsWithSeparators; } openDocumentsWithSplitSeparators( index->first(), urlsWithSeparators.mid(0, pickSeparator) , isFirstView ); if(pickSeparator != urlsWithSeparators.size() - 1) openDocumentsWithSplitSeparators( index->second(), urlsWithSeparators.mid(pickSeparator+1, urlsWithSeparators.size() - (pickSeparator+1) ), isFirstView ); // Clean up the child-indices, because document-loading may fail if(!index->first()->viewCount() && !index->first()->isSplit()) { qCDebug(SHELL) << "unsplitting first"; index->unsplit(index->first()); } else if(!index->second()->viewCount() && !index->second()->isSplit()) { qCDebug(SHELL) << "unsplitting second"; index->unsplit(index->second()); } return ret; } void DocumentController::vcsAnnotateCurrentDocument() { IDocument* doc = activeDocument(); if (!doc) return; QUrl url = doc->url(); IProject* project = KDevelop::ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IBasicVersionControl* iface = project->versionControlPlugin()->extension(); auto helper = new VcsPluginHelper(project->versionControlPlugin(), iface); connect(doc->textDocument(), &KTextEditor::Document::aboutToClose, helper, static_cast(&VcsPluginHelper::disposeEventually)); Q_ASSERT(qobject_cast(doc->activeTextView())); // can't use new signal slot syntax here, AnnotationViewInterface is not a QObject connect(doc->activeTextView(), SIGNAL(annotationBorderVisibilityChanged(View*,bool)), helper, SLOT(disposeEventually(View*,bool))); helper->addContextDocument(url); helper->annotation(); } else { KMessageBox::error(nullptr, i18n("Could not annotate the document because it is not " "part of a version-controlled project.")); } } #include "moc_documentcontroller.cpp" diff --git a/kdevplatform/shell/languagecontroller.cpp b/kdevplatform/shell/languagecontroller.cpp index 3d3f530768..1b46df2820 100644 --- a/kdevplatform/shell/languagecontroller.cpp +++ b/kdevplatform/shell/languagecontroller.cpp @@ -1,379 +1,381 @@ /*************************************************************************** * Copyright 2006 Adam Treat * * Copyright 2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "languagecontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "problemmodelset.h" #include "core.h" #include "settings/languagepreferences.h" #include "completionsettings.h" #include "debug.h" namespace { QString KEY_SupportedMimeTypes() { return QStringLiteral("X-KDevelop-SupportedMimeTypes"); } QString KEY_ILanguageSupport() { return QStringLiteral("ILanguageSupport"); } } #if QT_VERSION < 0x050600 inline uint qHash(const QMimeType& mime, uint seed = 0) { return qHash(mime.name(), seed); } #endif namespace KDevelop { typedef QHash LanguageHash; typedef QHash > LanguageCache; class LanguageControllerPrivate { public: explicit LanguageControllerPrivate(LanguageController *controller) : dataMutex(QMutex::Recursive) , backgroundParser(new BackgroundParser(controller)) , staticAssistantsManager(nullptr) , m_cleanedUp(false) , problemModelSet(new ProblemModelSet(controller)) , m_controller(controller) {} void documentActivated(KDevelop::IDocument *document) { QUrl url = document->url(); if (!url.isValid()) { return; } activeLanguages.clear(); QList languages = m_controller->languagesForUrl(url); + activeLanguages.reserve(languages.size()); foreach (const auto lang, languages) { activeLanguages << lang; } } QList activeLanguages; mutable QMutex dataMutex; LanguageHash languages; //Maps language-names to languages LanguageCache languageCache; //Maps mimetype-names to languages typedef QMultiHash MimeTypeCache; MimeTypeCache mimeTypeCache; //Maps mimetypes to languages BackgroundParser *backgroundParser; StaticAssistantsManager* staticAssistantsManager; bool m_cleanedUp; void addLanguageSupport(ILanguageSupport* support, const QStringList& mimetypes); void addLanguageSupport(ILanguageSupport* support); ProblemModelSet *problemModelSet; private: LanguageController *m_controller; }; void LanguageControllerPrivate::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes) { Q_ASSERT(!languages.contains(languageSupport->name())); languages.insert(languageSupport->name(), languageSupport); foreach(const QString& mimeTypeName, mimetypes) { qCDebug(SHELL) << "adding supported mimetype:" << mimeTypeName << "language:" << languageSupport->name(); languageCache[mimeTypeName] << languageSupport; QMimeType mime = QMimeDatabase().mimeTypeForName(mimeTypeName); if (mime.isValid()) { mimeTypeCache.insert(mime, languageSupport); } else { qCWarning(SHELL) << "could not create mime-type" << mimeTypeName; } } } void LanguageControllerPrivate::addLanguageSupport(KDevelop::ILanguageSupport* languageSupport) { if (languages.contains(languageSupport->name())) return; Q_ASSERT(dynamic_cast(languageSupport)); KPluginMetaData info = Core::self()->pluginController()->pluginInfo(dynamic_cast(languageSupport)); QStringList mimetypes = KPluginMetaData::readStringList(info.rawData(), KEY_SupportedMimeTypes()); addLanguageSupport(languageSupport, mimetypes); } LanguageController::LanguageController(QObject *parent) : ILanguageController(parent) , d(new LanguageControllerPrivate(this)) { setObjectName(QStringLiteral("LanguageController")); } LanguageController::~LanguageController() = default; void LanguageController::initialize() { d->backgroundParser->loadSettings(); d->staticAssistantsManager = new StaticAssistantsManager(this); // make sure the DUChain is setup before we try to access it from different threads at the same time DUChain::self(); connect(Core::self()->documentController(), &IDocumentController::documentActivated, this, [&] (IDocument* document) { d->documentActivated(document); }); } void LanguageController::cleanup() { QMutexLocker lock(&d->dataMutex); d->m_cleanedUp = true; } QList LanguageController::activeLanguages() { QMutexLocker lock(&d->dataMutex); return d->activeLanguages; } StaticAssistantsManager* LanguageController::staticAssistantsManager() const { return d->staticAssistantsManager; } ICompletionSettings *LanguageController::completionSettings() const { return &CompletionSettings::self(); } ProblemModelSet* LanguageController::problemModelSet() const { return d->problemModelSet; } QList LanguageController::loadedLanguages() const { QMutexLocker lock(&d->dataMutex); QList ret; if(d->m_cleanedUp) return ret; + ret.reserve(d->languages.size()); foreach(ILanguageSupport* lang, d->languages) ret << lang; return ret; } ILanguageSupport* LanguageController::language(const QString &name) const { QMutexLocker lock(&d->dataMutex); if(d->m_cleanedUp) return nullptr; if(d->languages.contains(name)) return d->languages[name]; // temporary support for deprecated-in-5.1 "X-KDevelop-Language" as fallback // remove in later version const QString keys[2] = { QStringLiteral("X-KDevelop-Languages"), QStringLiteral("X-KDevelop-Language") }; QList supports; for (const auto& key : keys) { QVariantMap constraints; constraints.insert(key, name); supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); if (key == keys[1]) { for (auto support : supports) { qCWarning(SHELL) << "Plugin" << Core::self()->pluginController()->pluginInfo(support).name() << " has deprecated (since 5.1) metadata key \"X-KDevelop-Language\", needs porting to: \"X-KDevelop-Languages\": ["<extension(); if(languageSupport) { d->addLanguageSupport(languageSupport); return languageSupport; } } return nullptr; } bool isNumeric(const QString& str) { int len = str.length(); if(len == 0) return false; for(int a = 0; a < len; ++a) if(!str[a].isNumber()) return false; return true; } QList LanguageController::languagesForUrl(const QUrl &url) { QMutexLocker lock(&d->dataMutex); QList languages; if(d->m_cleanedUp) return languages; const QString fileName = url.fileName(); ///TODO: cache regexp or simple string pattern for endsWith matching QRegExp exp(QString(), Qt::CaseInsensitive, QRegExp::Wildcard); ///non-crashy part: Use the mime-types of known languages for(LanguageControllerPrivate::MimeTypeCache::const_iterator it = d->mimeTypeCache.constBegin(); it != d->mimeTypeCache.constEnd(); ++it) { foreach(const QString& pattern, it.key().globPatterns()) { if(pattern.startsWith(QLatin1Char('*'))) { const QStringRef subPattern = pattern.midRef(1); if (!subPattern.contains(QLatin1Char('*'))) { //optimize: we can skip the expensive QRegExp in this case //and do a simple string compare (much faster) if (fileName.endsWith(subPattern)) { languages << *it; } continue; } } exp.setPattern(pattern); if(int position = exp.indexIn(fileName)) { if(position != -1 && exp.matchedLength() + position == fileName.length()) languages << *it; } } } if(!languages.isEmpty()) return languages; //Never use findByUrl from within a background thread, and never load a language support //from within the backgruond thread. Both is unsafe, and can lead to crashes if(!languages.isEmpty() || QThread::currentThread() != thread()) return languages; QMimeType mimeType; if (url.isLocalFile()) { mimeType = QMimeDatabase().mimeTypeForFile(url.toLocalFile()); } else { // remote file, only look at the extension mimeType = QMimeDatabase().mimeTypeForUrl(url); } if (mimeType.isDefault()) { // ask the document controller about a more concrete mimetype IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (doc) { mimeType = doc->mimeType(); } } languages = languagesForMimetype(mimeType.name()); return languages; } QList LanguageController::languagesForMimetype(const QString& mimetype) { QMutexLocker lock(&d->dataMutex); QList languages; LanguageCache::ConstIterator it = d->languageCache.constFind(mimetype); if (it != d->languageCache.constEnd()) { languages = it.value(); } else { QVariantMap constraints; constraints.insert(KEY_SupportedMimeTypes(), mimetype); QList supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); if (supports.isEmpty()) { qCDebug(SHELL) << "no languages for mimetype:" << mimetype; d->languageCache.insert(mimetype, QList()); } else { foreach (IPlugin *support, supports) { ILanguageSupport* languageSupport = support->extension(); qCDebug(SHELL) << "language-support:" << languageSupport; if(languageSupport) { d->addLanguageSupport(languageSupport); languages << languageSupport; } } } } return languages; } QList LanguageController::mimetypesForLanguageName(const QString& languageName) { QMutexLocker lock(&d->dataMutex); QList mimetypes; for (LanguageCache::ConstIterator iter = d->languageCache.constBegin(); iter != d->languageCache.constEnd(); ++iter) { foreach (ILanguageSupport* language, iter.value()) { if (language->name() == languageName) { mimetypes << iter.key(); break; } } } return mimetypes; } BackgroundParser *LanguageController::backgroundParser() const { return d->backgroundParser; } void LanguageController::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes) { d->addLanguageSupport(languageSupport, mimetypes); } } #include "moc_languagecontroller.cpp" diff --git a/kdevplatform/shell/launchconfigurationdialog.cpp b/kdevplatform/shell/launchconfigurationdialog.cpp index 38d68d1035..259c65e8c5 100644 --- a/kdevplatform/shell/launchconfigurationdialog.cpp +++ b/kdevplatform/shell/launchconfigurationdialog.cpp @@ -1,1026 +1,1027 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "launchconfigurationdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "runcontroller.h" #include "launchconfiguration.h" #include "debug.h" #include #include #include namespace KDevelop { bool launchConfigGreaterThan(KDevelop::LaunchConfigurationType* a, KDevelop::LaunchConfigurationType* b) { return a->name()>b->name(); } //TODO: Maybe use KPageDialog instead, might make the model stuff easier and the default-size stuff as well LaunchConfigurationDialog::LaunchConfigurationDialog(QWidget* parent) : QDialog(parent) { setWindowTitle( i18n( "Launch Configurations" ) ); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(mainWidget); setupUi(mainWidget); - splitter->setSizes(QList() << 260 << 620); + splitter->setSizes(QList{260, 620}); splitter->setCollapsible(0, false); addConfig->setToolTip(i18nc("@info:tooltip", "Add a new launch configuration.")); deleteConfig->setEnabled( false ); deleteConfig->setToolTip(i18nc("@info:tooltip", "Delete selected launch configuration.")); model = new LaunchConfigurationsModel( tree ); tree->setModel( model ); tree->setExpandsOnDoubleClick( true ); tree->setSelectionBehavior( QAbstractItemView::SelectRows ); tree->setSelectionMode( QAbstractItemView::SingleSelection ); tree->setUniformRowHeights( true ); tree->setItemDelegate( new LaunchConfigurationModelDelegate(this) ); tree->setColumnHidden(1, true); for(int row=0; rowrowCount(); row++) { tree->setExpanded(model->index(row, 0), true); } tree->setContextMenuPolicy(Qt::CustomContextMenu); connect( tree, &QTreeView::customContextMenuRequested, this, &LaunchConfigurationDialog::doTreeContextMenu ); connect( deleteConfig, &QPushButton::clicked, this, &LaunchConfigurationDialog::deleteConfiguration); connect( model, &LaunchConfigurationsModel::dataChanged, this, &LaunchConfigurationDialog::modelChanged ); connect( tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &LaunchConfigurationDialog::selectionChanged); QModelIndex idx = model->indexForConfig( Core::self()->runControllerInternal()->defaultLaunch() ); qCDebug(SHELL) << "selecting index:" << idx; if( !idx.isValid() ) { for( int i = 0; i < model->rowCount(); i++ ) { if( model->rowCount( model->index( i, 0, QModelIndex() ) ) > 0 ) { idx = model->index( 1, 0, model->index( i, 0, QModelIndex() ) ); break; } } if( !idx.isValid() ) { idx = model->index( 0, 0, QModelIndex() ); } } tree->selectionModel()->select( QItemSelection( idx, idx ), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); // Unfortunately tree->resizeColumnToContents() only looks at the top-level // items, instead of all open ones. Hence we're calculating it ourselves like // this: // Take the selected index, check if it has childs, if so take the first child // Then count the level by going up, then let the tree calculate the width // for the selected or its first child index and add indentation*level // // If Qt Software ever fixes resizeColumnToContents, the following line // can be enabled and the rest be removed // tree->resizeColumnToContents( 0 ); int level = 0; QModelIndex widthidx = idx; if( model->rowCount( idx ) > 0 ) { widthidx = model->index( 0, 0, idx ); } QModelIndex parentidx = widthidx.parent(); while( parentidx.isValid() ) { level++; parentidx = parentidx.parent(); } // make sure the base column width is honored, e.g. when no launch configs exist tree->resizeColumnToContents(0); int width = tree->columnWidth( 0 ); while ( widthidx.isValid() ) { width = qMax( width, level*tree->indentation() + tree->indentation() + tree->sizeHintForIndex( widthidx ).width() ); widthidx = widthidx.parent(); } tree->setColumnWidth( 0, width ); QMenu* m = new QMenu(this); QList types = Core::self()->runController()->launchConfigurationTypes(); std::sort(types.begin(), types.end(), launchConfigGreaterThan); //we want it in reverse order foreach(LaunchConfigurationType* type, types) { connect(type, &LaunchConfigurationType::signalAddLaunchConfiguration, this, &LaunchConfigurationDialog::addConfiguration); QMenu* suggestionsMenu = type->launcherSuggestions(); if(suggestionsMenu) { // take ownership suggestionsMenu->setParent(m, suggestionsMenu->windowFlags()); m->addMenu(suggestionsMenu); } } // Simplify menu structure to get rid of 1-entry levels while (m->actions().count() == 1) { QMenu* subMenu = m->actions().at(0)->menu(); if (subMenu && subMenu->isEnabled() && subMenu->actions().count()<5) { m = subMenu; } else { break; } } if(!m->isEmpty()) { QAction* separator = new QAction(m); separator->setSeparator(true); m->insertAction(m->actions().at(0), separator); } foreach(LaunchConfigurationType* type, types) { QAction* action = new QAction(type->icon(), type->name(), m); action->setProperty("configtype", qVariantFromValue(type)); connect(action, &QAction::triggered, this, &LaunchConfigurationDialog::createEmptyLauncher); if(!m->actions().isEmpty()) m->insertAction(m->actions().at(0), action); else m->addAction(action); } addConfig->setMenu(m); addConfig->setEnabled( !m->isEmpty() ); messageWidget->setCloseButtonVisible( false ); messageWidget->setMessageType( KMessageWidget::Warning ); messageWidget->setText( i18n("No launch configurations available. (Is any of the Execute plugins loaded?)") ); messageWidget->setVisible( m->isEmpty() ); connect(debugger, static_cast(&QComboBox::currentIndexChanged), this, &LaunchConfigurationDialog::launchModeChanged); connect(buttonBox, &QDialogButtonBox::accepted, this, &LaunchConfigurationDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &LaunchConfigurationDialog::reject); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, static_cast(&LaunchConfigurationDialog::saveConfig) ); connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, static_cast(&LaunchConfigurationDialog::saveConfig) ); mainLayout->addWidget(buttonBox); resize( QSize(qMax(1200, sizeHint().width()), qMax(500, sizeHint().height())) ); } void LaunchConfigurationDialog::doTreeContextMenu(const QPoint& point) { if ( ! tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex selected = tree->selectionModel()->selectedRows().first(); if ( selected.parent().isValid() && ! selected.parent().parent().isValid() ) { // only display the menu if a launch config is clicked QMenu menu(tree); QAction* rename = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename configuration"), &menu); QAction* delete_ = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete configuration"), &menu); connect(rename, &QAction::triggered, this, &LaunchConfigurationDialog::renameSelected); connect(delete_, &QAction::triggered, this, &LaunchConfigurationDialog::deleteConfiguration); menu.addAction(rename); menu.addAction(delete_); menu.exec(tree->viewport()->mapToGlobal(point)); } } } void LaunchConfigurationDialog::renameSelected() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex parent = tree->selectionModel()->selectedRows().first(); if( parent.parent().isValid() ) { parent = parent.parent(); } QModelIndex index = model->index(tree->selectionModel()->selectedRows().first().row(), 0, parent); tree->edit( index ); } } QSize LaunchConfigurationDialog::sizeHint() const { QSize s = QDialog::sizeHint(); return s.expandedTo(QSize(880, 520)); } void LaunchConfigurationDialog::createEmptyLauncher() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); LaunchConfigurationType* type = qobject_cast(action->property("configtype").value()); Q_ASSERT(type); IProject* p = model->projectForIndex(tree->currentIndex()); QPair< QString, QString > launcher( type->launchers().at( 0 )->supportedModes().at(0), type->launchers().at( 0 )->id() ); ILaunchConfiguration* l = ICore::self()->runController()->createLaunchConfiguration(type, launcher, p); addConfiguration(l); } void LaunchConfigurationDialog::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { if( !deselected.indexes().isEmpty() ) { LaunchConfiguration* l = model->configForIndex( deselected.indexes().first() ); if( l ) { disconnect(l, &LaunchConfiguration::nameChanged, this, &LaunchConfigurationDialog::updateNameLabel); if( currentPageChanged ) { if( KMessageBox::questionYesNo( this, i18n("Selected Launch Configuration has unsaved changes. Do you want to save it?"), i18n("Unsaved Changes") ) == KMessageBox::Yes ) { saveConfig( deselected.indexes().first() ); } else { LaunchConfigPagesContainer* tab = qobject_cast( stack->currentWidget() ); tab->setLaunchConfiguration( l ); buttonBox->button(QDialogButtonBox::Apply)->setEnabled( false ); currentPageChanged = false; } } } } updateNameLabel(nullptr); for( int i = 1; i < stack->count(); i++ ) { QWidget* w = stack->widget(i); stack->removeWidget(w); delete w; } if( !selected.indexes().isEmpty() ) { QModelIndex idx = selected.indexes().first(); LaunchConfiguration* l = model->configForIndex( idx ); ILaunchMode* lm = model->modeForIndex( idx ); if( l ) { updateNameLabel( l ); tree->expand( model->indexForConfig( l ) ); connect( l, &LaunchConfiguration::nameChanged, this, &LaunchConfigurationDialog::updateNameLabel ); if( lm ) { QVariant currentLaunchMode = idx.sibling(idx.row(), 1).data(Qt::EditRole); { QSignalBlocker blocker(debugger); QList launchers = l->type()->launchers(); debugger->clear(); for( QList::const_iterator it = launchers.constBegin(); it != launchers.constEnd(); ++it ) { if( ((*it)->supportedModes().contains( lm->id() ) ) ) { debugger->addItem( (*it)->name(), (*it)->id() ); } } debugger->setCurrentIndex(debugger->findData(currentLaunchMode)); } debugger->setVisible(debugger->count()>0); debugLabel->setVisible(debugger->count()>0); ILauncher* launcher = l->type()->launcherForId( currentLaunchMode.toString() ); if( launcher ) { LaunchConfigPagesContainer* tab = launcherWidgets.value( launcher ); if(!tab) { QList pages = launcher->configPages(); if(!pages.isEmpty()) { tab = new LaunchConfigPagesContainer( launcher->configPages(), stack ); connect( tab, &LaunchConfigPagesContainer::changed, this, &LaunchConfigurationDialog::pageChanged ); stack->addWidget( tab ); } } if(tab) { tab->setLaunchConfiguration( l ); stack->setCurrentWidget( tab ); } else { QLabel* label = new QLabel(i18nc("%1 is a launcher name", "No configuration is needed for '%1'", launcher->name()), stack); label->setAlignment(Qt::AlignCenter); QFont font = label->font(); font.setItalic(true); label->setFont(font); stack->addWidget(label); stack->setCurrentWidget(label); } updateNameLabel( l ); addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); } else { addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); } } else { //TODO: enable removal button LaunchConfigurationType* type = l->type(); LaunchConfigPagesContainer* tab = typeWidgets.value( type ); if( !tab ) { tab = new LaunchConfigPagesContainer( type->configPages(), stack ); connect( tab, &LaunchConfigPagesContainer::changed, this, &LaunchConfigurationDialog::pageChanged ); stack->addWidget( tab ); } qCDebug(SHELL) << "created pages, setting config up"; tab->setLaunchConfiguration( l ); stack->setCurrentWidget( tab ); addConfig->setEnabled( addConfig->menu() && !addConfig->menu()->isEmpty() ); deleteConfig->setEnabled( true ); debugger->setVisible( false ); debugLabel->setVisible( false ); } } else { addConfig->setEnabled( addConfig->menu() && !addConfig->menu()->isEmpty() ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); QLabel* l = new QLabel(i18n("Select a configuration to edit from the left,
" "or click the \"Add\" button to add a new one.
"), stack); l->setAlignment(Qt::AlignCenter); stack->addWidget(l); stack->setCurrentWidget(l); debugger->setVisible( false ); debugLabel->setVisible( false ); } } else { debugger->setVisible( false ); debugLabel->setVisible( false ); addConfig->setEnabled( false ); deleteConfig->setEnabled( false ); stack->setCurrentIndex( 0 ); } } void LaunchConfigurationDialog::saveConfig( const QModelIndex& idx ) { Q_UNUSED( idx ); LaunchConfigPagesContainer* tab = qobject_cast( stack->currentWidget() ); if( tab ) { tab->save(); buttonBox->button(QDialogButtonBox::Apply)->setEnabled( false ); currentPageChanged = false; } } void LaunchConfigurationDialog::saveConfig() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { saveConfig( tree->selectionModel()->selectedRows().first() ); } } void LaunchConfigurationDialog::pageChanged() { currentPageChanged = true; buttonBox->button(QDialogButtonBox::Apply)->setEnabled( true ); } void LaunchConfigurationDialog::modelChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (tree->selectionModel()) { QModelIndex index = tree->selectionModel()->selectedRows().first(); if (index.row() >= topLeft.row() && index.row() <= bottomRight.row() && bottomRight.column() == 1) selectionChanged(tree->selectionModel()->selection(), tree->selectionModel()->selection()); } } void LaunchConfigurationDialog::deleteConfiguration() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { model->deleteConfiguration( tree->selectionModel()->selectedRows().first() ); tree->resizeColumnToContents( 0 ); } } void LaunchConfigurationDialog::updateNameLabel( LaunchConfiguration* l ) { if( l ) { configName->setText( i18n("Editing %2: %1", l->name(), l->type()->name() ) ); } else { configName->clear(); } } void LaunchConfigurationDialog::createConfiguration() { if( !tree->selectionModel()->selectedRows().isEmpty() ) { QModelIndex idx = tree->selectionModel()->selectedRows().first(); if( idx.parent().isValid() ) { idx = idx.parent(); } model->createConfiguration( idx ); QModelIndex newindex = model->index( model->rowCount( idx ) - 1, 0, idx ); tree->selectionModel()->select( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->edit( newindex ); tree->resizeColumnToContents( 0 ); } } void LaunchConfigurationDialog::addConfiguration(ILaunchConfiguration* _launch) { LaunchConfiguration* launch = dynamic_cast(_launch); Q_ASSERT(launch); int row = launch->project() ? model->findItemForProject(launch->project())->row : 0; QModelIndex idx = model->index(row, 0); model->addConfiguration(launch, idx); QModelIndex newindex = model->index( model->rowCount( idx ) - 1, 0, idx ); tree->selectionModel()->select( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->selectionModel()->setCurrentIndex( newindex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); tree->edit( newindex ); tree->resizeColumnToContents( 0 ); } LaunchConfigurationsModel::LaunchConfigurationsModel(QObject* parent): QAbstractItemModel(parent) { GenericPageItem* global = new GenericPageItem; global->text = i18n("Global"); global->row = 0; + const auto projects = Core::self()->projectController()->projects(); + topItems.reserve(1 + projects.size()); topItems << global; - foreach( IProject* p, Core::self()->projectController()->projects() ) - { + for (IProject* p : projects) { ProjectItem* t = new ProjectItem; t->project = p; t->row = topItems.count(); topItems << t; } foreach( LaunchConfiguration* l, Core::self()->runControllerInternal()->launchConfigurationsInternal() ) { addItemForLaunchConfig( l ); } } void LaunchConfigurationsModel::addItemForLaunchConfig( LaunchConfiguration* l ) { LaunchItem* t = new LaunchItem; t->launch = l; TreeItem* parent; if( l->project() ) { parent = findItemForProject( l->project() ); } else { parent = topItems.at(0); } t->parent = parent; t->row = parent->children.count(); parent->children.append( t ); addLaunchModeItemsForLaunchConfig ( t ); } void LaunchConfigurationsModel::addLaunchModeItemsForLaunchConfig ( LaunchItem* t ) { QList items; QSet modes; foreach( ILauncher* launcher, t->launch->type()->launchers() ) { foreach( const QString& mode, launcher->supportedModes() ) { if( !modes.contains( mode ) && launcher->configPages().count() > 0 ) { modes.insert( mode ); LaunchModeItem* lmi = new LaunchModeItem; lmi->mode = Core::self()->runController()->launchModeForId( mode ); lmi->parent = t; lmi->row = t->children.count(); items.append( lmi ); } } } if( !items.isEmpty() ) { QModelIndex p = indexForConfig( t->launch ); beginInsertRows( p, t->children.count(), t->children.count() + items.count() - 1 ); t->children.append( items ); endInsertRows(); } } LaunchConfigurationsModel::ProjectItem* LaunchConfigurationsModel::findItemForProject( IProject* p ) { foreach( TreeItem* t, topItems ) { ProjectItem* pi = dynamic_cast( t ); if( pi && pi->project == p ) { return pi; } } Q_ASSERT(false); return nullptr; } int LaunchConfigurationsModel::columnCount(const QModelIndex& parent) const { Q_UNUSED( parent ); return 2; } QVariant LaunchConfigurationsModel::data(const QModelIndex& index, int role) const { if( index.isValid() && index.column() >= 0 && index.column() < 2 ) { TreeItem* t = static_cast( index.internalPointer() ); switch( role ) { case Qt::DisplayRole: { LaunchItem* li = dynamic_cast( t ); if( li ) { if( index.column() == 0 ) { return li->launch->name(); } else if( index.column() == 1 ) { return li->launch->type()->name(); } } ProjectItem* pi = dynamic_cast( t ); if( pi && index.column() == 0 ) { return pi->project->name(); } GenericPageItem* gpi = dynamic_cast( t ); if( gpi && index.column() == 0 ) { return gpi->text; } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi ) { if( index.column() == 0 ) { return lmi->mode->name(); } else if( index.column() == 1 ) { LaunchConfiguration* l = configForIndex( index ); return l->type()->launcherForId( l->launcherForMode( lmi->mode->id() ) )->name(); } } break; } case Qt::DecorationRole: { LaunchItem* li = dynamic_cast( t ); if( index.column() == 0 && li ) { return li->launch->type()->icon(); } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi && index.column() == 0 ) { return lmi->mode->icon(); } if ( index.column() == 0 && !index.parent().isValid() ) { if (index.row() == 0) { // global item return QIcon::fromTheme(QStringLiteral("folder")); } else { // project item return QIcon::fromTheme(QStringLiteral("folder-development")); } } break; } case Qt::EditRole: { LaunchItem* li = dynamic_cast( t ); if( li ) { if( index.column() == 0 ) { return li->launch->name(); } else if ( index.column() == 1 ) { return li->launch->type()->id(); } } LaunchModeItem* lmi = dynamic_cast( t ); if( lmi && index.column() == 1 ) { return configForIndex( index )->launcherForMode( lmi->mode->id() ); } break; } default: break; } } return QVariant(); } QModelIndex LaunchConfigurationsModel::index(int row, int column, const QModelIndex& parent) const { if( !hasIndex( row, column, parent ) ) return QModelIndex(); TreeItem* tree; if( !parent.isValid() ) { tree = topItems.at( row ); } else { TreeItem* t = static_cast( parent.internalPointer() ); tree = t->children.at( row ); } if( tree ) { return createIndex( row, column, tree ); } return QModelIndex(); } QModelIndex LaunchConfigurationsModel::parent(const QModelIndex& child) const { if( child.isValid() ) { TreeItem* t = static_cast( child.internalPointer() ); if( t->parent ) { return createIndex( t->parent->row, 0, t->parent ); } } return QModelIndex(); } int LaunchConfigurationsModel::rowCount(const QModelIndex& parent) const { if( parent.column() > 0 ) return 0; if( parent.isValid() ) { TreeItem* t = static_cast( parent.internalPointer() ); return t->children.count(); } else { return topItems.count(); } return 0; } QVariant LaunchConfigurationsModel::headerData(int section, Qt::Orientation orientation, int role) const { if( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { if( section == 0 ) { return i18nc("Name of the Launch Configurations", "Name"); } else if( section == 1 ) { return i18nc("The type of the Launch Configurations (i.e. Python Application, C++ Application)", "Type"); } } return QVariant(); } Qt::ItemFlags LaunchConfigurationsModel::flags(const QModelIndex& index) const { if( index.isValid() && index.column() >= 0 && index.column() < columnCount( QModelIndex() ) ) { TreeItem* t = static_cast( index.internalPointer() ); if( t && ( dynamic_cast( t ) || ( dynamic_cast( t ) && index.column() == 1 ) ) ) { return Qt::ItemFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable ); } else if( t ) { return Qt::ItemFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); } } return Qt::NoItemFlags; } bool LaunchConfigurationsModel::setData(const QModelIndex& index, const QVariant& value, int role) { if( index.isValid() && index.parent().isValid() && role == Qt::EditRole ) { if( index.row() >= 0 && index.row() < rowCount( index.parent() ) ) { LaunchItem* t = dynamic_cast( static_cast( index.internalPointer() ) ); if( t ) { if( index.column() == 0 ) { t->launch->setName( value.toString() ); } else if( index.column() == 1 ) { if (t->launch->type()->id() != value.toString()) { t->launch->setType( value.toString() ); QModelIndex p = indexForConfig(t->launch); qCDebug(SHELL) << data(p); beginRemoveRows( p, 0, t->children.count() ); qDeleteAll( t->children ); t->children.clear(); endRemoveRows(); addLaunchModeItemsForLaunchConfig( t ); } } emit dataChanged(index, index); return true; } LaunchModeItem* lmi = dynamic_cast( static_cast( index.internalPointer() ) ); if( lmi ) { if( index.column() == 1 && index.data(Qt::EditRole)!=value) { LaunchConfiguration* l = configForIndex( index ); l->setLauncherForMode( lmi->mode->id(), value.toString() ); emit dataChanged(index, index); return true; } } } } return false; } ILaunchMode* LaunchConfigurationsModel::modeForIndex( const QModelIndex& idx ) const { if( idx.isValid() ) { LaunchModeItem* item = dynamic_cast( static_cast( idx.internalPointer() ) ); if( item ) { return item->mode; } } return nullptr; } LaunchConfiguration* LaunchConfigurationsModel::configForIndex(const QModelIndex& idx ) const { if( idx.isValid() ) { LaunchItem* item = dynamic_cast( static_cast( idx.internalPointer() ) ); if( item ) { return item->launch; } LaunchModeItem* lmitem = dynamic_cast( static_cast( idx.internalPointer() ) ); if( lmitem ) { return dynamic_cast( lmitem->parent )->launch; } } return nullptr; } QModelIndex LaunchConfigurationsModel::indexForConfig( LaunchConfiguration* l ) const { if( l ) { TreeItem* tparent = topItems.at( 0 ); if( l->project() ) { foreach( TreeItem* t, topItems ) { ProjectItem* pi = dynamic_cast( t ); if( pi && pi->project == l->project() ) { tparent = t; break; } } } if( tparent ) { foreach( TreeItem* c, tparent->children ) { LaunchItem* li = dynamic_cast( c ); if( li->launch && li->launch == l ) { return index( c->row, 0, index( tparent->row, 0, QModelIndex() ) ); } } } } return QModelIndex(); } void LaunchConfigurationsModel::deleteConfiguration( const QModelIndex& index ) { LaunchItem* t = dynamic_cast( static_cast( index.internalPointer() ) ); if( !t ) return; beginRemoveRows( parent( index ), index.row(), index.row() ); t->parent->children.removeAll( t ); Core::self()->runControllerInternal()->removeLaunchConfiguration( t->launch ); endRemoveRows(); } void LaunchConfigurationsModel::createConfiguration(const QModelIndex& parent ) { if(!Core::self()->runController()->launchConfigurationTypes().isEmpty()) { TreeItem* t = static_cast( parent.internalPointer() ); ProjectItem* ti = dynamic_cast( t ); LaunchConfigurationType* type = Core::self()->runController()->launchConfigurationTypes().at(0); QPair launcher = qMakePair( type->launchers().at( 0 )->supportedModes().at(0), type->launchers().at( 0 )->id() ); IProject* p = ( ti ? ti->project : nullptr ); ILaunchConfiguration* l = Core::self()->runController()->createLaunchConfiguration( type, launcher, p ); addConfiguration(l, parent); } } void LaunchConfigurationsModel::addConfiguration(ILaunchConfiguration* l, const QModelIndex& parent) { if( parent.isValid() ) { beginInsertRows( parent, rowCount( parent ), rowCount( parent ) ); addItemForLaunchConfig( dynamic_cast( l ) ); endInsertRows(); } else { delete l; Q_ASSERT(false && "could not add the configuration"); } } IProject* LaunchConfigurationsModel::projectForIndex(const QModelIndex& idx) { if(idx.parent().isValid()) { return projectForIndex(idx.parent()); } else { const ProjectItem* item = dynamic_cast(topItems[idx.row()]); return item ? item->project : nullptr; } } LaunchConfigPagesContainer::LaunchConfigPagesContainer( const QList& factories, QWidget* parent ) : QWidget(parent) { setLayout( new QVBoxLayout( this ) ); layout()->setContentsMargins( 0, 0, 0, 0 ); QWidget* parentwidget = this; QTabWidget* tab = nullptr; if( factories.count() > 1 ) { tab = new QTabWidget( this ); parentwidget = tab; layout()->addWidget( tab ); } foreach( LaunchConfigurationPageFactory* fac, factories ) { LaunchConfigurationPage* page = fac->createWidget( parentwidget ); if ( page->layout() ) { // remove margins for single page, reset margins for tabbed display const int pageMargin = tab ? -1 : 0; page->layout()->setContentsMargins(pageMargin, pageMargin, pageMargin, pageMargin); } pages.append( page ); connect( page, &LaunchConfigurationPage::changed, this, &LaunchConfigPagesContainer::changed ); if( tab ) { tab->addTab( page, page->icon(), page->title() ); } else { layout()->addWidget( page ); } } } void LaunchConfigPagesContainer::setLaunchConfiguration( KDevelop::LaunchConfiguration* l ) { config = l; foreach( LaunchConfigurationPage* p, pages ) { p->loadFromConfiguration( config->config(), config->project() ); } } void LaunchConfigPagesContainer::save() { foreach( LaunchConfigurationPage* p, pages ) { p->saveToConfiguration( config->config() ); } config->config().sync(); } QWidget* LaunchConfigurationModelDelegate::createEditor ( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index ) const { const LaunchConfigurationsModel* model = static_cast(index.model()); ILaunchMode* mode = model->modeForIndex( index ); LaunchConfiguration* config = model->configForIndex( index ); if( index.column() == 1 && mode && config ) { KComboBox* box = new KComboBox( parent ); QList launchers = config->type()->launchers(); for( QList::const_iterator it = launchers.constBegin(); it != launchers.constEnd(); ++it ) { if( ((*it)->supportedModes().contains( mode->id() ) ) ) { box->addItem( (*it)->name(), (*it)->id() ); } } return box; } else if( !mode && config && index.column() == 1 ) { KComboBox* box = new KComboBox( parent ); const QList types = Core::self()->runController()->launchConfigurationTypes(); for( QList::const_iterator it = types.begin(); it != types.end(); ++it ) { box->addItem( (*it)->name(), (*it)->id() ); } return box; } return QStyledItemDelegate::createEditor ( parent, option, index ); } void LaunchConfigurationModelDelegate::setEditorData ( QWidget* editor, const QModelIndex& index ) const { const LaunchConfigurationsModel* model = static_cast(index.model()); LaunchConfiguration* config = model->configForIndex( index ); if( index.column() == 1 && config ) { KComboBox* box = qobject_cast( editor ); box->setCurrentIndex( box->findData( index.data( Qt::EditRole ) ) ); } else { QStyledItemDelegate::setEditorData ( editor, index ); } } void LaunchConfigurationModelDelegate::setModelData ( QWidget* editor, QAbstractItemModel* model, const QModelIndex& index ) const { LaunchConfigurationsModel* lmodel = static_cast(model); LaunchConfiguration* config = lmodel->configForIndex( index ); if( index.column() == 1 && config ) { KComboBox* box = qobject_cast( editor ); lmodel->setData( index, box->itemData( box->currentIndex() ) ); } else { QStyledItemDelegate::setModelData ( editor, model, index ); } } void LaunchConfigurationDialog::launchModeChanged(int item) { QModelIndex index = tree->currentIndex(); if(debugger->isVisible() && item>=0) tree->model()->setData(index.sibling(index.row(), 1), debugger->itemData(item), Qt::EditRole); } } diff --git a/kdevplatform/shell/loadedpluginsdialog.cpp b/kdevplatform/shell/loadedpluginsdialog.cpp index 768c857799..23810acc08 100644 --- a/kdevplatform/shell/loadedpluginsdialog.cpp +++ b/kdevplatform/shell/loadedpluginsdialog.cpp @@ -1,307 +1,306 @@ /************************************************************************** * 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 }; explicit PluginsModel(QObject* parent = nullptr) : 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 nullptr; if (index.parent().isValid()) return nullptr; if (index.column() != 0) return nullptr; if (index.row() >= m_plugins.count()) return nullptr; 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: explicit LoadedPluginsDelegate(QAbstractItemView *itemView, QObject *parent = nullptr) : KWidgetItemDelegate(itemView, parent) , pushButton(new QPushButton) { pushButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); // only for getting size matters } ~LoadedPluginsDelegate() override { delete pushButton; } 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, nullptr); int iconSize = option.rect.height() - MARGIN * 2; 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 QModelIndex &index) const override { Q_UNUSED(index); QPushButton *button = new QPushButton(); button->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); - setBlockedEventTypes(button, QList() << QEvent::MouseButtonPress - << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick); + 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()) { 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) { KAboutData aboutData = KAboutData::fromPluginMetaData(pluginInfo(p)); if (!aboutData.componentName().isEmpty()) { // Be sure the about data is not completely empty KDevelop::ScopedDialog aboutPlugin(aboutData, itemView()); aboutPlugin->exec(); return; } } } private: QPushButton *pushButton; }; class PluginsView : public QListView { Q_OBJECT public: explicit PluginsView(QWidget* parent = nullptr) :QListView(parent) { setModel(new PluginsModel(this)); 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/kdevplatform/shell/openprojectdialog.cpp b/kdevplatform/shell/openprojectdialog.cpp index 6d18cefdb1..a40a1ea3cc 100644 --- a/kdevplatform/shell/openprojectdialog.cpp +++ b/kdevplatform/shell/openprojectdialog.cpp @@ -1,367 +1,369 @@ /*************************************************************************** * Copyright (C) 2008 by Andreas Pakulat #include #include #include #include #include #include #include #include "core.h" #include "uicontroller.h" #include "plugincontroller.h" #include "mainwindow.h" #include "shellextension.h" #include "projectsourcepage.h" #include #include namespace { struct URLInfo { bool isValid; bool isDir; QString extension; }; URLInfo urlInfo(const QUrl& url) { URLInfo ret; ret.isValid = false; if (url.isLocalFile()) { QFileInfo info(url.toLocalFile()); ret.isValid = info.exists(); if (ret.isValid) { ret.isDir = info.isDir(); ret.extension = info.suffix(); } } else if (url.isValid()) { KIO::StatJob* statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, KDevelop::Core::self()->uiControllerInternal()->defaultMainWindow()); ret.isValid = statJob->exec(); // TODO: do this asynchronously so that the user isn't blocked while typing every letter of the hostname in sftp://hostname if (ret.isValid) { KIO::UDSEntry entry = statJob->statResult(); ret.isDir = entry.isDir(); ret.extension = QFileInfo(entry.stringValue(KIO::UDSEntry::UDS_NAME)).suffix(); } } return ret; } } namespace KDevelop { OpenProjectDialog::OpenProjectDialog(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin, QWidget* parent) : KAssistantDialog( parent ) , m_urlIsDirectory(false) , sourcePage(nullptr) , openPage(nullptr) , projectInfoPage(nullptr) { resize(QSize(700, 500)); // KAssistantDialog creates a help button by default, no option to prevent that auto helpButton = button(QDialogButtonBox::Help); if (helpButton) { buttonBox()->removeButton(helpButton); delete helpButton; } const bool useKdeFileDialog = qEnvironmentVariableIsSet("KDE_FULL_SESSION"); QStringList filters, allEntry; QString filterFormat = useKdeFileDialog ? QStringLiteral("%1|%2 (%1)") : QStringLiteral("%2 (%1)"); allEntry << QLatin1String("*.") + ShellExtension::getInstance()->projectFileExtension(); filters << filterFormat.arg(QLatin1String("*.") + ShellExtension::getInstance()->projectFileExtension(), ShellExtension::getInstance()->projectFileDescription()); QVector plugins = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); foreach(const KPluginMetaData& info, plugins) { m_projectPlugins.insert(info.name(), info); QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); // some project file manager plugins like KDevGenericManager have no file filter set if (filter.isEmpty()) { m_genericProjectPlugins << info.name(); continue; } QString desc = info.value(QStringLiteral("X-KDevelop-ProjectFilesFilterDescription")); m_projectFilters.insert(info.name(), filter); allEntry += filter; filters << filterFormat.arg(filter.join(QLatin1Char(' ')), desc); } if (useKdeFileDialog) filters.prepend(i18n("%1|All Project Files (%1)", allEntry.join(QLatin1Char(' ')))); QUrl start = startUrl.isValid() ? startUrl : Core::self()->projectController()->projectsBaseDirectory(); start = start.adjusted(QUrl::NormalizePathSegments); KPageWidgetItem* currentPage = nullptr; if( fetch ) { sourcePageWidget = new ProjectSourcePage(start, repoUrl, vcsOrProviderPlugin, this); connect( sourcePageWidget, &ProjectSourcePage::isCorrect, this, &OpenProjectDialog::validateSourcePage ); sourcePage = addPage( sourcePageWidget, i18n("Select Source") ); currentPage = sourcePage; } if (useKdeFileDialog) { openPageWidget = new OpenProjectPage( start, filters, this ); connect( openPageWidget, &OpenProjectPage::urlSelected, this, &OpenProjectDialog::validateOpenUrl ); connect( openPageWidget, &OpenProjectPage::accepted, this, &OpenProjectDialog::openPageAccepted ); openPage = addPage( openPageWidget, i18n("Select a build system setup file, existing KDevelop project, " "or any folder to open as a project") ); if (!currentPage) { currentPage = openPage; } } else { nativeDialog = new QFileDialog(this, i18n("Open Project")); nativeDialog->setDirectoryUrl(start); nativeDialog->setFileMode(QFileDialog::Directory); } ProjectInfoPage* page = new ProjectInfoPage( this ); connect( page, &ProjectInfoPage::projectNameChanged, this, &OpenProjectDialog::validateProjectName ); connect( page, &ProjectInfoPage::projectManagerChanged, this, &OpenProjectDialog::validateProjectManager ); projectInfoPage = addPage( page, i18n("Project Information") ); if (!currentPage) { currentPage = projectInfoPage; } setValid( sourcePage, false ); setValid( openPage, false ); setValid( projectInfoPage, false); setAppropriate( projectInfoPage, false ); setCurrentPage( currentPage ); setWindowTitle(i18n("Open Project")); } bool OpenProjectDialog::execNativeDialog() { while (true) { if (nativeDialog->exec()) { QUrl selectedUrl = nativeDialog->selectedUrls().at(0); if (urlInfo(selectedUrl).isValid) { // validate directory first to populate m_projectName and m_projectManager validateOpenUrl(selectedUrl.adjusted(QUrl::RemoveFilename)); validateOpenUrl(selectedUrl); return true; } } else { return false; } } } int OpenProjectDialog::exec() { if (nativeDialog && !execNativeDialog()) { reject(); return QDialog::Rejected; } return KAssistantDialog::exec(); } void OpenProjectDialog::validateSourcePage(bool valid) { setValid(sourcePage, valid); if (!nativeDialog) { openPageWidget->setUrl(sourcePageWidget->workingDir()); } } void OpenProjectDialog::validateOpenUrl( const QUrl& url_ ) { URLInfo urlInfo = ::urlInfo(url_); const QUrl url = url_.adjusted(QUrl::StripTrailingSlash); // openPage is used only in KDE if (openPage) { if ( urlInfo.isValid ) { // reset header openPage->setHeader(i18n("Open \"%1\" as project", url.fileName())); } else { // report error KColorScheme scheme(palette().currentColorGroup()); const QString errorMsg = i18n("Selected URL is invalid"); openPage->setHeader(QStringLiteral("%2") .arg(scheme.foreground(KColorScheme::NegativeText).color().name(), errorMsg) ); setAppropriate( projectInfoPage, false ); setAppropriate( openPage, true ); setValid( openPage, false ); return; } } m_selected = url; if( urlInfo.isDir || urlInfo.extension != ShellExtension::getInstance()->projectFileExtension() ) { m_urlIsDirectory = urlInfo.isDir; setAppropriate( projectInfoPage, true ); m_url = url; if( !urlInfo.isDir ) { m_url = m_url.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename); } ProjectInfoPage* page = qobject_cast( projectInfoPage->widget() ); if( page ) { page->setProjectName( m_url.fileName() ); // clear the filelist m_fileList.clear(); if( urlInfo.isDir ) { // If a dir was selected fetch all files in it KIO::ListJob* job = KIO::listDir( m_url ); connect( job, &KIO::ListJob::entries, this, &OpenProjectDialog::storeFileList); KJobWidgets::setWindow(job, Core::self()->uiController()->activeMainWindow()); job->exec(); } else { // Else we'll just take the given file m_fileList << url.fileName(); } // Now find a manager for the file(s) in our filelist. QVector choices; Q_FOREACH ( const auto& file, m_fileList ) { auto plugins = projectManagerForFile(file); if ( plugins.contains(QStringLiteral("")) ) { plugins.removeAll(QStringLiteral("")); choices.append({i18n("Open existing file \"%1\"", file), QStringLiteral(""), QString()}); } + choices.reserve(choices.size() + plugins.size()); Q_FOREACH ( const auto& plugin, plugins ) { auto meta = m_projectPlugins.value(plugin); choices.append({file + QLatin1String(" (") + plugin + QLatin1Char(')'), meta.pluginId(), meta.iconName(), file}); } } // add managers that work in any case (e.g. KDevGenericManager) + choices.reserve(choices.size() + m_genericProjectPlugins.size()); Q_FOREACH ( const auto& plugin, m_genericProjectPlugins ) { qCDebug(SHELL) << plugin; auto meta = m_projectPlugins.value(plugin); choices.append({plugin, meta.pluginId(), meta.iconName()}); } page->populateProjectFileCombo(choices); } m_url.setPath( m_url.path() + QLatin1Char('/') + m_url.fileName() + QLatin1Char('.') + ShellExtension::getInstance()->projectFileExtension() ); } else { setAppropriate( projectInfoPage, false ); m_url = url; m_urlIsDirectory = false; } validateProjectInfo(); setValid( openPage, true ); } QStringList OpenProjectDialog::projectManagerForFile(const QString& file) const { QStringList ret; foreach( const QString& manager, m_projectFilters.keys() ) { foreach( const QString& filterexp, m_projectFilters.value(manager) ) { QRegExp exp( filterexp, Qt::CaseSensitive, QRegExp::Wildcard ); if ( exp.exactMatch(file) ) { ret.append(manager); } } } if ( file.endsWith(ShellExtension::getInstance()->projectFileExtension()) ) { ret.append(QStringLiteral("")); } return ret; } void OpenProjectDialog::openPageAccepted() { if ( isValid( openPage ) ) { next(); } } void OpenProjectDialog::validateProjectName( const QString& name ) { m_projectName = name; validateProjectInfo(); } void OpenProjectDialog::validateProjectInfo() { setValid( projectInfoPage, (!projectName().isEmpty() && !projectManager().isEmpty()) ); } void OpenProjectDialog::validateProjectManager( const QString& manager, const QString & fileName ) { m_projectManager = manager; if ( m_urlIsDirectory ) { m_selected = m_url.resolved( QUrl(QLatin1String("./") + fileName) ); } validateProjectInfo(); } QUrl OpenProjectDialog::projectFileUrl() const { return m_url; } QUrl OpenProjectDialog::selectedUrl() const { return m_selected; } QString OpenProjectDialog::projectName() const { return m_projectName; } QString OpenProjectDialog::projectManager() const { return m_projectManager; } void OpenProjectDialog::storeFileList(KIO::Job*, const KIO::UDSEntryList& list) { foreach( const KIO::UDSEntry& entry, list ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if( name != QLatin1String(".") && name != QLatin1String("..") && !entry.isDir() ) { m_fileList << name; } } } } diff --git a/kdevplatform/shell/plugincontroller.cpp b/kdevplatform/shell/plugincontroller.cpp index 71167f65e9..df20139554 100644 --- a/kdevplatform/shell/plugincontroller.cpp +++ b/kdevplatform/shell/plugincontroller.cpp @@ -1,812 +1,815 @@ /* This file is part of the KDE project Copyright 2004, 2007 Alexander Dymo Copyright 2006 Matt Rogers Based on code from Kopete Copyright (c) 2002-2003 Martijn Klingens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plugincontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "shellextension.h" #include "runcontroller.h" #include "debugcontroller.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "projectcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" namespace { inline QString KEY_Plugins() { return QStringLiteral("Plugins"); } inline QString KEY_Suffix_Enabled() { return QStringLiteral("Enabled"); } inline QString KEY_LoadMode() { return QStringLiteral("X-KDevelop-LoadMode"); } inline QString KEY_Category() { return QStringLiteral("X-KDevelop-Category"); } inline QString KEY_Mode() { return QStringLiteral("X-KDevelop-Mode"); } inline QString KEY_Version() { return QStringLiteral("X-KDevelop-Version"); } inline QString KEY_Interfaces() { return QStringLiteral("X-KDevelop-Interfaces"); } inline QString KEY_Required() { return QStringLiteral("X-KDevelop-IRequired"); } inline QString KEY_Optional() { return QStringLiteral("X-KDevelop-IOptional"); } inline QString KEY_KPlugin() { return QStringLiteral("KPlugin"); } inline QString KEY_EnabledByDefault() { return QStringLiteral("EnabledByDefault"); } inline QString KEY_Global() { return QStringLiteral("Global"); } inline QString KEY_Project() { return QStringLiteral("Project"); } inline QString KEY_Gui() { return QStringLiteral("GUI"); } inline QString KEY_AlwaysOn() { return QStringLiteral("AlwaysOn"); } inline QString KEY_UserSelectable() { return QStringLiteral("UserSelectable"); } bool isUserSelectable( const KPluginMetaData& info ) { QString loadMode = info.value(KEY_LoadMode()); return loadMode.isEmpty() || loadMode == KEY_UserSelectable(); } bool isGlobalPlugin( const KPluginMetaData& info ) { return info.value(KEY_Category()) == KEY_Global(); } bool hasMandatoryProperties( const KPluginMetaData& info ) { QString mode = info.value(KEY_Mode()); if (mode.isEmpty()) { return false; } // when the plugin is installed into the versioned plugin path, it's good to go if (info.fileName().contains(QLatin1String("/kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION) "/"))) { return true; } // the version property is only required when the plugin is not installed into the right directory QVariant version = info.rawData().value(KEY_Version()).toVariant(); if (version.isValid() && version.value() == KDEVELOP_PLUGIN_VERSION) { return true; } return false; } bool constraintsMatch( const KPluginMetaData& info, const QVariantMap& constraints) { for (auto it = constraints.begin(); it != constraints.end(); ++it) { const auto property = info.rawData().value(it.key()).toVariant(); if (!property.isValid()) { return false; } else if (property.canConvert()) { QSet values = property.toStringList().toSet(); QSet expected = it.value().toStringList().toSet(); if (!values.contains(expected)) { return false; } } else if (it.value() != property) { return false; } } return true; } struct Dependency { explicit Dependency(const QString &dependency) : interface(dependency) { if (dependency.contains(QLatin1Char('@'))) { const auto list = dependency.split(QLatin1Char('@'), QString::SkipEmptyParts); if (list.size() == 2) { interface = list.at(0); pluginName = list.at(1); } } } QString interface; QString pluginName; }; } namespace KDevelop { class PluginControllerPrivate { public: QVector plugins; //map plugin infos to currently loaded plugins typedef QHash InfoToPluginMap; InfoToPluginMap loadedPlugins; // The plugin manager's mode. The mode is StartingUp until loadAllPlugins() // has finished loading the plugins, after which it is set to Running. // ShuttingDown and DoneShutdown are used during shutdown by the // async unloading of plugins. enum CleanupMode { Running /**< the plugin manager is running */, CleaningUp /**< the plugin manager is cleaning up for shutdown */, CleanupDone /**< the plugin manager has finished cleaning up */ }; CleanupMode cleanupMode; bool canUnload(const KPluginMetaData& plugin) { qCDebug(SHELL) << "checking can unload for:" << plugin.name() << plugin.value(KEY_LoadMode()); if (plugin.value(KEY_LoadMode()) == KEY_AlwaysOn()) { return false; } const QStringList interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces()); qCDebug(SHELL) << "checking dependencies:" << interfaces; foreach (const KPluginMetaData& info, loadedPlugins.keys()) { if (info.pluginId() != plugin.pluginId()) { QStringList dependencies = KPluginMetaData::readStringList(plugin.rawData(), KEY_Required()); dependencies += KPluginMetaData::readStringList(plugin.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginId()) { continue; } if (interfaces.contains(dependency.interface) && !canUnload(info)) { return false; } } } } return true; } KPluginMetaData infoForId( const QString& id ) const { foreach (const KPluginMetaData& info, plugins) { if (info.pluginId() == id) { return info; } } return KPluginMetaData(); } /** * Iterate over all cached plugin infos, and call the functor for every enabled plugin. * * If an extension and/or pluginName is given, the functor will only be called for * those plugins matching this information. * * The functor should return false when the iteration can be stopped, and true if it * should be continued. */ template void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = QVariantMap(), const QString &pluginName = {}) { foreach (const auto& info, plugins) { if ((pluginName.isEmpty() || info.pluginId() == pluginName) && (extension.isEmpty() || KPluginMetaData::readStringList(info.rawData(), KEY_Interfaces()).contains(extension)) && constraintsMatch(info, constraints) && isEnabled(info)) { if (!func(info)) { break; } } } } enum EnableState { DisabledByEnv, DisabledBySetting, DisabledByUnknown, FirstEnabledState, EnabledBySetting = FirstEnabledState, AlwaysEnabled }; /** * Estimate enabled state of a plugin */ EnableState enabledState(const KPluginMetaData& info) const { // first check black listing from environment static const QStringList disabledPlugins = QString::fromLatin1(qgetenv("KDEV_DISABLE_PLUGINS")).split(QLatin1Char(';')); if (disabledPlugins.contains(info.pluginId())) { return DisabledByEnv; } if (!isUserSelectable( info )) return AlwaysEnabled; // read stored user preference const KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); const QString pluginEnabledKey = info.pluginId() + KEY_Suffix_Enabled(); if (grp.hasKey(pluginEnabledKey)) { return grp.readEntry(pluginEnabledKey, true) ? EnabledBySetting : DisabledBySetting; } // should not happen return DisabledByUnknown; } /** * Decide whether a plugin is enabled */ bool isEnabled(const KPluginMetaData& info) const { return (enabledState(info) >= FirstEnabledState); } Core *core; }; PluginController::PluginController(Core *core) : IPluginController(), d(new PluginControllerPrivate) { setObjectName(QStringLiteral("PluginController")); d->core = core; QSet foundPlugins; auto newPlugins = KPluginLoader::findPlugins(QStringLiteral("kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION)), [&](const KPluginMetaData& meta) { if (meta.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"))) { foundPlugins.insert(meta.pluginId()); return true; } else { qCWarning(SHELL) << "Plugin" << meta.fileName() << "is installed into the kdevplatform plugin directory, but does not have" " \"KDevelop/Plugin\" set as the service type. This plugin will not be loaded."; return false; } }); qCDebug(SHELL) << "Found" << newPlugins.size() << "plugins:" << foundPlugins; if (newPlugins.isEmpty()) { qCWarning(SHELL) << "Did not find any plugins, check your environment."; qCWarning(SHELL) << " Note: QT_PLUGIN_PATH is set to:" << qgetenv("QT_PLUGIN_PATH"); } d->plugins = newPlugins; KTextEditorIntegration::initialize(); const QVector ktePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")) && md.serviceTypes().contains(QStringLiteral("KDevelop/Plugin")); }); foundPlugins.clear(); std::for_each(ktePlugins.cbegin(), ktePlugins.cend(), [&foundPlugins](const KPluginMetaData& data) { foundPlugins << data.pluginId(); }); qCDebug(SHELL) << "Found" << ktePlugins.size() << " KTextEditor plugins:" << foundPlugins; + d->plugins.reserve(d->plugins.size() + ktePlugins.size()); foreach (const auto& info, ktePlugins) { auto data = info.rawData(); // add some KDevelop specific JSON data data[KEY_Category()] = KEY_Global(); data[KEY_Mode()] = KEY_Gui(); data[KEY_Version()] = KDEVELOP_PLUGIN_VERSION; d->plugins.append({data, info.fileName(), info.metaDataFileName()}); } d->cleanupMode = PluginControllerPrivate::Running; // Register the KDevelop::IPlugin* metatype so we can properly unload it qRegisterMetaType( "KDevelop::IPlugin*" ); } PluginController::~PluginController() { if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) { qCWarning(SHELL) << "Destructing plugin controller without going through the shutdown process!"; } } KPluginMetaData PluginController::pluginInfo( const IPlugin* plugin ) const { return d->loadedPlugins.key(const_cast(plugin)); } void PluginController::cleanup() { if(d->cleanupMode != PluginControllerPrivate::Running) { //qCDebug(SHELL) << "called when not running. state =" << d->cleanupMode; return; } d->cleanupMode = PluginControllerPrivate::CleaningUp; // Ask all plugins to unload while ( !d->loadedPlugins.isEmpty() ) { //Let the plugin do some stuff before unloading unloadPlugin(d->loadedPlugins.begin().value(), Now); } d->cleanupMode = PluginControllerPrivate::CleanupDone; } IPlugin* PluginController::loadPlugin( const QString& pluginName ) { return loadPluginInternal( pluginName ); } void PluginController::initialize() { QElapsedTimer timer; timer.start(); QMap pluginMap; if( ShellExtension::getInstance()->defaultPlugins().isEmpty() ) { foreach( const KPluginMetaData& pi, d->plugins ) { QJsonValue enabledByDefaultValue = pi.rawData()[KEY_KPlugin()].toObject()[KEY_EnabledByDefault()]; // plugins enabled until explicitly specified otherwise const bool enabledByDefault = (enabledByDefaultValue.isNull() || enabledByDefaultValue.toBool()); pluginMap.insert(pi.pluginId(), enabledByDefault); } } else { // Get the default from the ShellExtension foreach( const QString& s, ShellExtension::getInstance()->defaultPlugins() ) { pluginMap.insert( s, true ); } } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); QMap entries = grp.entryMap(); QMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { const QString key = it.key(); if (key.endsWith(KEY_Suffix_Enabled())) { const QString pluginid = key.left(key.length() - 7); const bool defValue = pluginMap.value( pluginid, false ); const bool enabled = grp.readEntry(key, defValue); pluginMap.insert( pluginid, enabled ); } } // store current known set of enabled plugins foreach (const KPluginMetaData& pi, d->plugins) { if (isUserSelectable(pi)) { auto it = pluginMap.constFind(pi.pluginId()); if (it != pluginMap.constEnd() && (it.value())) { grp.writeEntry(pi.pluginId() + KEY_Suffix_Enabled(), true); } } else { // Backward compat: Remove any now-obsolete entries grp.deleteEntry(pi.pluginId() + QLatin1String("Disabled")); } } // Synchronize so we're writing out to the file. grp.sync(); // load global plugins foreach (const KPluginMetaData& pi, d->plugins) { if (isGlobalPlugin(pi)) { loadPluginInternal(pi.pluginId()); } } qCDebug(SHELL) << "Done loading plugins - took:" << timer.elapsed() << "ms"; } QList PluginController::loadedPlugins() const { return d->loadedPlugins.values(); } bool PluginController::unloadPlugin( const QString & pluginId ) { IPlugin *thePlugin = plugin( pluginId ); bool canUnload = d->canUnload( d->infoForId( pluginId ) ); qCDebug(SHELL) << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload; if( thePlugin && canUnload ) { return unloadPlugin(thePlugin, Later); } return (canUnload && thePlugin); } bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion) { qCDebug(SHELL) << "unloading plugin:" << plugin << pluginInfo( plugin ).name(); emit unloadingPlugin(plugin); plugin->unload(); emit pluginUnloaded(plugin); //Remove the plugin from our list of plugins so we create a new //instance when we're asked for it again. //This is important to do right here, not later when the plugin really //vanishes. For example project re-opening might try to reload the plugin //and then would get the "old" pointer which will be deleted in the next //event loop run and thus causing crashes. for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.value() == plugin ) { d->loadedPlugins.erase( it ); break; } } if (deletion == Later) plugin->deleteLater(); else delete plugin; return true; } KPluginMetaData PluginController::infoForPluginId( const QString &pluginId ) const { foreach (const KPluginMetaData& info, d->plugins) { if (info.pluginId() == pluginId) { return info; } } return KPluginMetaData(); } IPlugin *PluginController::loadPluginInternal( const QString &pluginId ) { QElapsedTimer timer; timer.start(); KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) { qCWarning(SHELL) << "Unable to find a plugin named '" << pluginId << "'!" ; return nullptr; } if ( IPlugin* plugin = d->loadedPlugins.value( info ) ) { return plugin; } const auto enabledState = d->enabledState(info); if (enabledState < PluginControllerPrivate::FirstEnabledState) { // Do not load disabled plugins qCDebug(SHELL) << "Not loading plugin named" << pluginId << ( (enabledState == PluginControllerPrivate::DisabledByEnv) ? "because disabled by KDEV_DISABLE_PLUGINS." : (enabledState == PluginControllerPrivate::DisabledBySetting) ? "because disabled by setting." : /* else, should not happen */ "because disabled for unknown reason."); return nullptr; } if ( !hasMandatoryProperties( info ) ) { qCWarning(SHELL) << "Unable to load plugin named" << pluginId << "because not all mandatory properties are set."; return nullptr; } if ( info.value(KEY_Mode()) == KEY_Gui() && Core::self()->setupFlags() == Core::NoUi ) { qCDebug(SHELL) << "Not loading plugin named" << pluginId << "- Running in No-Ui mode, but the plugin says it needs a GUI"; return nullptr; } qCDebug(SHELL) << "Attempting to load" << pluginId << "- name:" << info.name(); emit loadingPlugin( info.pluginId() ); // first, ensure all dependencies are available and not disabled // this is unrelated to whether they are loaded already or not. // when we depend on e.g. A and B, but B cannot be found, then we // do not want to load A first and then fail on B and leave A loaded. // this would happen if we'd skip this step here and directly loadDependencies. QStringList missingInterfaces; if ( !hasUnresolvedDependencies( info, missingInterfaces ) ) { qCWarning(SHELL) << "Can't load plugin" << pluginId << "some of its required dependencies could not be fulfilled:" << missingInterfaces.join(QLatin1Char(',')); return nullptr; } // now ensure all dependencies are loaded QString failedDependency; if( !loadDependencies( info, failedDependency ) ) { qCWarning(SHELL) << "Can't load plugin" << pluginId << "because a required dependency could not be loaded:" << failedDependency; return nullptr; } // same for optional dependencies, but don't error out if anything fails loadOptionalDependencies( info ); // now we can finally load the plugin itself KPluginLoader loader(info.fileName()); auto factory = loader.factory(); if (!factory) { qCWarning(SHELL) << "Can't load plugin" << pluginId << "because a factory to load the plugin could not be obtained:" << loader.errorString(); return nullptr; } // now create it auto plugin = factory->create(d->core); if (!plugin) { if (auto katePlugin = factory->create(d->core, QVariantList() << info.pluginId())) { plugin = new KTextEditorIntegration::Plugin(katePlugin, d->core); } else { qCWarning(SHELL) << "Creating plugin" << pluginId << "failed."; return nullptr; } } KConfigGroup group = Core::self()->activeSession()->config()->group(KEY_Plugins()); // runtime errors such as missing executables on the system or such get checked now if (plugin->hasError()) { qCWarning(SHELL) << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription() << "Disabling the plugin now."; group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), false); // do the same as KPluginInfo did group.sync(); unloadPlugin(pluginId); return nullptr; } // yay, it all worked - the plugin is loaded d->loadedPlugins.insert(info, plugin); group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), true); // do the same as KPluginInfo did group.sync(); qCDebug(SHELL) << "Successfully loaded plugin" << pluginId << "from" << loader.fileName() << "- took:" << timer.elapsed() << "ms"; emit pluginLoaded( plugin ); return plugin; } IPlugin* PluginController::plugin( const QString& pluginId ) { KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) return nullptr; return d->loadedPlugins.value( info ); } bool PluginController::hasUnresolvedDependencies( const KPluginMetaData& info, QStringList& missing ) const { QSet required = KPluginMetaData::readStringList(info.rawData(), KEY_Required()).toSet(); if (!required.isEmpty()) { d->foreachEnabledPlugin([&required] (const KPluginMetaData& plugin) -> bool { foreach (const QString& iface, KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces())) { required.remove(iface); required.remove(iface + QLatin1Char('@') + plugin.pluginId()); } return !required.isEmpty(); }); } // if we found all dependencies required should be empty now if (!required.isEmpty()) { missing = required.toList(); return false; } return true; } void PluginController::loadOptionalDependencies( const KPluginMetaData& info ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { qCDebug(SHELL) << "Couldn't load optional dependency:" << dep << info.pluginId(); } } } bool PluginController::loadDependencies( const KPluginMetaData& info, QString& failedDependency ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Required()); foreach (const QString& value, dependencies) { Dependency dependency(value); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { failedDependency = value; return false; } } return true; } IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints) { IPlugin* plugin = nullptr; d->foreachEnabledPlugin([this, &plugin] (const KPluginMetaData& info) -> bool { plugin = d->loadedPlugins.value( info ); if( !plugin ) { plugin = loadPluginInternal( info.pluginId() ); } return !plugin; }, extension, constraints, pluginName); return plugin; } QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints) { //qCDebug(SHELL) << "Finding all Plugins for Extension:" << extension << "|" << constraints; QList plugins; d->foreachEnabledPlugin([this, &plugins] (const KPluginMetaData& info) -> bool { IPlugin* plugin = d->loadedPlugins.value( info ); if( !plugin) { plugin = loadPluginInternal( info.pluginId() ); } if (plugin && !plugins.contains(plugin)) { plugins << plugin; } return true; }, extension, constraints); return plugins; } QVector PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const { QVector plugins; d->foreachEnabledPlugin([&plugins] (const KPluginMetaData& info) -> bool { plugins << info; return true; }, extension, constraints); return plugins; } QStringList PluginController::allPluginNames() { QStringList names; + names.reserve(d->plugins.size()); Q_FOREACH( const KPluginMetaData& info , d->plugins ) { names << info.pluginId(); } return names; } QList PluginController::queryPluginsForContextMenuExtensions(KDevelop::Context* context, QWidget* parent) const { // This fixes random order of extension menu items between different runs of KDevelop. // Without sorting we have random reordering of "Analyze With" submenu for example: // 1) "Cppcheck" actions, "Vera++" actions - first run // 2) "Vera++" actions, "Cppcheck" actions - some other run. QMultiMap sortedPlugins; for (auto it = d->loadedPlugins.constBegin(); it != d->loadedPlugins.constEnd(); ++it) { sortedPlugins.insert(it.key().name(), it.value()); } QList exts; + exts.reserve(sortedPlugins.size()); foreach (IPlugin* plugin, sortedPlugins) { exts << plugin->contextMenuExtension(context, parent); } exts << Core::self()->debugControllerInternal()->contextMenuExtension(context, parent); exts << Core::self()->documentationControllerInternal()->contextMenuExtension(context, parent); exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension(context, parent); exts << Core::self()->runControllerInternal()->contextMenuExtension(context, parent); exts << Core::self()->projectControllerInternal()->contextMenuExtension(context, parent); return exts; } QStringList PluginController::projectPlugins() { QStringList names; foreach (const KPluginMetaData& info, d->plugins) { if (info.value(KEY_Category()) == KEY_Project()) { names << info.pluginId(); } } return names; } void PluginController::loadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { loadPluginInternal( name ); } } void PluginController::unloadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { unloadPlugin( name ); } } QVector PluginController::allPluginInfos() const { return d->plugins; } void PluginController::updateLoadedPlugins() { QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); foreach( const KPluginMetaData& info, d->plugins ) { if( isGlobalPlugin( info ) ) { bool enabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginId() ) ) ) || !isUserSelectable( info ); bool loaded = d->loadedPlugins.contains( info ); if( loaded && !enabled ) { qCDebug(SHELL) << "unloading" << info.pluginId(); if( !unloadPlugin( info.pluginId() ) ) { grp.writeEntry( info.pluginId() + KEY_Suffix_Enabled(), false ); } } else if( !loaded && enabled ) { loadPluginInternal( info.pluginId() ); } } // TODO: what about project plugins? what about dependency plugins? } } void PluginController::resetToDefaults() { KSharedConfigPtr cfg = Core::self()->activeSession()->config(); cfg->deleteGroup( KEY_Plugins() ); cfg->sync(); KConfigGroup grp = cfg->group( KEY_Plugins() ); QStringList plugins = ShellExtension::getInstance()->defaultPlugins(); if( plugins.isEmpty() ) { foreach( const KPluginMetaData& info, d->plugins ) { if (!isUserSelectable(info)) { continue; } QJsonValue enabledByDefault = info.rawData()[KEY_KPlugin()].toObject()[KEY_EnabledByDefault()]; // plugins enabled until explicitly specified otherwise if (enabledByDefault.isNull() || enabledByDefault.toBool()) { plugins << info.pluginId(); } } } foreach( const QString& s, plugins ) { grp.writeEntry(s + KEY_Suffix_Enabled(), true); } grp.sync(); } } diff --git a/kdevplatform/shell/projectcontroller.cpp b/kdevplatform/shell/projectcontroller.cpp index 76051b72b9..700abc129e 100644 --- a/kdevplatform/shell/projectcontroller.cpp +++ b/kdevplatform/shell/projectcontroller.cpp @@ -1,1280 +1,1283 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectcontroller.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 #include #include #include #include #include #include #include #include "core.h" // TODO: Should get rid off this include (should depend on IProject only) #include "project.h" #include "mainwindow.h" #include "shellextension.h" #include "plugincontroller.h" #include "configdialog.h" #include "uicontroller.h" #include "documentcontroller.h" #include "openprojectdialog.h" #include "sessioncontroller.h" #include "session.h" #include "debug.h" namespace KDevelop { class ProjectControllerPrivate { public: QList m_projects; QMap< IProject*, QList > m_projectPlugins; QPointer m_recentProjectsAction; Core* m_core; // IProject* m_currentProject; ProjectModel* model; QPointer m_openProject; QPointer m_fetchProject; QPointer m_closeProject; QPointer m_openConfig; IProjectDialogProvider* dialog; QList m_currentlyOpening; // project-file urls that are being opened ProjectController* q; ProjectBuildSetModel* buildset; bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller ProjectChangesModel* m_changesModel = nullptr; QHash< IProject*, QPointer > m_parseJobs; // parse jobs that add files from the project to the background parser. explicit ProjectControllerPrivate( ProjectController* p ) : m_core(nullptr), model(nullptr), dialog(nullptr), q(p), buildset(nullptr), m_foundProjectFile(false), m_cleaningUp(false) { } void unloadAllProjectPlugins() { if( m_projects.isEmpty() ) m_core->pluginControllerInternal()->unloadProjectPlugins(); } void projectConfig( QObject * obj ) { if( !obj ) return; Project* proj = qobject_cast(obj); if( !proj ) return; auto cfgDlg = new KDevelop::ConfigDialog(m_core->uiController()->activeMainWindow()); cfgDlg->setAttribute(Qt::WA_DeleteOnClose); cfgDlg->setModal(true); QVector configPages; ProjectConfigOptions options; options.developerFile = proj->developerFile(); options.developerTempFile = proj->developerTempFile(); options.projectTempFile = proj->projectTempFile(); options.project = proj; foreach (IPlugin* plugin, findPluginsForProject(proj)) { - for (int i = 0; i < plugin->perProjectConfigPages(); ++i) { + const int perProjectConfigPagesCount = plugin->perProjectConfigPages(); + configPages.reserve(configPages.size() + perProjectConfigPagesCount); + for (int i = 0; i < perProjectConfigPagesCount; ++i) { configPages.append(plugin->perProjectConfigPage(i, options, cfgDlg)); } } std::sort(configPages.begin(), configPages.end(), [](const ConfigPage* a, const ConfigPage* b) { return a->name() < b->name(); }); for (auto page : configPages) { cfgDlg->appendConfigPage(page); } QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) { Q_UNUSED(page) Q_ASSERT_X(proj, Q_FUNC_INFO, "ConfigDialog signalled project config change, but no project set for configuring!"); emit q->projectConfigurationChanged(proj); }); cfgDlg->setWindowTitle(i18n("Configure Project %1", proj->name())); QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, [proj]() { proj->projectConfiguration()->sync(); }); cfgDlg->show(); } void saveListOfOpenedProjects() { auto activeSession = Core::self()->activeSession(); if (!activeSession) { return; } QList openProjects; openProjects.reserve( m_projects.size() ); foreach( IProject* project, m_projects ) { openProjects.append(project->projectFile().toUrl()); } activeSession->setContainedProjects( openProjects ); } // Recursively collects builder dependencies for a project. static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project ) { QList< IProjectBuilder* > auxBuilders = topBuilder->additionalBuilderPlugins( project ); destination.append( auxBuilders ); foreach( IProjectBuilder* auxBuilder, auxBuilders ) { collectBuilders( destination, auxBuilder, project ); } } QVector findPluginsForProject( IProject* project ) const { QList plugins = m_core->pluginController()->loadedPlugins(); QVector projectPlugins; QList buildersForKcm; // Important to also include the "top" builder for the project, so // projects with only one such builder are kept working. Otherwise the project config // dialog is empty for such cases. if( IBuildSystemManager* buildSystemManager = project->buildSystemManager() ) { buildersForKcm << buildSystemManager->builder(); collectBuilders( buildersForKcm, buildSystemManager->builder(), project ); } foreach(auto plugin, plugins) { auto info = m_core->pluginController()->pluginInfo(plugin); IProjectFileManager* manager = plugin->extension(); if( manager && manager != project->projectFileManager() ) { // current plugin is a manager but does not apply to given project, skip continue; } IProjectBuilder* builder = plugin->extension(); if ( builder && !buildersForKcm.contains( builder ) ) { continue; } qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name(); projectPlugins << plugin; } return projectPlugins; } void updateActionStates() { // if only one project loaded, this is always our target int itemCount = (m_projects.size() == 1) ? 1 : 0; if (itemCount == 0) { // otherwise base on selection ProjectItemContext* itemContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemContext) { itemCount = itemContext->items().count(); } } m_openConfig->setEnabled(itemCount == 1); m_closeProject->setEnabled(itemCount > 0); } void openProjectConfig() { // if only one project loaded, this is our target IProject *project = (m_projects.count() == 1) ? m_projects.at(0) : nullptr; // otherwise base on selection if (!project) { ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx && ctx->items().count() == 1) { project = ctx->items().at(0)->project(); } } if (project) { q->configureProject(project); } } void closeSelectedProjects() { QSet projects; // if only one project loaded, this is our target if (m_projects.count() == 1) { projects.insert(m_projects.at(0)); } else { // otherwise base on selection ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx) { foreach (ProjectBaseItem* item, ctx->items()) { projects.insert(item->project()); } } } foreach (IProject* project, projects) { q->closeProject(project); } } void importProject(const QUrl& url_) { QUrl url(url_); if (url.isLocalFile()) { const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } if ( !url.isValid() ) { KMessageBox::error(Core::self()->uiControllerInternal()->activeMainWindow(), i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile))); return; } if ( m_currentlyOpening.contains(url)) { qCDebug(SHELL) << "Already opening " << url << ". Aborting."; KPassivePopup::message( i18n( "Project already being opened"), i18n( "Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile) ), m_core->uiController()->activeMainWindow() ); return; } foreach( IProject* project, m_projects ) { if ( url == project->projectFile().toUrl() ) { if ( dialog->userWantsReopen() ) { // close first, then open again by falling through q->closeProject(project); } else { // abort return; } } } m_currentlyOpening += url; m_core->pluginControllerInternal()->loadProjectPlugins(); Project* project = new Project(); QObject::connect(project, &Project::aboutToOpen, q, &ProjectController::projectAboutToBeOpened); if ( !project->open( Path(url) ) ) { m_currentlyOpening.removeAll(url); q->abortOpeningProject(project); project->deleteLater(); } } void areaChanged(Sublime::Area* area) { KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code")); ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code")); } }; IProjectDialogProvider::IProjectDialogProvider() {} IProjectDialogProvider::~IProjectDialogProvider() {} ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* const p) : d(p) {} ProjectDialogProvider::~ProjectDialogProvider() {} bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig ); if (!cfg->isConfigWritable(true)) { qCDebug(SHELL) << "can't write to configfile"; return false; } KConfigGroup grp = cfg->group( "Project" ); grp.writeEntry( "Name", name ); grp.writeEntry( "CreatedFrom", createdFrom ); grp.writeEntry( "Manager", manager ); cfg->sync(); return true; } bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg) { if ( !projectFileUrl.isLocalFile() ) { QTemporaryFile tmp; if ( !tmp.open() ) { return false; } if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) { return false; } // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519 tmp.close(); auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl); KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow()); return uploadJob->exec(); } // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root, // thus keeping .kdev file relocatable return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ); } bool projectFileExists( const QUrl& u ) { if( u.isLocalFile() ) { return QFileInfo::exists( u.toLocalFile() ); } else { auto statJob = KIO::stat(u, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow()); return statJob->exec(); } } bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig ); KConfigGroup grp = cfg->group( "Project" ); QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName(); return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) && grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); ScopedDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg->exec() == QDialog::Rejected) { return QUrl(); } QUrl projectFileUrl = dlg->projectFileUrl(); qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg->projectName() << dlg->projectManager(); if ( dlg->projectManager() == QLatin1String("") ) { return projectFileUrl; } // controls if existing project file should be saved bool writeProjectConfigToFile = true; if( projectFileExists( projectFileUrl ) ) { // check whether config is equal bool shouldAsk = true; if( projectFileUrl == dlg->selectedUrl() ) { if( projectFileUrl.isLocalFile() ) { shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), dlg ); } else { shouldAsk = false; QTemporaryFile tmpFile; if (tmpFile.open()) { auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName())); KJobWidgets::setWindow(downloadJob, qApp->activeWindow()); if (downloadJob->exec()) { shouldAsk = !equalProjectFile(tmpFile.fileName(), dlg); } } } } if ( shouldAsk ) { KGuiItem yes = KStandardGuiItem::yes(); yes.setText(i18n("Override")); yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration.")); yes.setIcon(QIcon()); KGuiItem no = KStandardGuiItem::no(); no.setText(i18n("Open Existing File")); no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration.")); no.setIcon(QIcon()); KGuiItem cancel = KStandardGuiItem::cancel(); cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project.")); int ret = KMessageBox::questionYesNoCancel(qApp->activeWindow(), i18n("There already exists a project configuration file at %1.\n" "Do you want to override it or open the existing file?", projectFileUrl.toDisplayString(QUrl::PreferLocalFile)), i18n("Override existing project configuration"), yes, no, cancel ); if ( ret == KMessageBox::No ) { writeProjectConfigToFile = false; } else if ( ret == KMessageBox::Cancel ) { return QUrl(); } // else fall through and write new file } else { writeProjectConfigToFile = false; } } if (writeProjectConfigToFile) { Path projectConfigDir(projectFileUrl); projectConfigDir.setLastPathSegment(QStringLiteral(".kdev4")); auto delJob = KIO::del(projectConfigDir.toUrl()); delJob->exec(); if (!writeProjectSettingsToConfigFile(projectFileUrl, dlg)) { KMessageBox::error(d->m_core->uiControllerInternal()->defaultMainWindow(), i18n("Unable to create configuration file %1", projectFileUrl.url())); return QUrl(); } } return projectFileUrl; } bool ProjectDialogProvider::userWantsReopen() { Q_ASSERT(d); return (KMessageBox::questionYesNo( d->m_core->uiControllerInternal()->defaultMainWindow(), i18n( "Reopen the current project?" ) ) == KMessageBox::No) ? false : true; } void ProjectController::setDialogProvider(IProjectDialogProvider* dialog) { Q_ASSERT(d->dialog); delete d->dialog; d->dialog = dialog; } ProjectController::ProjectController( Core* core ) : IProjectController( core ), d( new ProjectControllerPrivate( this ) ) { qRegisterMetaType>(); setObjectName(QStringLiteral("ProjectController")); d->m_core = core; d->model = new ProjectModel(); //NOTE: this is required to be called here, such that the // actions are available when the UI controller gets // initialized *before* the project controller if (Core::self()->setupFlags() != Core::NoUi) { setupActions(); } } void ProjectController::setupActions() { KActionCollection * ac = d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction*action; d->m_openProject = action = ac->addAction( QStringLiteral("project_open") ); action->setText(i18nc( "@action", "Open / Import Project..." ) ); action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop 4 project or import " "an existing Project into KDevelop 4. This entry " "allows one to select a KDevelop4 project file " "or an existing directory to open it in KDevelop. " "When opening an existing directory that does " "not yet have a KDevelop4 project file, the file " "will be created." ) ); action->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect(action, &QAction::triggered, this, [&] { openProject(); }); d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") ); action->setText(i18nc( "@action", "Fetch Project..." ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) ); action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch " "and then imports it into KDevelop 4." ) ); // action->setIcon(QIcon::fromTheme("project-open")); connect( action, &QAction::triggered, this, &ProjectController::fetchProject ); // action = ac->addAction( "project_close" ); // action->setText( i18n( "C&lose Project" ) ); // connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) ); // action->setToolTip( i18n( "Close project" ) ); // action->setWhatsThis( i18n( "Closes the current project." ) ); // action->setEnabled( false ); d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") ); connect( action, &QAction::triggered, this, [&] { d->closeSelectedProjects(); } ); action->setText( i18nc( "@action", "Close Project(s)" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) ); action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) ); action->setEnabled( false ); d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") ); connect( action, &QAction::triggered, this, [&] { d->openProjectConfig(); } ); action->setText( i18n( "Open Configuration..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) ); action->setEnabled( false ); action = ac->addAction( QStringLiteral("commit_current_project") ); connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject ); action->setText( i18n( "Commit Current Project..." ) ); action->setIconText( i18n( "Commit..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) ); connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged, this, [&] (Sublime::Area* area) { d->areaChanged(area); }); d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action); KSharedConfig * config = KSharedConfig::openConfig().data(); // KConfigGroup group = config->group( "General Options" ); d->m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this); ac->addAction( QStringLiteral("project_open_recent"), d->m_recentProjectsAction ); d->m_recentProjectsAction->setText( i18n( "Open Recent Project" ) ); d->m_recentProjectsAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) ); d->m_recentProjectsAction->loadEntries( KConfigGroup(config, "RecentProjects") ); QAction* openProjectForFileAction = new QAction( this ); ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction); openProjectForFileAction->setText(i18n("Open Project for Current File")); connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot); } ProjectController::~ProjectController() { delete d->model; delete d->dialog; } void ProjectController::cleanup() { if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } saveRecentProjectsActionEntries(); d->m_cleaningUp = true; if( buildSetModel() ) { buildSetModel()->storeToSession( Core::self()->activeSession() ); } closeAllProjects(); } void ProjectController::saveRecentProjectsActionEntries() { if (!d->m_recentProjectsAction) return; auto config = KSharedConfig::openConfig(); KConfigGroup recentGroup = config->group("RecentProjects"); d->m_recentProjectsAction->saveEntries( recentGroup ); config->sync(); } void ProjectController::initialize() { d->buildset = new ProjectBuildSetModel( this ); buildSetModel()->loadFromSession( Core::self()->activeSession() ); connect( this, &ProjectController::projectOpened, d->buildset, &ProjectBuildSetModel::loadFromProject ); connect( this, &ProjectController::projectClosing, d->buildset, &ProjectBuildSetModel::saveToProject ); connect( this, &ProjectController::projectClosed, d->buildset, &ProjectBuildSetModel::projectClosed ); d->m_changesModel = new ProjectChangesModel(this); loadSettings(false); d->dialog = new ProjectDialogProvider(d.data()); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"), this, QDBusConnection::ExportScriptableSlots ); KSharedConfigPtr config = Core::self()->activeSession()->config(); KConfigGroup group = config->group( "General Options" ); const auto projects = group.readEntry( "Open Projects", QList() ); connect( Core::self()->selectionController(), &ISelectionController::selectionChanged, this, [&] () { d->updateActionStates(); } ); connect(this, &ProjectController::projectOpened, this, [&] () { d->updateActionStates(); }); connect(this, &ProjectController::projectClosing, this, [&] () { d->updateActionStates(); }); QTimer::singleShot(0, this, [this, projects](){ openProjects(projects); emit initialized(); }); } void ProjectController::openProjects(const QList& projects) { foreach (const QUrl& url, projects) openProject(url); } void ProjectController::loadSettings( bool projectIsLoaded ) { Q_UNUSED(projectIsLoaded) } void ProjectController::saveSettings( bool projectIsLoaded ) { Q_UNUSED( projectIsLoaded ); } int ProjectController::projectCount() const { return d->m_projects.count(); } IProject* ProjectController::projectAt( int num ) const { if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() ) return d->m_projects.at( num ); return nullptr; } QList ProjectController::projects() const { return d->m_projects; } void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries) { KIO::SimpleJob* job(dynamic_cast(_job)); Q_ASSERT(job); foreach(const KIO::UDSEntry& entry, entries) { if(d->m_foundProjectFile) break; if(!entry.isDir()) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if(name.endsWith(QLatin1String(".kdev4"))) { //We have found a project-file, open it openProject(Path(Path(job->url()), name).toUrl()); d->m_foundProjectFile = true; } } } } void ProjectController::openProjectForUrlSlot(bool) { if(ICore::self()->documentController()->activeDocument()) { QUrl url = ICore::self()->documentController()->activeDocument()->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(!project) { openProjectForUrl(url); }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("Project already open: %1", project->name())); } }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No active document")); } } void ProjectController::openProjectForUrl(const QUrl& sourceUrl) { Q_ASSERT(!sourceUrl.isRelative()); QUrl dirUrl = sourceUrl; if (sourceUrl.isLocalFile() && !QFileInfo(sourceUrl.toLocalFile()).isDir()) { dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); } QUrl testAt = dirUrl; d->m_foundProjectFile = false; while(!testAt.path().isEmpty()) { KIO::ListJob* job = KIO::listDir(testAt); connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); job->exec(); if(d->m_foundProjectFile) { //Fine! We have directly opened the project d->m_foundProjectFile = false; return; } QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename); if(oldTest == testAt) break; } QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl); if(askForOpen.isValid()) openProject(askForOpen); } void ProjectController::openProject( const QUrl &projectFile ) { QUrl url = projectFile; if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); if ( url.isEmpty() ) { return; } } Q_ASSERT(!url.isRelative()); QList existingSessions; if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url)) { foreach( const Session* session, Core::self()->sessionController()->sessions()) { if(session->containedProjects().contains(url)) { existingSessions << session; #if 0 ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc. //If this session is empty, close it if(Core::self()->sessionController()->activeSession()->description().isEmpty()) { //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); } #endif } } } if ( ! existingSessions.isEmpty() ) { ScopedDialog dialog(Core::self()->uiControllerInternal()->activeMainWindow()); dialog->setWindowTitle(i18n("Project Already Open")); auto mainLayout = new QVBoxLayout(dialog); mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one " "other session.
What do you want to do?"))); QGroupBox sessions; sessions.setLayout(new QVBoxLayout); QRadioButton* newSession = new QRadioButton(i18n("Add project to current session")); sessions.layout()->addWidget(newSession); newSession->setChecked(true); foreach ( const Session* session, existingSessions ) { QRadioButton* button = new QRadioButton(i18n("Open session %1", session->description())); button->setProperty("sessionid", session->id().toString()); sessions.layout()->addWidget(button); } sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); mainLayout->addWidget(&sessions); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if (!dialog->exec()) return; foreach ( const QObject* obj, sessions.children() ) { if ( const QRadioButton* button = qobject_cast(obj) ) { QString sessionid = button->property("sessionid").toString(); if ( button->isChecked() && ! sessionid.isEmpty() ) { Core::self()->sessionController()->loadSession(sessionid); return; } } } } if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); } if ( !url.isEmpty() ) { d->importProject(url); } } bool ProjectController::fetchProjectFromUrl(const QUrl& repoUrl) { IPlugin* vcsOrProviderPlugin = nullptr; // TODO: query also projectprovider plugins, and that before plain vcs plugins // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things auto* pluginController = d->m_core->pluginController(); const auto& vcsPlugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); for (auto* plugin : vcsPlugins) { auto* iface = plugin->extension(); if (iface->isValidRemoteRepositoryUrl(repoUrl)) { vcsOrProviderPlugin = plugin; break; } } if (!vcsOrProviderPlugin) { KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString())); return false; } const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); if (!url.isEmpty()) { d->importProject(url); } return true; } void ProjectController::fetchProject() { QUrl url = d->dialog->askProjectConfigLocation(true); if ( !url.isEmpty() ) { d->importProject(url); } } void ProjectController::projectImportingFinished( IProject* project ) { if( !project ) { qCWarning(SHELL) << "OOOPS: 0-pointer project"; return; } IPlugin *managerPlugin = project->managerPlugin(); QList pluglist; pluglist.append( managerPlugin ); d->m_projectPlugins.insert( project, pluglist ); d->m_projects.append( project ); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } if (Core::self()->setupFlags() != Core::NoUi) { d->m_recentProjectsAction->addUrl( project->projectFile().toUrl() ); saveRecentProjectsActionEntries(); } Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl())); d->m_currentlyOpening.removeAll(project->projectFile().toUrl()); emit projectOpened( project ); reparseProject(project); } // helper method for closeProject() void ProjectController::unloadUnusedProjectPlugins(IProject* proj) { QList pluginsForProj = d->m_projectPlugins.value( proj ); d->m_projectPlugins.remove( proj ); QList otherProjectPlugins; + otherProjectPlugins.reserve(d->m_projectPlugins.size()); Q_FOREACH( const QList& _list, d->m_projectPlugins ) { otherProjectPlugins << _list; } QSet pluginsForProjSet = QSet::fromList( pluginsForProj ); QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins ); // loaded - target = tobe unloaded. QSet tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet ); Q_FOREACH( IPlugin* _plugin, tobeRemoved ) { KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin ); if( _plugInfo.isValid() ) { QString _plugName = _plugInfo.pluginId(); qCDebug(SHELL) << "about to unloading :" << _plugName; Core::self()->pluginController()->unloadPlugin( _plugName ); } } } // helper method for closeProject() void ProjectController::closeAllOpenedFiles(IProject* proj) { foreach(IDocument* doc, Core::self()->documentController()->openDocuments()) { if (proj->inProject(IndexedString(doc->url()))) { doc->close(); } } } // helper method for closeProject() void ProjectController::initializePluginCleanup(IProject* proj) { // Unloading (and thus deleting) these plugins is not a good idea just yet // as we're being called by the view part and it gets deleted when we unload the plugin(s) // TODO: find a better place to unload connect(proj, &IProject::destroyed, this, [&] { d->unloadAllProjectPlugins(); }); } void ProjectController::takeProject(IProject* proj) { if (!proj) { return; } // loading might have failed d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); d->m_projects.removeAll(proj); emit projectClosing(proj); //Core::self()->saveSettings(); // The project file is being closed. // Now we can save settings for all of the Core // objects including this one!! unloadUnusedProjectPlugins(proj); closeAllOpenedFiles(proj); proj->close(); if (d->m_projects.isEmpty()) { initializePluginCleanup(proj); } if(!d->m_cleaningUp) d->saveListOfOpenedProjects(); emit projectClosed(proj); } void ProjectController::closeProject(IProject* proj) { takeProject(proj); proj->deleteLater(); // be safe when deleting } void ProjectController::closeAllProjects() { foreach (auto project, d->m_projects) { closeProject(project); } } void ProjectController::abortOpeningProject(IProject* proj) { d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); emit projectOpeningAborted(proj); } ProjectModel* ProjectController::projectModel() { return d->model; } IProject* ProjectController::findProjectForUrl( const QUrl& url ) const { if (d->m_projects.isEmpty()) { return nullptr; } ProjectBaseItem* item = d->model->itemForPath(IndexedString(url)); if (item) { return item->project(); } return nullptr; } IProject* ProjectController::findProjectByName( const QString& name ) { Q_FOREACH( IProject* proj, d->m_projects ) { if( proj->name() == name ) { return proj; } } return nullptr; } void ProjectController::configureProject( IProject* project ) { d->projectConfig( project ); } void ProjectController::addProject(IProject* project) { Q_ASSERT(project); if (d->m_projects.contains(project)) { qCWarning(SHELL) << "Project already tracked by this project controller:" << project; return; } // fake-emit signals so listeners are aware of a new project being added emit projectAboutToBeOpened(project); project->setParent(this); d->m_projects.append(project); emit projectOpened(project); } bool ProjectController::isProjectNameUsed( const QString& name ) const { foreach( IProject* p, projects() ) { if( p->name() == name ) { return true; } } return false; } QUrl ProjectController::projectsBaseDirectory() const { KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" ); return group.readEntry("Projects Base Directory", QUrl::fromLocalFile(QDir::homePath() + QLatin1String("/projects"))); } QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(!project) { // Find a project with the correct base directory at least foreach(IProject* candidateProject, Core::self()->projectController()->projects()) { if(candidateProject->path().toUrl().isParentOf(url)) { project = candidateProject; break; } } } Path parent = Path(url).parent(); QString prefixText; if (project) { if (format == FormatHtml) { prefixText = QLatin1String("") + project->name() + QLatin1String("/"); } else { prefixText = project->name() + QLatin1Char(':'); } QString relativePath = project->path().relativePath(parent); if(relativePath.startsWith(QLatin1String("./"))) { relativePath = relativePath.mid(2); } if (!relativePath.isEmpty()) { prefixText += relativePath + QLatin1Char('/'); } } else { prefixText = parent.pathOrUrl() + QLatin1Char('/'); } return prefixText; } QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(project && project->path() == Path(url)) { if (format == FormatHtml) { return QLatin1String("") + project->name() + QLatin1String(""); } else { return project->name(); } } QString prefixText = prettyFilePath( url, format ); if (format == FormatHtml) { return prefixText + QLatin1String("") + url.fileName() + QLatin1String(""); } else { return prefixText + url.fileName(); } } ContextMenuExtension ProjectController::contextMenuExtension(Context* ctx, QWidget* parent) { Q_UNUSED(parent); ContextMenuExtension ext; if ( ctx->type() != Context::ProjectItemContext || !static_cast(ctx)->items().isEmpty() ) { return ext; } ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentProjectsAction); return ext; } ProjectBuildSetModel* ProjectController::buildSetModel() { return d->buildset; } ProjectChangesModel* ProjectController::changesModel() { return d->m_changesModel; } void ProjectController::commitCurrentProject() { IDocument* doc=ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url=doc->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IPlugin* plugin = project->versionControlPlugin(); IBasicVersionControl* vcs=plugin->extension(); if(vcs) { ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent); const Path basePath = project->path(); VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl())); bool ret = showVcsDiff(patchSource); if(!ret) { ScopedDialog commitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } } } QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const { Path path(path_); IProject* sourceDirProject = nullptr, *buildDirProject = nullptr; Q_FOREACH(IProject* proj, d->m_projects) { if(proj->path().isParentOf(path) || proj->path() == path) sourceDirProject = proj; if(proj->buildSystemManager()) { Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem()); if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path)) buildDirProject = proj; } } if(!reverse) { // Map-target is the build directory if(sourceDirProject && sourceDirProject->buildSystemManager()) { // We're in the source, map into the build directory QString relativePath = sourceDirProject->path().relativePath(path); Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem()); build.addPath(relativePath); while(!QFile::exists(build.path())) build = build.parent(); return build.pathOrUrl(); }else if(buildDirProject && fallbackRoot) { // We're in the build directory, map to the build directory root return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl(); } }else{ // Map-target is the source directory if(buildDirProject) { Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()); // We're in the source, map into the build directory QString relativePath = build.relativePath(path); Path source = buildDirProject->path(); source.addPath(relativePath); while(!QFile::exists(source.path())) source = source.parent(); return source.pathOrUrl(); }else if(sourceDirProject && fallbackRoot) { // We're in the source directory, map to the root return sourceDirProject->path().pathOrUrl(); } } return QString(); } void ProjectController::reparseProject( IProject* project, bool forceUpdate ) { if (auto job = d->m_parseJobs.value(project)) { job->kill(); } d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate); ICore::self()->runController()->registerJob(d->m_parseJobs[project]); } } diff --git a/kdevplatform/shell/projectsourcepage.cpp b/kdevplatform/shell/projectsourcepage.cpp index 0be7e4f943..77fc40ae1a 100644 --- a/kdevplatform/shell/projectsourcepage.cpp +++ b/kdevplatform/shell/projectsourcepage.cpp @@ -1,316 +1,318 @@ /*************************************************************************** * Copyright (C) 2010 by Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "projectsourcepage.h" #include "ui_projectsourcepage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static const int FROM_FILESYSTEM_SOURCE_INDEX = 0; ProjectSourcePage::ProjectSourcePage(const QUrl& initial, const QUrl& repoUrl, IPlugin* preSelectPlugin, QWidget* parent) : QWidget(parent) { m_ui = new Ui::ProjectSourcePage; m_ui->setupUi(this); m_ui->status->setCloseButtonVisible(false); m_ui->status->setMessageType(KMessageWidget::Error); m_ui->workingDir->setUrl(initial); m_ui->workingDir->setMode(KFile::Directory); m_ui->remoteWidget->setLayout(new QVBoxLayout(m_ui->remoteWidget)); m_ui->sources->addItem(QIcon::fromTheme(QStringLiteral("folder")), i18n("From File System")); m_plugins.append(nullptr); int preselectIndex = -1; IPluginController* pluginManager = ICore::self()->pluginController(); QList plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IBasicVersionControl") ); + m_plugins.reserve(m_plugins.size() + plugins.size()); foreach( IPlugin* p, plugins ) { if (p == preSelectPlugin) { preselectIndex = m_plugins.count(); } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } plugins = pluginManager->allPluginsForExtension( QStringLiteral("org.kdevelop.IProjectProvider") ); + m_plugins.reserve(m_plugins.size() + plugins.size()); foreach( IPlugin* p, plugins ) { if (p == preSelectPlugin) { preselectIndex = m_plugins.count(); } m_plugins.append(p); m_ui->sources->addItem(QIcon::fromTheme(pluginManager->pluginInfo(p).iconName()), p->extension()->name()); } if (preselectIndex == -1) { // "From File System" is quite unlikely to be what the user wants, so default to first real plugin... const int defaultIndex = (m_plugins.count() > 1) ? 1 : 0; KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); preselectIndex = configGroup.readEntry("LastProviderIndex", defaultIndex); } preselectIndex = qBound(0, preselectIndex, m_ui->sources->count() - 1); m_ui->sources->setCurrentIndex(preselectIndex); setSourceWidget(preselectIndex, repoUrl); // connect as last step, otherwise KMessageWidget could get both animatedHide() and animatedShow() // during setup and due to a bug will ignore any but the first call // Only fixed for KF5 5.32 connect(m_ui->workingDir, &KUrlRequester::textChanged, this, &ProjectSourcePage::reevaluateCorrection); connect(m_ui->sources, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSourcePage::setSourceIndex); connect(m_ui->get, &QPushButton::clicked, this, &ProjectSourcePage::checkoutVcsProject); } ProjectSourcePage::~ProjectSourcePage() { KConfigGroup configGroup = KSharedConfig::openConfig()->group("Providers"); configGroup.writeEntry("LastProviderIndex", m_ui->sources->currentIndex()); delete m_ui; } void ProjectSourcePage::setSourceIndex(int index) { setSourceWidget(index, QUrl()); } void ProjectSourcePage::setSourceWidget(int index, const QUrl& repoUrl) { m_locationWidget = nullptr; m_providerWidget = nullptr; QLayout* remoteWidgetLayout = m_ui->remoteWidget->layout(); QLayoutItem *child; while ((child = remoteWidgetLayout->takeAt(0)) != nullptr) { delete child->widget(); delete child; } IBasicVersionControl* vcIface = vcsPerIndex(index); IProjectProvider* providerIface; bool found=false; if(vcIface) { found=true; m_locationWidget=vcIface->vcsLocation(m_ui->sourceBox); connect(m_locationWidget, &VcsLocationWidget::changed, this, &ProjectSourcePage::locationChanged); // set after connect, to trigger handler if (!repoUrl.isEmpty()) { m_locationWidget->setLocation(repoUrl); } remoteWidgetLayout->addWidget(m_locationWidget); } else { providerIface = providerPerIndex(index); if(providerIface) { found=true; m_providerWidget=providerIface->providerWidget(m_ui->sourceBox); connect(m_providerWidget, &IProjectProviderWidget::changed, this, &ProjectSourcePage::projectChanged); remoteWidgetLayout->addWidget(m_providerWidget); } } reevaluateCorrection(); m_ui->sourceBox->setVisible(found); } IBasicVersionControl* ProjectSourcePage::vcsPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } IProjectProvider* ProjectSourcePage::providerPerIndex(int index) { IPlugin* p = m_plugins.value(index); if(!p) return nullptr; else return p->extension(); } VcsJob* ProjectSourcePage::jobPerCurrent() { QUrl url=m_ui->workingDir->url(); IPlugin* p=m_plugins[m_ui->sources->currentIndex()]; VcsJob* job=nullptr; if(IBasicVersionControl* iface=p->extension()) { Q_ASSERT(iface && m_locationWidget); job=iface->createWorkingCopy(m_locationWidget->location(), url); } else if(m_providerWidget) { job=m_providerWidget->createWorkingCopy(url); } return job; } void ProjectSourcePage::checkoutVcsProject() { QUrl url=m_ui->workingDir->url(); QDir d(url.toLocalFile()); if(!url.isLocalFile() && !d.exists()) { bool corr = d.mkpath(d.path()); if(!corr) { KMessageBox::error(nullptr, i18n("Could not create the directory: %1", d.path())); return; } } VcsJob* job=jobPerCurrent(); if (!job) { return; } m_ui->sources->setEnabled(false); m_ui->sourceBox->setEnabled(false); m_ui->workingDir->setEnabled(false); m_ui->get->setEnabled(false); m_ui->creationProgress->setValue(m_ui->creationProgress->minimum()); connect(job, &VcsJob::result, this, &ProjectSourcePage::projectReceived); // Can't use new signal-slot syntax, KJob::percent is private :/ connect(job, SIGNAL(percent(KJob*,ulong)), SLOT(progressChanged(KJob*,ulong))); connect(job, &VcsJob::infoMessage, this, &ProjectSourcePage::infoMessage); ICore::self()->runController()->registerJob(job); } void ProjectSourcePage::progressChanged(KJob*, unsigned long value) { m_ui->creationProgress->setValue(value); } void ProjectSourcePage::infoMessage(KJob* , const QString& text, const QString& /*rich*/) { m_ui->creationProgress->setFormat(i18nc("Format of the progress bar text. progress and info", "%1 : %p%", text)); } void ProjectSourcePage::projectReceived(KJob* job) { if (job->error()) { m_ui->creationProgress->setValue(0); } else { m_ui->creationProgress->setValue(m_ui->creationProgress->maximum()); } reevaluateCorrection(); m_ui->creationProgress->setFormat(QStringLiteral("%p%")); } void ProjectSourcePage::reevaluateCorrection() { //TODO: Probably we should just ignore remote URL's, I don't think we're ever going //to support checking out to remote directories const QUrl cwd = m_ui->workingDir->url(); const QDir dir = cwd.toLocalFile(); // case where we import a project from local file system if (m_ui->sources->currentIndex() == FROM_FILESYSTEM_SOURCE_INDEX) { emit isCorrect(dir.exists()); return; } // all other cases where remote locations need to be specified bool correct=!cwd.isRelative() && (!cwd.isLocalFile() || QDir(cwd.adjusted(QUrl::RemoveFilename).toLocalFile()).exists()); emit isCorrect(correct && m_ui->creationProgress->value() == m_ui->creationProgress->maximum()); const bool validWidget = ((m_locationWidget && m_locationWidget->isCorrect()) || (m_providerWidget && m_providerWidget->isCorrect())); const bool isFolderEmpty = (correct && cwd.isLocalFile() && dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()); const bool validToCheckout = correct && validWidget && (!dir.exists() || isFolderEmpty); m_ui->get->setEnabled(validToCheckout); m_ui->creationProgress->setEnabled(validToCheckout); if(!correct) setStatus(i18n("You need to specify a valid or nonexistent directory to check out a project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !validWidget) setStatus(i18n("You need to specify the source for your remote project")); else if(!m_ui->get->isEnabled() && m_ui->workingDir->isEnabled() && !isFolderEmpty) setStatus(i18n("You need to specify an empty folder as your project destination")); else clearStatus(); } void ProjectSourcePage::locationChanged() { Q_ASSERT(m_locationWidget); if(m_locationWidget->isCorrect()) { QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf(QLatin1Char('/'))+1); QUrl current = QUrl::fromUserInput(currentUrl + m_locationWidget->projectName()); m_ui->workingDir->setUrl(current); } else reevaluateCorrection(); } void ProjectSourcePage::projectChanged(const QString& name) { Q_ASSERT(m_providerWidget); QString currentUrl = m_ui->workingDir->text(); currentUrl = currentUrl.left(currentUrl.lastIndexOf(QLatin1Char('/'))+1); QUrl current = QUrl::fromUserInput(currentUrl + name); m_ui->workingDir->setUrl(current); } void ProjectSourcePage::setStatus(const QString& message) { m_ui->status->setText(message); m_ui->status->animatedShow(); } void ProjectSourcePage::clearStatus() { #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5,32,0) // workaround for KMessageWidget bug: // animatedHide() will not explicitly hide the widget if it is not yet shown. // So if it has never been explicitly hidden otherwise, // if show() is called on the parent later the KMessageWidget will be shown as well. // As this method is sometimes called when the page is created and thus not yet shown, // we have to ensure the hidden state ourselves here. if (!m_ui->status->isVisible()) { m_ui->status->hide(); return; } #endif m_ui->status->animatedHide(); } QUrl ProjectSourcePage::workingDir() const { return m_ui->workingDir->url(); } diff --git a/kdevplatform/shell/runcontroller.cpp b/kdevplatform/shell/runcontroller.cpp index 7cd6440a16..410f0e5cf5 100644 --- a/kdevplatform/shell/runcontroller.cpp +++ b/kdevplatform/shell/runcontroller.cpp @@ -1,1037 +1,1040 @@ /* This file is part of KDevelop Copyright 2007-2008 Hamish Rodda Copyright 2008 Aleix Pol This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "runcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "uicontroller.h" #include "projectcontroller.h" #include "mainwindow.h" #include "launchconfiguration.h" #include "launchconfigurationdialog.h" #include "unitylauncher.h" #include "debug.h" #include #include #include #include using namespace KDevelop; namespace { namespace Strings { QString LaunchConfigurationsGroup() { return QStringLiteral("Launch"); } QString LaunchConfigurationsListEntry() { return QStringLiteral("Launch Configurations"); } QString CurrentLaunchConfigProjectEntry() { return QStringLiteral("Current Launch Config Project"); } QString CurrentLaunchConfigNameEntry() { return QStringLiteral("Current Launch Config GroupName"); } QString ConfiguredFromProjectItemEntry() { return QStringLiteral("Configured from ProjectItem"); } } } typedef QPair Target; Q_DECLARE_METATYPE(Target) //TODO: Doesn't handle add/remove of launch configs in the dialog or renaming of configs //TODO: Doesn't auto-select launch configs opened from projects class DebugMode : public ILaunchMode { public: DebugMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("debug-run")); } QString id() const override { return QStringLiteral("debug"); } QString name() const override { return i18n("Debug"); } }; class ProfileMode : public ILaunchMode { public: ProfileMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("office-chart-area")); } QString id() const override { return QStringLiteral("profile"); } QString name() const override { return i18n("Profile"); } }; class ExecuteMode : public ILaunchMode { public: ExecuteMode() {} QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("system-run")); } QString id() const override { return QStringLiteral("execute"); } QString name() const override { return i18n("Execute"); } }; class KDevelop::RunControllerPrivate { public: QItemDelegate* delegate; IRunController::State state; RunController* q; QHash jobs; QAction* stopAction; KActionMenu* stopJobsMenu; QAction* runAction; QAction* dbgAction; KSelectAction* currentTargetAction; QMap launchConfigurationTypes; QList launchConfigurations; QMap launchModes; QMap > launchAsInfo; KDevelop::ProjectBaseItem* contextItem; DebugMode* debugMode; ExecuteMode* executeMode; ProfileMode* profileMode; UnityLauncher* unityLauncher; bool hasLaunchConfigType( const QString& typeId ) { return launchConfigurationTypes.contains( typeId ); } void saveCurrentLaunchAction() { if (!currentTargetAction) return; if( currentTargetAction->currentAction() ) { KConfigGroup grp = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); LaunchConfiguration* l = static_cast( currentTargetAction->currentAction()->data().value() ); grp.writeEntry( Strings::CurrentLaunchConfigProjectEntry(), l->project() ? l->project()->name() : QString() ); grp.writeEntry( Strings::CurrentLaunchConfigNameEntry(), l->configGroupName() ); grp.sync(); } } QString launchActionText( LaunchConfiguration* l ) { QString label; if( l->project() ) { label = QStringLiteral("%1 : %2").arg( l->project()->name(), l->name()); } else { label = l->name(); } return label; } void launchAs( int id ) { //qCDebug(SHELL) << "Launching id:" << id; QPair info = launchAsInfo[id]; //qCDebug(SHELL) << "fetching type and mode:" << info.first << info.second; LaunchConfigurationType* type = launchConfigurationTypeForId( info.first ); ILaunchMode* mode = q->launchModeForId( info.second ); //qCDebug(SHELL) << "got mode and type:" << type << type->id() << mode << mode->id(); if( type && mode ) { ILauncher* launcher = nullptr; foreach (ILauncher *l, type->launchers()) { //qCDebug(SHELL) << "available launcher" << l << l->id() << l->supportedModes(); if (l->supportedModes().contains(mode->id())) { launcher = l; break; } } if (launcher) { QStringList itemPath = Core::self()->projectController()->projectModel()->pathFromIndex(contextItem->index()); ILaunchConfiguration* ilaunch = nullptr; foreach (LaunchConfiguration *l, launchConfigurations) { QStringList path = l->config().readEntry(Strings::ConfiguredFromProjectItemEntry(), QStringList()); if (l->type() == type && path == itemPath) { qCDebug(SHELL) << "already generated ilaunch" << path; ilaunch = l; break; } } if (!ilaunch) { ilaunch = q->createLaunchConfiguration( type, qMakePair( mode->id(), launcher->id() ), contextItem->project(), contextItem->text() ); LaunchConfiguration* launch = static_cast(ilaunch); type->configureLaunchFromItem( launch->config(), contextItem ); launch->config().writeEntry(Strings::ConfiguredFromProjectItemEntry(), itemPath); //qCDebug(SHELL) << "created config, launching"; } else { //qCDebug(SHELL) << "reusing generated config, launching"; } q->setDefaultLaunch(ilaunch); q->execute( mode->id(), ilaunch ); } } } void updateCurrentLaunchAction() { if (!currentTargetAction) return; KConfigGroup launchGrp = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); QString currentLaunchProject = launchGrp.readEntry( Strings::CurrentLaunchConfigProjectEntry(), "" ); QString currentLaunchName = launchGrp.readEntry( Strings::CurrentLaunchConfigNameEntry(), "" ); LaunchConfiguration* l = nullptr; if( currentTargetAction->currentAction() ) { l = static_cast( currentTargetAction->currentAction()->data().value() ); } else if( !launchConfigurations.isEmpty() ) { l = launchConfigurations.at( 0 ); } if( l && ( ( !currentLaunchProject.isEmpty() && ( !l->project() || l->project()->name() != currentLaunchProject ) ) || l->configGroupName() != currentLaunchName ) ) { foreach( QAction* a, currentTargetAction->actions() ) { LaunchConfiguration* l = static_cast( qvariant_cast( a->data() ) ); if( currentLaunchName == l->configGroupName() && ( ( currentLaunchProject.isEmpty() && !l->project() ) || ( l->project() && l->project()->name() == currentLaunchProject ) ) ) { a->setChecked( true ); break; } } } if( !currentTargetAction->currentAction() ) { qCDebug(SHELL) << "oops no current action, using first if list is non-empty"; if( !currentTargetAction->actions().isEmpty() ) { currentTargetAction->actions().at(0)->setChecked( true ); } } } void addLaunchAction( LaunchConfiguration* l ) { if (!currentTargetAction) return; QAction* action = currentTargetAction->addAction(launchActionText( l )); action->setData(qVariantFromValue(l)); } void readLaunchConfigs( const KSharedConfigPtr& cfg, IProject* prj ) { KConfigGroup group(cfg, Strings::LaunchConfigurationsGroup()); QStringList configs = group.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); foreach( const QString& cfg, configs ) { KConfigGroup grp = group.group( cfg ); if( launchConfigurationTypeForId( grp.readEntry( LaunchConfiguration::LaunchConfigurationTypeEntry(), "" ) ) ) { q->addLaunchConfiguration( new LaunchConfiguration( grp, prj ) ); } } } LaunchConfigurationType* launchConfigurationTypeForId( const QString& id ) { QMap::iterator it = launchConfigurationTypes.find( id ); if( it != launchConfigurationTypes.end() ) { return it.value(); } else { qCWarning(SHELL) << "couldn't find type for id:" << id << ". Known types:" << launchConfigurationTypes.keys(); } return nullptr; } }; RunController::RunController(QObject *parent) : IRunController(parent) , d(new RunControllerPrivate) { setObjectName(QStringLiteral("RunController")); QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kdevelop/RunController"), this, QDBusConnection::ExportScriptableSlots); // TODO: need to implement compile only if needed before execute // TODO: need to implement abort all running programs when project closed d->currentTargetAction = nullptr; d->state = Idle; d->q = this; d->delegate = new RunDelegate(this); d->contextItem = nullptr; d->executeMode = nullptr; d->debugMode = nullptr; d->profileMode = nullptr; d->unityLauncher = new UnityLauncher(this); d->unityLauncher->setLauncherId(KAboutData::applicationData().desktopFileName()); if(!(Core::self()->setupFlags() & Core::NoUi)) { // Note that things like registerJob() do not work without the actions, it'll simply crash. setupActions(); } } RunController::~RunController() = default; void KDevelop::RunController::launchChanged( LaunchConfiguration* l ) { foreach( QAction* a, d->currentTargetAction->actions() ) { if( static_cast( a->data().value() ) == l ) { a->setText( d->launchActionText( l ) ); break; } } } void RunController::cleanup() { delete d->executeMode; d->executeMode = nullptr; delete d->profileMode; d->profileMode = nullptr; delete d->debugMode; d->debugMode = nullptr; stopAllProcesses(); d->saveCurrentLaunchAction(); } void RunController::initialize() { d->executeMode = new ExecuteMode(); addLaunchMode( d->executeMode ); d->profileMode = new ProfileMode(); addLaunchMode( d->profileMode ); d->debugMode = new DebugMode; addLaunchMode( d->debugMode ); d->readLaunchConfigs( Core::self()->activeSession()->config(), nullptr ); foreach (IProject* project, Core::self()->projectController()->projects()) { slotProjectOpened(project); } connect(Core::self()->projectController(), &IProjectController::projectOpened, this, &RunController::slotProjectOpened); connect(Core::self()->projectController(), &IProjectController::projectClosing, this, &RunController::slotProjectClosing); connect(Core::self()->projectController(), &IProjectController::projectConfigurationChanged, this, &RunController::slotRefreshProject); if( (Core::self()->setupFlags() & Core::NoUi) == 0 ) { // Only do this in GUI mode d->updateCurrentLaunchAction(); } } KJob* RunController::execute(const QString& runMode, ILaunchConfiguration* launch) { if( !launch ) { qCDebug(SHELL) << "execute called without launch config!"; return nullptr; } LaunchConfiguration* run = static_cast(launch); //TODO: Port to launch framework, probably needs to be part of the launcher //if(!run.dependencies().isEmpty()) // ICore::self()->documentController()->saveAllDocuments(IDocument::Silent); //foreach(KJob* job, run.dependencies()) //{ // jobs.append(job); //} qCDebug(SHELL) << "mode:" << runMode; QString launcherId = run->launcherForMode( runMode ); qCDebug(SHELL) << "launcher id:" << launcherId; ILauncher* launcher = run->type()->launcherForId( launcherId ); if( !launcher ) { KMessageBox::error( qApp->activeWindow(), i18n("The current launch configuration does not support the '%1' mode.", runMode), QString()); return nullptr; } KJob* launchJob = launcher->start(runMode, run); registerJob(launchJob); return launchJob; } void RunController::setupActions() { QAction* action; // TODO not multi-window friendly, FIXME KActionCollection* ac = Core::self()->uiControllerInternal()->defaultMainWindow()->actionCollection(); action = new QAction(i18n("Configure Launches..."), this); ac->addAction(QStringLiteral("configure_launches"), action); action->setMenuRole(QAction::NoRole); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item action->setStatusTip(i18n("Open Launch Configuration Dialog")); action->setToolTip(i18nc("@info:tooltip", "Open Launch Configuration Dialog")); action->setWhatsThis(i18nc("@info:whatsthis", "Opens a dialog to setup new launch configurations, or to change the existing ones.")); connect(action, &QAction::triggered, this, &RunController::showConfigurationDialog); d->runAction = new QAction( QIcon::fromTheme(QStringLiteral("system-run")), i18n("Execute Launch"), this); d->runAction->setIconText( i18nc("Short text for 'Execute launch' used in the toolbar", "Execute") ); ac->setDefaultShortcut( d->runAction, Qt::SHIFT + Qt::Key_F9); d->runAction->setToolTip(i18nc("@info:tooltip", "Execute current launch")); d->runAction->setStatusTip(i18n("Execute current launch")); d->runAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration.")); ac->addAction(QStringLiteral("run_execute"), d->runAction); connect(d->runAction, &QAction::triggered, this, &RunController::slotExecute); d->dbgAction = new QAction( QIcon::fromTheme(QStringLiteral("debug-run")), i18n("Debug Launch"), this); ac->setDefaultShortcut( d->dbgAction, Qt::ALT + Qt::Key_F9); d->dbgAction->setIconText( i18nc("Short text for 'Debug launch' used in the toolbar", "Debug") ); d->dbgAction->setToolTip(i18nc("@info:tooltip", "Debug current launch")); d->dbgAction->setStatusTip(i18n("Debug current launch")); d->dbgAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration inside a Debugger.")); ac->addAction(QStringLiteral("run_debug"), d->dbgAction); connect(d->dbgAction, &QAction::triggered, this, &RunController::slotDebug); Core::self()->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(d->dbgAction); // TODO: at least get a profile target, it's sad to have the menu entry without a profiler // QAction* profileAction = new QAction( QIcon::fromTheme(""), i18n("Profile Launch"), this); // profileAction->setToolTip(i18nc("@info:tooltip", "Profile current launch")); // profileAction->setStatusTip(i18n("Profile current launch")); // profileAction->setWhatsThis(i18nc("@info:whatsthis", "Executes the target or the program specified in currently active launch configuration inside a Profiler.")); // ac->addAction("run_profile", profileAction); // connect(profileAction, SIGNAL(triggered(bool)), this, SLOT(slotProfile())); action = d->stopAction = new QAction( QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Stop All Jobs"), this); action->setIconText(i18nc("Short text for 'Stop All Jobs' used in the toolbar", "Stop All")); // Ctrl+Escape would be nicer, but that is taken by the ksysguard desktop shortcut ac->setDefaultShortcut( action, QKeySequence(QStringLiteral("Ctrl+Shift+Escape"))); action->setToolTip(i18nc("@info:tooltip", "Stop all currently running jobs")); action->setWhatsThis(i18nc("@info:whatsthis", "Requests that all running jobs are stopped.")); action->setEnabled(false); ac->addAction(QStringLiteral("run_stop_all"), action); connect(action, &QAction::triggered, this, &RunController::stopAllProcesses); Core::self()->uiControllerInternal()->area(0, QStringLiteral("debug"))->addAction(action); action = d->stopJobsMenu = new KActionMenu( QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Stop"), this); action->setIconText(i18nc("Short text for 'Stop' used in the toolbar", "Stop")); action->setToolTip(i18nc("@info:tooltip", "Menu allowing to stop individual jobs")); action->setWhatsThis(i18nc("@info:whatsthis", "List of jobs that can be stopped individually.")); action->setEnabled(false); ac->addAction(QStringLiteral("run_stop_menu"), action); d->currentTargetAction = new KSelectAction( i18n("Current Launch Configuration"), this); d->currentTargetAction->setToolTip(i18nc("@info:tooltip", "Current launch configuration")); d->currentTargetAction->setStatusTip(i18n("Current launch Configuration")); d->currentTargetAction->setWhatsThis(i18nc("@info:whatsthis", "Select which launch configuration to run when run is invoked.")); ac->addAction(QStringLiteral("run_default_target"), d->currentTargetAction); } LaunchConfigurationType* RunController::launchConfigurationTypeForId( const QString& id ) { return d->launchConfigurationTypeForId( id ); } void KDevelop::RunController::slotProjectOpened(KDevelop::IProject * project) { d->readLaunchConfigs( project->projectConfiguration(), project ); d->updateCurrentLaunchAction(); } void KDevelop::RunController::slotProjectClosing(KDevelop::IProject * project) { if (!d->currentTargetAction) return; foreach (QAction* action, d->currentTargetAction->actions()) { LaunchConfiguration* l = static_cast(qvariant_cast(action->data())); if ( project == l->project() ) { l->save(); d->launchConfigurations.removeAll(l); delete l; bool wasSelected = action->isChecked(); delete action; if (wasSelected && !d->currentTargetAction->actions().isEmpty()) d->currentTargetAction->actions().at(0)->setChecked(true); } } } void KDevelop::RunController::slotRefreshProject(KDevelop::IProject* project) { slotProjectClosing(project); slotProjectOpened(project); } void RunController::slotDebug() { if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("debug") ); } } void RunController::slotProfile() { if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("profile") ); } } void RunController::slotExecute() { if (d->launchConfigurations.isEmpty()) { showConfigurationDialog(); } if (!d->launchConfigurations.isEmpty()) { executeDefaultLaunch( QStringLiteral("execute") ); } } void KDevelop::RunController::showConfigurationDialog() const { LaunchConfigurationDialog dlg; dlg.exec(); } LaunchConfiguration* KDevelop::RunController::defaultLaunch() const { QAction* projectAction = d->currentTargetAction->currentAction(); if( projectAction ) return static_cast(qvariant_cast(projectAction->data())); return nullptr; } void KDevelop::RunController::registerJob(KJob * job) { if (!job) return; if (!(job->capabilities() & KJob::Killable)) { // see e.g. https://bugs.kde.org/show_bug.cgi?id=314187 qCWarning(SHELL) << "non-killable job" << job << "registered - this might lead to crashes on shutdown."; } if (!d->jobs.contains(job)) { QAction* stopJobAction = nullptr; if (Core::self()->setupFlags() != Core::NoUi) { stopJobAction = new QAction(job->objectName().isEmpty() ? i18n("<%1> Unnamed job", QString::fromUtf8(job->staticMetaObject.className())) : job->objectName(), this); stopJobAction->setData(QVariant::fromValue(static_cast(job))); d->stopJobsMenu->addAction(stopJobAction); connect (stopJobAction, &QAction::triggered, this, &RunController::slotKillJob); job->setUiDelegate( new KDialogJobUiDelegate() ); } d->jobs.insert(job, stopJobAction); connect( job, &KJob::finished, this, &RunController::finished ); connect( job, &KJob::destroyed, this, &RunController::jobDestroyed ); // FIXME percent is a private signal and thus we cannot use new connext syntax connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(jobPercentChanged())); IRunController::registerJob(job); emit jobRegistered(job); } job->start(); checkState(); } void KDevelop::RunController::unregisterJob(KJob * job) { IRunController::unregisterJob(job); Q_ASSERT(d->jobs.contains(job)); // Delete the stop job action QAction *action = d->jobs.take(job); if (action) action->deleteLater(); checkState(); emit jobUnregistered(job); } void KDevelop::RunController::checkState() { bool running = false; int jobCount = 0; int totalProgress = 0; for (auto it = d->jobs.constBegin(), end = d->jobs.constEnd(); it != end; ++it) { KJob *job = it.key(); if (!job->isSuspended()) { running = true; ++jobCount; totalProgress += job->percent(); } } d->unityLauncher->setProgressVisible(running); if (jobCount > 0) { d->unityLauncher->setProgress((totalProgress + 1) / jobCount); } else { d->unityLauncher->setProgress(0); } if ( ( d->state != Running ? false : true ) == running ) { d->state = running ? Running : Idle; emit runStateChanged(d->state); } if (Core::self()->setupFlags() != Core::NoUi) { d->stopAction->setEnabled(running); d->stopJobsMenu->setEnabled(running); } } void KDevelop::RunController::stopAllProcesses() { // composite jobs might remove child jobs, see also: // https://bugs.kde.org/show_bug.cgi?id=258904 // foreach already iterates over a copy foreach (KJob* job, d->jobs.keys()) { // now we check the real list whether it was deleted if (!d->jobs.contains(job)) continue; if (job->capabilities() & KJob::Killable) { job->kill(KJob::EmitResult); } else { qCWarning(SHELL) << "cannot stop non-killable job: " << job; } } } void KDevelop::RunController::slotKillJob() { QAction* action = dynamic_cast(sender()); Q_ASSERT(action); KJob* job = static_cast(qvariant_cast(action->data())); if (job->capabilities() & KJob::Killable) job->kill(); } void KDevelop::RunController::finished(KJob * job) { unregisterJob(job); switch (job->error()) { case KJob::NoError: case KJob::KilledJobError: case OutputJob::FailedShownError: break; default: { ///WARNING: do *not* use a nested event loop here, it might cause /// random crashes later on, see e.g.: /// https://bugs.kde.org/show_bug.cgi?id=309811 auto dialog = new QDialog(qApp->activeWindow()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(i18n("Process Error")); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, dialog); KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Warning, job->errorString(), QStringList(), QString(), nullptr, KMessageBox::NoExec); dialog->show(); } } } void RunController::jobDestroyed(QObject* job) { KJob* kjob = static_cast(job); if (d->jobs.contains(kjob)) { qCWarning(SHELL) << "job destroyed without emitting finished signal!"; unregisterJob(kjob); } } void RunController::jobPercentChanged() { checkState(); } void KDevelop::RunController::suspended(KJob * job) { Q_UNUSED(job); checkState(); } void KDevelop::RunController::resumed(KJob * job) { Q_UNUSED(job); checkState(); } QList< KJob * > KDevelop::RunController::currentJobs() const { return d->jobs.keys(); } QList RunController::launchConfigurations() const { QList configs; - foreach (LaunchConfiguration *config, launchConfigurationsInternal()) + const auto configsInternal = launchConfigurationsInternal(); + configs.reserve(configsInternal.size()); + for (LaunchConfiguration* config : configsInternal) { configs << config; + } return configs; } QList RunController::launchConfigurationsInternal() const { return d->launchConfigurations; } QList RunController::launchConfigurationTypes() const { return d->launchConfigurationTypes.values(); } void RunController::addConfigurationType( LaunchConfigurationType* type ) { if( !d->launchConfigurationTypes.contains( type->id() ) ) { d->launchConfigurationTypes.insert( type->id(), type ); } } void RunController::removeConfigurationType( LaunchConfigurationType* type ) { foreach( LaunchConfiguration* l, d->launchConfigurations ) { if( l->type() == type ) { removeLaunchConfigurationInternal( l ); } } d->launchConfigurationTypes.remove( type->id() ); } void KDevelop::RunController::addLaunchMode(KDevelop::ILaunchMode* mode) { if( !d->launchModes.contains( mode->id() ) ) { d->launchModes.insert( mode->id(), mode ); } } QList< KDevelop::ILaunchMode* > KDevelop::RunController::launchModes() const { return d->launchModes.values(); } void KDevelop::RunController::removeLaunchMode(KDevelop::ILaunchMode* mode) { d->launchModes.remove( mode->id() ); } KDevelop::ILaunchMode* KDevelop::RunController::launchModeForId(const QString& id) const { QMap::iterator it = d->launchModes.find( id ); if( it != d->launchModes.end() ) { return it.value(); } return nullptr; } void KDevelop::RunController::addLaunchConfiguration(KDevelop::LaunchConfiguration* l) { if( !d->launchConfigurations.contains( l ) ) { d->addLaunchAction( l ); d->launchConfigurations << l; if( !d->currentTargetAction->currentAction() ) { if( !d->currentTargetAction->actions().isEmpty() ) { d->currentTargetAction->actions().at(0)->setChecked( true ); } } connect( l, &LaunchConfiguration::nameChanged, this, &RunController::launchChanged ); } } void KDevelop::RunController::removeLaunchConfiguration(KDevelop::LaunchConfiguration* l) { KConfigGroup launcherGroup; if( l->project() ) { launcherGroup = l->project()->projectConfiguration()->group( Strings::LaunchConfigurationsGroup() ); } else { launcherGroup = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); } QStringList configs = launcherGroup.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); configs.removeAll( l->configGroupName() ); launcherGroup.deleteGroup( l->configGroupName() ); launcherGroup.writeEntry( Strings::LaunchConfigurationsListEntry(), configs ); launcherGroup.sync(); removeLaunchConfigurationInternal( l ); } void RunController::removeLaunchConfigurationInternal(LaunchConfiguration *l) { foreach( QAction* a, d->currentTargetAction->actions() ) { if( static_cast( a->data().value() ) == l ) { bool wasSelected = a->isChecked(); d->currentTargetAction->removeAction( a ); if( wasSelected && !d->currentTargetAction->actions().isEmpty() ) { d->currentTargetAction->actions().at(0)->setChecked( true ); } break; } } d->launchConfigurations.removeAll( l ); delete l; } void KDevelop::RunController::executeDefaultLaunch(const QString& runMode) { if (auto dl = defaultLaunch()) { execute(runMode, dl); } else { qCWarning(SHELL) << "no default launch!"; } } void RunController::setDefaultLaunch(ILaunchConfiguration* l) { foreach( QAction* a, d->currentTargetAction->actions() ) { if( static_cast( a->data().value() ) == l ) { a->setChecked(true); break; } } } bool launcherNameExists(const QString& name) { foreach(ILaunchConfiguration* config, Core::self()->runControllerInternal()->launchConfigurations()) { if(config->name()==name) return true; } return false; } QString makeUnique(const QString& name) { if(launcherNameExists(name)) { for(int i=2; ; i++) { QString proposed = QStringLiteral("%1 (%2)").arg(name).arg(i); if(!launcherNameExists(proposed)) { return proposed; } } } return name; } ILaunchConfiguration* RunController::createLaunchConfiguration ( LaunchConfigurationType* type, const QPair& launcher, IProject* project, const QString& name ) { KConfigGroup launchGroup; if( project ) { launchGroup = project->projectConfiguration()->group( Strings::LaunchConfigurationsGroup() ); } else { launchGroup = Core::self()->activeSession()->config()->group( Strings::LaunchConfigurationsGroup() ); } QStringList configs = launchGroup.readEntry( Strings::LaunchConfigurationsListEntry(), QStringList() ); uint num = 0; QString baseName = QStringLiteral("Launch Configuration"); while( configs.contains( QStringLiteral( "%1 %2" ).arg( baseName ).arg( num ) ) ) { num++; } QString groupName = QStringLiteral( "%1 %2" ).arg( baseName ).arg( num ); KConfigGroup launchConfigGroup = launchGroup.group( groupName ); QString cfgName = name; if( name.isEmpty() ) { cfgName = i18n("New %1 Launcher", type->name() ); cfgName = makeUnique(cfgName); } launchConfigGroup.writeEntry(LaunchConfiguration::LaunchConfigurationNameEntry(), cfgName ); launchConfigGroup.writeEntry(LaunchConfiguration::LaunchConfigurationTypeEntry(), type->id() ); launchConfigGroup.sync(); configs << groupName; launchGroup.writeEntry( Strings::LaunchConfigurationsListEntry(), configs ); launchGroup.sync(); LaunchConfiguration* l = new LaunchConfiguration( launchConfigGroup, project ); l->setLauncherForMode( launcher.first, launcher.second ); Core::self()->runControllerInternal()->addLaunchConfiguration( l ); return l; } QItemDelegate * KDevelop::RunController::delegate() const { return d->delegate; } ContextMenuExtension RunController::contextMenuExtension(Context* ctx, QWidget* parent) { d->launchAsInfo.clear(); d->contextItem = nullptr; ContextMenuExtension ext; if( ctx->type() == Context::ProjectItemContext ) { KDevelop::ProjectItemContext* prjctx = static_cast(ctx); if( prjctx->items().count() == 1 ) { ProjectBaseItem* itm = prjctx->items().at( 0 ); int i = 0; foreach( ILaunchMode* mode, d->launchModes ) { KActionMenu* menu = new KActionMenu(i18n("%1 As...", mode->name() ), parent); foreach( LaunchConfigurationType* type, launchConfigurationTypes() ) { bool hasLauncher = false; foreach( ILauncher* launcher, type->launchers() ) { if( launcher->supportedModes().contains( mode->id() ) ) { hasLauncher = true; } } if( hasLauncher && type->canLaunch(itm) ) { d->launchAsInfo[i] = qMakePair( type->id(), mode->id() ); QAction* act = new QAction(menu); act->setText( type->name() ); qCDebug(SHELL) << "Connect " << i << "for action" << act->text() << "in mode" << mode->name(); connect( act, &QAction::triggered, this, [this, i] () { d->launchAs(i); } ); menu->addAction(act); i++; } } if( menu->menu()->actions().count() > 0 ) { ext.addAction( ContextMenuExtension::RunGroup, menu); } else { delete menu; } } if( ext.actions( ContextMenuExtension::RunGroup ).count() > 0 ) { d->contextItem = itm; } } } return ext; } RunDelegate::RunDelegate( QObject* parent ) : QItemDelegate(parent), runProviderBrush( KColorScheme::View, KColorScheme::PositiveText ), errorBrush( KColorScheme::View, KColorScheme::NegativeText ) { } void RunDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const { QStyleOptionViewItem opt = option; // if( status.isValid() && status.canConvert() ) // { // IRunProvider::OutputTypes type = status.value(); // if( type == IRunProvider::RunProvider ) // { // opt.palette.setBrush( QPalette::Text, runProviderBrush.brush( option.palette ) ); // } else if( type == IRunProvider::StandardError ) // { // opt.palette.setBrush( QPalette::Text, errorBrush.brush( option.palette ) ); // } // } QItemDelegate::paint(painter, opt, index); } #include "moc_runcontroller.cpp" diff --git a/kdevplatform/shell/sessioncontroller.cpp b/kdevplatform/shell/sessioncontroller.cpp index 5aae860623..2de0a26f78 100644 --- a/kdevplatform/shell/sessioncontroller.cpp +++ b/kdevplatform/shell/sessioncontroller.cpp @@ -1,652 +1,658 @@ /* This file is part of KDevelop Copyright 2008 Andreas Pakulat Copyright 2010 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "sessioncontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "session.h" #include "core.h" #include "uicontroller.h" #include "shellextension.h" #include "sessionlock.h" #include "sessionchooserdialog.h" #include "debug.h" #include #include #include namespace KDevelop { namespace { int argc = 0; char** argv = nullptr; } void SessionController::setArguments(int _argc, char** _argv) { argc = _argc; argv = _argv; } static QStringList standardArguments() { QStringList ret; for(int a = 0; a < argc; ++a) { QString arg = QString::fromLocal8Bit(argv[a]); if(arg.startsWith(QLatin1String("-graphicssystem")) || arg.startsWith(QLatin1String("-style"))) { ret << QLatin1Char('-') + arg; if(a+1 < argc) ret << QString::fromLocal8Bit(argv[a+1]); } } return ret; } class SessionControllerPrivate : public QObject { Q_OBJECT public: explicit SessionControllerPrivate( SessionController* s ) : q(s) , activeSession(nullptr) , grp(nullptr) { } ~SessionControllerPrivate() override { } Session* findSessionForName( const QString& name ) const { foreach( Session* s, sessionActions.keys() ) { if( s->name() == name ) return s; } return nullptr; } Session* findSessionForId(const QString& idString) { QUuid id(idString); foreach( Session* s, sessionActions.keys() ) { if( s->id() == id) return s; } return nullptr; } void newSession() { qsrand(QDateTime::currentDateTimeUtc().toTime_t()); Session* session = new Session( QUuid::createUuid().toString() ); KProcess::startDetached(ShellExtension::getInstance()->executableFilePath(), QStringList() << QStringLiteral("-s") << session->id().toString() << standardArguments()); delete session; #if 0 //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); #endif } void deleteCurrentSession() { int choice = KMessageBox::warningContinueCancel(Core::self()->uiController()->activeMainWindow(), i18n("The current session and all contained settings will be deleted. The projects will stay unaffected. Do you really want to continue?")); if(choice == KMessageBox::Continue) { q->deleteSessionFromDisk(sessionLock); q->emitQuitSession(); } } void renameSession() { bool ok; auto newSessionName = QInputDialog::getText(Core::self()->uiController()->activeMainWindow(), i18n("Rename Session"), i18n("New Session Name:"), QLineEdit::Normal, q->activeSession()->name(), &ok); if (ok) { static_cast(q->activeSession())->setName(newSessionName); } q->updateXmlGuiActionList(); // resort } bool loadSessionExternally( Session* s ) { Q_ASSERT( s ); KProcess::startDetached(ShellExtension::getInstance()->executableFilePath(), QStringList() << QStringLiteral("-s") << s->id().toString() << standardArguments()); return true; } TryLockSessionResult activateSession( Session* s ) { Q_ASSERT( s ); activeSession = s; TryLockSessionResult result = SessionController::tryLockSession( s->id().toString()); if( !result.lock ) { activeSession = nullptr; return result; } Q_ASSERT(s->id().toString() == result.lock->id()); sessionLock = result.lock; KConfigGroup grp = KSharedConfig::openConfig()->group( SessionController::cfgSessionGroup() ); grp.writeEntry( SessionController::cfgActiveSessionEntry(), s->id().toString() ); grp.sync(); if (Core::self()->setupFlags() & Core::NoUi) return result; QHash::iterator it = sessionActions.find(s); Q_ASSERT( it != sessionActions.end() ); (*it)->setCheckable(true); (*it)->setChecked(true); for(it = sessionActions.begin(); it != sessionActions.end(); ++it) { if(it.key() != s) (*it)->setCheckable(false); } return result; } void loadSessionFromAction(QAction* action) { auto session = action->data().value(); loadSessionExternally(session); } void addSession( Session* s ) { if (Core::self()->setupFlags() & Core::NoUi) { sessionActions[s] = nullptr; return; } QAction* a = new QAction( grp ); a->setText( s->description() ); a->setCheckable( false ); a->setData(QVariant::fromValue(s)); sessionActions[s] = a; q->actionCollection()->addAction(QLatin1String("session_") + s->id().toString(), a); connect( s, &Session::sessionUpdated, this, &SessionControllerPrivate::sessionUpdated ); sessionUpdated( s ); } SessionController* q; QHash sessionActions; ISession* activeSession; QActionGroup* grp; ISessionLock::Ptr sessionLock; static QString sessionBaseDirectory() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + qApp->applicationName() + QLatin1String("/sessions/"); } QString ownSessionDirectory() const { Q_ASSERT(activeSession); return q->sessionDirectory( activeSession->id().toString() ); } private Q_SLOTS: void sessionUpdated( KDevelop::ISession* s ) { sessionActions[static_cast( s )]->setText( KStringHandler::rsqueeze(s->description()) ); } }; SessionController::SessionController( QObject *parent ) : QObject( parent ), d(new SessionControllerPrivate(this)) { setObjectName(QStringLiteral("SessionController")); setComponentName(QStringLiteral("kdevsession"), i18n("Session Manager")); setXMLFile(QStringLiteral("kdevsessionui.rc")); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/SessionController"), this, QDBusConnection::ExportScriptableSlots ); if (Core::self()->setupFlags() & Core::NoUi) return; QAction* action = actionCollection()->addAction(QStringLiteral("new_session")); connect(action, &QAction::triggered, this, [&] { d->newSession(); }); action->setText( i18nc("@action:inmenu", "Start New Session") ); action->setToolTip( i18nc("@info:tooltip", "Start a new KDevelop instance with an empty session") ); action->setIcon(QIcon::fromTheme(QStringLiteral("window-new"))); action = actionCollection()->addAction(QStringLiteral("rename_session")); connect(action, &QAction::triggered, this, [&] { d->renameSession(); }); action->setText( i18n("Rename Current Session...") ); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); action = actionCollection()->addAction(QStringLiteral("delete_session")); connect(action, &QAction::triggered, this, [&] { d->deleteCurrentSession(); }); action->setText( i18n("Delete Current Session...") ); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); action = actionCollection()->addAction( QStringLiteral("quit"), this, SIGNAL(quitSession()) ); action->setText( i18n("Quit") ); action->setMenuRole( QAction::NoRole ); // OSX: prevent QT from hiding this due to conflict with 'Quit KDevelop...' actionCollection()->setDefaultShortcut( action, Qt::CTRL | Qt::Key_Q ); action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); d->grp = new QActionGroup( this ); connect( d->grp, &QActionGroup::triggered, this, [&] (QAction* a) { d->loadSessionFromAction(a); } ); } SessionController::~SessionController() = default; void SessionController::startNewSession() { d->newSession(); } void SessionController::cleanup() { if (d->activeSession) { Q_ASSERT(d->activeSession->id().toString() == d->sessionLock->id()); if (d->activeSession->isTemporary()) { deleteSessionFromDisk(d->sessionLock); } d->activeSession = nullptr; } d->sessionLock.clear(); qDeleteAll(d->sessionActions); d->sessionActions.clear(); } void SessionController::initialize( const QString& session ) { QDir sessiondir( SessionControllerPrivate::sessionBaseDirectory() ); foreach( const QString& s, sessiondir.entryList( QDir::AllDirs | QDir::NoDotAndDotDot ) ) { QUuid id( s ); if( id.isNull() ) continue; // Only create sessions for directories that represent proper uuid's Session* ses = new Session( id.toString(), this ); //Delete sessions that have no name and are empty if( ses->containedProjects().isEmpty() && ses->name().isEmpty() && (session.isEmpty() || (ses->id().toString() != session && ses->name() != session)) ) { TryLockSessionResult result = tryLockSession(s); if (result.lock) { deleteSessionFromDisk(result.lock); } delete ses; } else { d->addSession( ses ); } } loadDefaultSession( session ); updateXmlGuiActionList(); } ISession* SessionController::activeSession() const { return d->activeSession; } ISessionLock::Ptr SessionController::activeSessionLock() const { return d->sessionLock; } void SessionController::loadSession( const QString& nameOrId ) { d->loadSessionExternally( session( nameOrId ) ); } QList SessionController::sessionNames() const { QStringList l; - foreach( const Session* s, d->sessionActions.keys() ) - { + const auto sessions = d->sessionActions.keys(); + l.reserve(sessions.size()); + for(const auto* s : sessions) { l << s->name(); } return l; } QList< const KDevelop::Session* > SessionController::sessions() const { QList< const KDevelop::Session* > ret; - foreach( const Session* s, d->sessionActions.keys() ) - { + const auto sessions = d->sessionActions.keys(); + ret.reserve(sessions.size()); + // turn to const pointers + for (const auto* s : sessions) { ret << s; } return ret; } Session* SessionController::createSession( const QString& name ) { Session* s; if(name.startsWith(QLatin1Char('{'))) { s = new Session( QUuid(name).toString(), this ); }else{ qsrand(QDateTime::currentDateTimeUtc().toTime_t()); s = new Session( QUuid::createUuid().toString(), this ); s->setName( name ); } d->addSession( s ); updateXmlGuiActionList(); return s; } void SessionController::deleteSession( const ISessionLock::Ptr& lock ) { Session* s = session(lock->id()); QHash::iterator it = d->sessionActions.find(s); Q_ASSERT( it != d->sessionActions.end() ); unplugActionList( QStringLiteral("available_sessions") ); actionCollection()->removeAction(*it); if (d->grp) { // happens in unit tests d->grp->removeAction(*it); plugActionList( QStringLiteral("available_sessions"), d->grp->actions() ); } if (s == d->activeSession) { d->activeSession = nullptr; } deleteSessionFromDisk(lock); emit sessionDeleted( s->id().toString() ); d->sessionActions.remove(s); delete s; } void SessionController::deleteSessionFromDisk( const ISessionLock::Ptr& lock ) { qCDebug(SHELL) << "Deleting session:" << lock->id(); static_cast(lock.data())->removeFromDisk(); ItemRepositoryRegistry::deleteRepositoryFromDisk(DUChain::repositoryPathForSession(lock)); } void SessionController::loadDefaultSession( const QString& session ) { QString load = session; if (load.isEmpty()) { KConfigGroup grp = KSharedConfig::openConfig()->group( cfgSessionGroup() ); load = grp.readEntry( cfgActiveSessionEntry(), "default" ); } // Iteratively try to load the session, asking user what to do in case of failure // If showForceOpenDialog() returns empty string, stop trying do { Session* s = this->session(load); if( !s ) { s = createSession( load ); } TryLockSessionResult result = d->activateSession( s ); if( result.lock ) { Q_ASSERT(d->activeSession == s); Q_ASSERT(d->sessionLock = result.lock); break; } load = handleLockedSession( s->name(), s->id().toString(), result.runInfo ); } while( !load.isEmpty() ); } Session* SessionController::session( const QString& nameOrId ) const { Session* ret = d->findSessionForName( nameOrId ); if(ret) return ret; return d->findSessionForId( nameOrId ); } QString SessionController::cloneSession( const QString& nameOrid ) { Session* origSession = session( nameOrid ); qsrand(QDateTime::currentDateTimeUtc().toTime_t()); QUuid id = QUuid::createUuid(); auto copyJob = KIO::copy(QUrl::fromLocalFile(sessionDirectory(origSession->id().toString())), QUrl::fromLocalFile(sessionDirectory( id.toString()))); KJobWidgets::setWindow(copyJob, Core::self()->uiController()->activeMainWindow()); copyJob->exec(); Session* newSession = new Session( id.toString() ); newSession->setName( i18n( "Copy of %1", origSession->name() ) ); d->addSession(newSession); updateXmlGuiActionList(); return newSession->name(); } void SessionController::updateXmlGuiActionList() { unplugActionList( QStringLiteral("available_sessions") ); if (d->grp) { auto actions = d->grp->actions(); std::sort(actions.begin(), actions.end(), [](const QAction* lhs, const QAction* rhs) { auto s1 = lhs->data().value(); auto s2 = rhs->data().value(); return QString::localeAwareCompare(s1->description(), s2->description()) < 0; }); plugActionList(QStringLiteral("available_sessions"), actions); } } QString SessionController::cfgSessionGroup() { return QStringLiteral("Sessions"); } QString SessionController::cfgActiveSessionEntry() { return QStringLiteral("Active Session ID"); } SessionInfos SessionController::availableSessionInfos() { SessionInfos sessionInfos; - foreach( const QString& sessionId, QDir( SessionControllerPrivate::sessionBaseDirectory() ).entryList( QDir::AllDirs ) ) { + const auto sessionDirs = QDir(SessionControllerPrivate::sessionBaseDirectory()).entryList(QDir::AllDirs); + sessionInfos.reserve(sessionDirs.size()); + for (const QString& sessionId : sessionDirs) { if( !QUuid( sessionId ).isNull() ) { sessionInfos << Session::parse( sessionId ); } } + sessionInfos.squeeze(); return sessionInfos; } QString SessionController::sessionDirectory(const QString& sessionId) { return SessionControllerPrivate::sessionBaseDirectory() + sessionId; } TryLockSessionResult SessionController::tryLockSession(const QString& id, bool doLocking) { return SessionLock::tryLockSession(id, doLocking); } bool SessionController::isSessionRunning(const QString& id) { return sessionRunInfo(id).isRunning; } SessionRunInfo SessionController::sessionRunInfo(const QString& id) { return SessionLock::tryLockSession(id, false).runInfo; } QString SessionController::showSessionChooserDialog(const QString& headerText, bool onlyRunning) { ///FIXME: move this code into sessiondialog.cpp QListView* view = new QListView; QLineEdit* filter = new QLineEdit; filter->setClearButtonEnabled( true ); filter->setPlaceholderText(i18n("Search")); QStandardItemModel* model = new QStandardItemModel(view); QSortFilterProxyModel *proxy = new QSortFilterProxyModel(model); proxy->setSourceModel(model); proxy->setFilterKeyColumn( 1 ); proxy->setFilterCaseSensitivity( Qt::CaseInsensitive ); connect(filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString); SessionChooserDialog dialog(view, proxy, filter); view->setEditTriggers(QAbstractItemView::NoEditTriggers); QVBoxLayout layout(dialog.mainWidget()); if(!headerText.isEmpty()) { QLabel* heading = new QLabel(headerText); QFont font = heading->font(); font.setBold(true); heading->setFont(font); layout.addWidget(heading); } model->setColumnCount(3); model->setHeaderData(0, Qt::Horizontal,i18n("Identity")); model->setHeaderData(1, Qt::Horizontal, i18n("Contents")); model->setHeaderData(2, Qt::Horizontal,i18n("State")); view->setModel(proxy); view->setModelColumn(1); QHBoxLayout* filterLayout = new QHBoxLayout(); filterLayout->addWidget(new QLabel(i18n("Filter:"))); filterLayout->addWidget(filter); layout.addLayout(filterLayout); layout.addWidget(view); filter->setFocus(); int row = 0; QString defaultSession = KSharedConfig::openConfig()->group( cfgSessionGroup() ).readEntry( cfgActiveSessionEntry(), "default" ); foreach(const KDevelop::SessionInfo& si, KDevelop::SessionController::availableSessionInfos()) { if ( si.name.isEmpty() && si.projects.isEmpty() ) { continue; } bool running = KDevelop::SessionController::isSessionRunning(si.uuid.toString()); if(onlyRunning && !running) continue; model->setItem(row, 0, new QStandardItem(si.uuid.toString())); model->setItem(row, 1, new QStandardItem(si.description)); model->setItem(row, 2, new QStandardItem); ++row; } model->sort(1); if(!onlyRunning) { model->setItem(row, 0, new QStandardItem); model->setItem(row, 1, new QStandardItem(QIcon::fromTheme(QStringLiteral("window-new")), i18n("Create New Session"))); } dialog.updateState(); dialog.mainWidget()->layout()->setContentsMargins(0,0,0,0); const QModelIndex defaultSessionIndex = model->match(model->index(0, 0), Qt::DisplayRole, defaultSession, 1, Qt::MatchExactly).value(0); view->selectionModel()->setCurrentIndex(proxy->mapFromSource(defaultSessionIndex), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); view->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); ///@todo We need a way to get a proper size-hint from the view, but unfortunately, that only seems possible after the view was shown. dialog.resize(QSize(900, 600)); if(dialog.exec() != QDialog::Accepted) // krazy:exclude=crashy { return QString(); } QModelIndex selected = view->selectionModel()->currentIndex(); if (!selected.isValid()) return QString(); const QString selectedSessionId = selected.sibling(selected.row(), 0).data().toString(); if (selectedSessionId.isEmpty()) { // "Create New Session" item selected, return a fresh UUID qsrand(QDateTime::currentDateTimeUtc().toTime_t()); return QUuid::createUuid().toString(); } return selectedSessionId; } QString SessionController::handleLockedSession( const QString& sessionName, const QString& sessionId, const SessionRunInfo& runInfo ) { return SessionLock::handleLockedSession(sessionName, sessionId, runInfo); } QString SessionController::sessionDir() { if( !activeSession() ) return QString(); return d->ownSessionDirectory(); } QString SessionController::sessionName() { if(!activeSession()) return QString(); return activeSession()->description(); } } #include "sessioncontroller.moc" #include "moc_sessioncontroller.cpp" diff --git a/kdevplatform/shell/settings/environmentprofilemodel.cpp b/kdevplatform/shell/settings/environmentprofilemodel.cpp index cc4c0944cd..6cb200e163 100644 --- a/kdevplatform/shell/settings/environmentprofilemodel.cpp +++ b/kdevplatform/shell/settings/environmentprofilemodel.cpp @@ -1,237 +1,238 @@ /* This file is part of KDevelop Copyright 2007 Andreas Pakulat Copyright 2017 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "environmentprofilemodel.h" #include "environmentprofilelistmodel.h" #include #include #include using namespace KDevelop; EnvironmentProfileModel::EnvironmentProfileModel(EnvironmentProfileListModel* profileListModel, QObject* parent) : QAbstractTableModel(parent) , m_profileListModel(profileListModel) { connect(m_profileListModel, &EnvironmentProfileListModel::profileAboutToBeRemoved, this, &EnvironmentProfileModel::onProfileAboutToBeRemoved); } int EnvironmentProfileModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return m_varsByIndex.count(); } int EnvironmentProfileModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return 2; } Qt::ItemFlags EnvironmentProfileModel::flags(const QModelIndex& index) const { if (!index.isValid()) { return Qt::NoItemFlags; } return (Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled); } QVariant EnvironmentProfileModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount() || index.column() < 0 || index.column() >= columnCount(QModelIndex()) || m_currentProfileName.isEmpty()) { return {}; } const auto variable = m_varsByIndex[index.row()]; if (role == VariableRole) { return variable; } if (role == ValueRole) { const auto& variables = m_profileListModel->variables(m_currentProfileName); return variables.value(variable); } if (role == Qt::DisplayRole || role == Qt::EditRole) { if (index.column() == VariableColumn) { return variable; } const auto& variables = m_profileListModel->variables(m_currentProfileName); return variables.value(variable); } return {}; } QVariant EnvironmentProfileModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section < 0 || section >= columnCount(QModelIndex()) || orientation != Qt::Horizontal || role != Qt::DisplayRole) { return {}; } if (section == VariableColumn) { return i18n("Variable"); } return i18n("Value"); } bool EnvironmentProfileModel::setData(const QModelIndex& index, const QVariant& data, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount() || index.column() < 0 || index.column() >= columnCount(QModelIndex()) || m_currentProfileName.isEmpty()) { return false; } if (role == Qt::EditRole) { auto& variables = m_profileListModel->variables(m_currentProfileName); if (index.column() == VariableColumn) { const QString oldVariable = m_varsByIndex[index.row()]; const QString value = variables.take(oldVariable); const QString newVariable = data.toString(); variables.insert(newVariable, value); m_varsByIndex[index.row()] = newVariable; } else { variables.insert(m_varsByIndex[index.row()], data.toString()); } emit dataChanged(index, index); } return true; } void EnvironmentProfileModel::addVariable(const QString& variableName, const QString& value) { if (m_currentProfileName.isEmpty()) { return; } const int pos = m_varsByIndex.indexOf(variableName); if (pos != -1) { return; // No duplicates, first value } auto& variables = m_profileListModel->variables(m_currentProfileName); const int insertPos = rowCount(); beginInsertRows(QModelIndex(), insertPos, insertPos); m_varsByIndex << variableName; variables.insert(variableName, value); endInsertRows(); } void EnvironmentProfileModel::removeVariables(const QStringList& variableNames) { for (const auto& variableName : variableNames) { removeVariable(variableName); } } void EnvironmentProfileModel::removeVariable(const QString& variableName) { if (m_currentProfileName.isEmpty()) { return; } const int pos = m_varsByIndex.indexOf(variableName); if (pos == -1) { return; } auto& variables = m_profileListModel->variables(m_currentProfileName); beginRemoveRows(QModelIndex(), pos, pos); m_varsByIndex.removeAt(pos); variables.remove(variableName); endRemoveRows(); } void EnvironmentProfileModel::setCurrentProfile(const QString& profileName) { if (profileName == m_currentProfileName) { return; } beginResetModel(); m_currentProfileName = profileName; m_varsByIndex.clear(); if (!m_currentProfileName.isEmpty()) { const auto& variables = m_profileListModel->variables(m_currentProfileName); + m_varsByIndex.reserve(variables.size()); const auto endIt = variables.constEnd(); for (auto it = variables.constBegin(); it != endIt; ++it) { m_varsByIndex << it.key(); } } endResetModel(); } void EnvironmentProfileModel::setVariablesFromString(const QString& plainText) { if (m_currentProfileName.isEmpty()) { return; } beginResetModel(); auto& variables = m_profileListModel->variables(m_currentProfileName); variables.clear(); m_varsByIndex.clear(); const auto lines = plainText.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (const auto& line : lines) { const int pos = line.indexOf(QLatin1Char('=')); // has a = and at least 1 char if (pos < 0) { continue; } const QString variableName = line.left(pos).trimmed(); if (variableName.isEmpty()) { continue; } const QString value = line.mid(pos+1).trimmed(); m_varsByIndex << variableName; variables.insert(variableName, value); } endResetModel(); } void EnvironmentProfileModel::onProfileAboutToBeRemoved(const QString& profileName) { if (m_currentProfileName == profileName) { setCurrentProfile(QString()); } } diff --git a/kdevplatform/shell/settings/environmentwidget.cpp b/kdevplatform/shell/settings/environmentwidget.cpp index 235d5eca08..ba2a0ee97b 100644 --- a/kdevplatform/shell/settings/environmentwidget.cpp +++ b/kdevplatform/shell/settings/environmentwidget.cpp @@ -1,341 +1,341 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Dukju Ahn Copyright 2008 Andreas Pakuat Copyright 2017 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "environmentwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "environmentprofilelistmodel.h" #include "environmentprofilemodel.h" #include "placeholderitemproxymodel.h" #include "debug.h" using namespace KDevelop; class ProfileNameValidator : public QValidator { Q_OBJECT public: explicit ProfileNameValidator(EnvironmentProfileListModel* environmentProfileListModel, QObject* parent = nullptr); QValidator::State validate(QString& input, int& pos) const override; private: const EnvironmentProfileListModel* const m_environmentProfileListModel; }; ProfileNameValidator::ProfileNameValidator(EnvironmentProfileListModel* environmentProfileListModel, QObject* parent) : QValidator(parent) , m_environmentProfileListModel(environmentProfileListModel) { } QValidator::State ProfileNameValidator::validate(QString& input, int& pos) const { Q_UNUSED(pos); if (input.isEmpty()) { return QValidator::Intermediate; } if (m_environmentProfileListModel->hasProfile(input)) { return QValidator::Intermediate; } return QValidator::Acceptable; } EnvironmentWidget::EnvironmentWidget( QWidget *parent ) : QWidget(parent) , m_environmentProfileListModel(new EnvironmentProfileListModel(this)) , m_environmentProfileModel(new EnvironmentProfileModel(m_environmentProfileListModel, this)) , m_proxyModel(new QSortFilterProxyModel(this)) { // setup ui ui.setupUi( this ); ui.profileSelect->setModel(m_environmentProfileListModel); m_proxyModel->setSourceModel(m_environmentProfileModel); PlaceholderItemProxyModel* topProxyModel = new PlaceholderItemProxyModel(this); topProxyModel->setSourceModel(m_proxyModel); topProxyModel->setColumnHint(0, i18n("Enter variable...")); connect(topProxyModel, &PlaceholderItemProxyModel::dataInserted, this, &EnvironmentWidget::onVariableInserted); ui.variableTable->setModel( topProxyModel ); ui.variableTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::ResizeToContents ); ui.variableTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); ui.removeVariableButton->setShortcut(Qt::Key_Delete); connect(ui.removeVariableButton, &QPushButton::clicked, this, &EnvironmentWidget::removeSelectedVariables); connect(ui.batchModeEditButton, &QPushButton::clicked, this, &EnvironmentWidget::batchModeEditButtonClicked); connect(ui.cloneProfileButton, &QPushButton::clicked, this, &EnvironmentWidget::cloneSelectedProfile); connect(ui.addProfileButton, &QPushButton::clicked, this, &EnvironmentWidget::addProfile); connect(ui.removeProfileButton, &QPushButton::clicked, this, &EnvironmentWidget::removeSelectedProfile); connect(ui.setAsDefaultProfileButton, &QPushButton::clicked, this, &EnvironmentWidget::setSelectedProfileAsDefault); connect(ui.profileSelect, static_cast(&KComboBox::currentIndexChanged), this, &EnvironmentWidget::onSelectedProfileChanged); connect(m_environmentProfileListModel, &EnvironmentProfileListModel::defaultProfileChanged, this, &EnvironmentWidget::onDefaultProfileChanged); connect(m_environmentProfileListModel, &EnvironmentProfileListModel::rowsInserted, this, &EnvironmentWidget::changed); connect(m_environmentProfileListModel, &EnvironmentProfileListModel::rowsRemoved, this, &EnvironmentWidget::changed); connect(m_environmentProfileListModel, &EnvironmentProfileListModel::defaultProfileChanged, this, &EnvironmentWidget::changed); connect(ui.variableTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EnvironmentWidget::updateDeleteVariableButton); connect(m_environmentProfileModel, &EnvironmentProfileModel::rowsInserted, this, &EnvironmentWidget::updateDeleteVariableButton); connect(m_environmentProfileModel, &EnvironmentProfileModel::rowsRemoved, this, &EnvironmentWidget::updateDeleteVariableButton); connect(m_environmentProfileModel, &EnvironmentProfileModel::modelReset, this, &EnvironmentWidget::updateDeleteVariableButton); connect(m_environmentProfileModel, &EnvironmentProfileModel::dataChanged, this, &EnvironmentWidget::changed); connect(m_environmentProfileModel, &EnvironmentProfileModel::rowsInserted, this, &EnvironmentWidget::changed); connect(m_environmentProfileModel, &EnvironmentProfileModel::rowsRemoved, this, &EnvironmentWidget::changed); } void EnvironmentWidget::selectProfile(const QString& profileName) { const int profileIndex = m_environmentProfileListModel->profileIndex(profileName); if (profileIndex < 0) { return; } ui.profileSelect->setCurrentIndex(profileIndex); } void EnvironmentWidget::updateDeleteVariableButton() { const auto selectedRows = ui.variableTable->selectionModel()->selectedRows(); ui.removeVariableButton->setEnabled(!selectedRows.isEmpty()); } void EnvironmentWidget::setSelectedProfileAsDefault() { const int selectedIndex = ui.profileSelect->currentIndex(); m_environmentProfileListModel->setDefaultProfile(selectedIndex); } void EnvironmentWidget::loadSettings( KConfig* config ) { qCDebug(SHELL) << "Loading profiles from config"; m_environmentProfileListModel->loadFromConfig(config); const int defaultProfileIndex = m_environmentProfileListModel->defaultProfileIndex(); ui.profileSelect->setCurrentIndex(defaultProfileIndex); } void EnvironmentWidget::saveSettings( KConfig* config ) { m_environmentProfileListModel->saveToConfig(config); } void EnvironmentWidget::defaults( KConfig* config ) { loadSettings( config ); } QString EnvironmentWidget::askNewProfileName(const QString& defaultName) { ScopedDialog dialog(this); dialog->setWindowTitle(i18n("Enter Name of New Environment Profile")); QVBoxLayout *layout = new QVBoxLayout(dialog); auto editLayout = new QHBoxLayout; auto label = new QLabel(i18n("Name:")); editLayout->addWidget(label); auto edit = new QLineEdit; editLayout->addWidget(edit); layout->addLayout(editLayout); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(false); okButton->setDefault(true); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); layout->addWidget(buttonBox); auto validator = new ProfileNameValidator(m_environmentProfileListModel, dialog); connect(edit, &QLineEdit::textChanged, validator, [validator, okButton](const QString& text) { int pos; QString t(text); const bool isValidProfileName = (validator->validate(t, pos) == QValidator::Acceptable); okButton->setEnabled(isValidProfileName); }); edit->setText(defaultName); edit->selectAll(); if (dialog->exec() != QDialog::Accepted) { return {}; } return edit->text(); } void EnvironmentWidget::removeSelectedVariables() { const auto selectedRows = ui.variableTable->selectionModel()->selectedRows(); if (selectedRows.isEmpty()) { return; } QStringList variables; variables.reserve(selectedRows.size()); for (const auto& idx : selectedRows) { const QString variable = idx.data(EnvironmentProfileModel::VariableRole).toString(); variables << variable; } m_environmentProfileModel->removeVariables(variables); } void EnvironmentWidget::onVariableInserted(int column, const QVariant& value) { Q_UNUSED(column); m_environmentProfileModel->addVariable(value.toString(), QString()); } void EnvironmentWidget::batchModeEditButtonClicked() { ScopedDialog dialog(this); dialog->setWindowTitle( i18n( "Batch Edit Mode" ) ); QVBoxLayout *layout = new QVBoxLayout(dialog); auto edit = new QPlainTextEdit; edit->setPlaceholderText(QStringLiteral("VARIABLE1=VALUE1\nVARIABLE2=VALUE2")); QString text; for (int i = 0; i < m_proxyModel->rowCount(); ++i) { const auto variable = m_proxyModel->index(i, EnvironmentProfileModel::VariableColumn).data().toString(); const auto value = m_proxyModel->index(i, EnvironmentProfileModel::ValueColumn).data().toString(); - text.append(QStringLiteral("%1=%2\n").arg(variable, value)); + text.append(variable + QLatin1Char('=') + value + QLatin1Char('\n')); } edit->setPlainText(text); layout->addWidget( edit ); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); layout->addWidget(buttonBox); dialog->resize(600, 400); if ( dialog->exec() != QDialog::Accepted ) { return; } m_environmentProfileModel->setVariablesFromString(edit->toPlainText()); } void EnvironmentWidget::addProfile() { const auto profileName = askNewProfileName(QString()); if (profileName.isEmpty()) { return; } const int profileIndex = m_environmentProfileListModel->addProfile(profileName); ui.profileSelect->setCurrentIndex(profileIndex); ui.variableTable->setFocus(Qt::OtherFocusReason); } void EnvironmentWidget::cloneSelectedProfile() { const int currentIndex = ui.profileSelect->currentIndex(); const auto currentProfileName = m_environmentProfileListModel->profileName(currentIndex); // pass original name as starting name, as the user might want to enter a variant of it const auto profileName = askNewProfileName(currentProfileName); if (profileName.isEmpty()) { return; } const int profileIndex = m_environmentProfileListModel->cloneProfile(profileName, currentProfileName); ui.profileSelect->setCurrentIndex(profileIndex); ui.variableTable->setFocus(Qt::OtherFocusReason); } void EnvironmentWidget::removeSelectedProfile() { if (ui.profileSelect->count() <= 1) { return; } const int selectedProfileIndex = ui.profileSelect->currentIndex(); m_environmentProfileListModel->removeProfile(selectedProfileIndex); const int defaultProfileIndex = m_environmentProfileListModel->defaultProfileIndex(); ui.profileSelect->setCurrentIndex(defaultProfileIndex); } void EnvironmentWidget::onDefaultProfileChanged(int defaultProfileIndex) { const int selectedProfileIndex = ui.profileSelect->currentIndex(); const bool isDefaultProfile = (defaultProfileIndex == selectedProfileIndex); ui.removeProfileButton->setEnabled(ui.profileSelect->count() > 1 && !isDefaultProfile); ui.setAsDefaultProfileButton->setEnabled(!isDefaultProfile); } void EnvironmentWidget::onSelectedProfileChanged(int selectedProfileIndex) { const auto selectedProfileName = m_environmentProfileListModel->profileName(selectedProfileIndex); m_environmentProfileModel->setCurrentProfile(selectedProfileName); const bool isDefaultProfile = (m_environmentProfileListModel->defaultProfileIndex() == selectedProfileIndex); ui.removeProfileButton->setEnabled(ui.profileSelect->count() > 1 && !isDefaultProfile); ui.setAsDefaultProfileButton->setEnabled(!isDefaultProfile); } #include "environmentwidget.moc" diff --git a/kdevplatform/shell/statusbar.cpp b/kdevplatform/shell/statusbar.cpp index 7264eb1015..ad68ca1aa3 100644 --- a/kdevplatform/shell/statusbar.cpp +++ b/kdevplatform/shell/statusbar.cpp @@ -1,256 +1,255 @@ /* This file is part of the KDE project Copyright 2007 Hamish Rodda This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "statusbar.h" #include "progresswidget/statusbarprogresswidget.h" #include "progresswidget/progressmanager.h" #include "progresswidget/progressdialog.h" #include #include #include #include #include #include #include #include "plugincontroller.h" #include "core.h" namespace KDevelop { StatusBar::StatusBar(QWidget* parent) : QStatusBar(parent) , m_timer(new QTimer(this)) , m_currentView(nullptr) { #ifdef Q_OS_MAC /* At time of writing this is only required for OSX and only has effect on OSX. ifdef for robustness to future platform dependent theme/widget changes https://phabricator.kde.org/D656 */ setStyleSheet("QStatusBar{background:transparent;}"); #endif m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &StatusBar::slotTimeout); connect(Core::self()->pluginController(), &IPluginController::pluginLoaded, this, &StatusBar::pluginLoaded); QList plugins = Core::self()->pluginControllerInternal()->allPluginsForExtension(QStringLiteral("IStatus")); foreach (IPlugin* plugin, plugins) registerStatus(plugin); registerStatus(Core::self()->languageController()->backgroundParser()); m_progressController = Core::self()->progressController(); m_progressDialog = new ProgressDialog(this, parent); // construct this first, then progressWidget m_progressDialog->setVisible(false); m_progressWidget = new StatusbarProgressWidget(m_progressDialog, this); addPermanentWidget(m_progressWidget, 0); } void StatusBar::removeError(QWidget* w) { removeWidget(w); w->deleteLater(); } void StatusBar::viewChanged(Sublime::View* view) { if (m_currentView) m_currentView->disconnect(this); m_currentView = view; if (view) { connect(view, &Sublime::View::statusChanged, this, &StatusBar::viewStatusChanged); QStatusBar::showMessage(view->viewStatus(), 0); } } void StatusBar::viewStatusChanged(Sublime::View* view) { QStatusBar::showMessage(view->viewStatus(), 0); } void StatusBar::pluginLoaded(IPlugin* plugin) { if (qobject_cast(plugin)) registerStatus(plugin); } void StatusBar::registerStatus(QObject* status) { Q_ASSERT(qobject_cast(status)); // can't convert this to new signal slot syntax, IStatus is not a QObject connect(status, SIGNAL(clearMessage(KDevelop::IStatus*)), SLOT(clearMessage(KDevelop::IStatus*)), Qt::QueuedConnection); connect(status, SIGNAL(showMessage(KDevelop::IStatus*,QString,int)), SLOT(showMessage(KDevelop::IStatus*,QString,int)), Qt::QueuedConnection); connect(status, SIGNAL(hideProgress(KDevelop::IStatus*)), SLOT(hideProgress(KDevelop::IStatus*)), Qt::QueuedConnection); connect(status, SIGNAL(showProgress(KDevelop::IStatus*,int,int,int)), SLOT(showProgress(KDevelop::IStatus*,int,int,int)), Qt::QueuedConnection); connect(status, SIGNAL(showErrorMessage(QString,int)), SLOT(showErrorMessage(QString,int)), Qt::QueuedConnection); } QWidget* errorMessage(QWidget* parent, const QString& text) { KSqueezedTextLabel* label = new KSqueezedTextLabel(parent); KStatefulBrush red(KColorScheme::Window, KColorScheme::NegativeText); QPalette pal = label->palette(); pal.setBrush(QPalette::WindowText, red.brush(label)); label->setPalette(pal); label->setAlignment(Qt::AlignRight); label->setText(text); label->setToolTip(text); return label; } QTimer* StatusBar::errorTimeout(QWidget* error, int timeout) { QTimer* timer = new QTimer(error); timer->setSingleShot(true); timer->setInterval(1000*timeout); connect(timer, &QTimer::timeout, this, [this, error](){ removeError(error); }); return timer; } void StatusBar::showErrorMessage(const QString& message, int timeout) { QWidget* error = errorMessage(this, message); QTimer* timer = errorTimeout(error, timeout); addWidget(error); timer->start(); // triggers removeError() } void StatusBar::slotTimeout() { QMutableMapIterator it = m_messages; while (it.hasNext()) { it.next(); if (it.value().timeout) { it.value().timeout -= m_timer->interval(); if (it.value().timeout == 0) it.remove(); } } updateMessage(); } void StatusBar::updateMessage() { if (m_timer->isActive()) { m_timer->stop(); m_timer->setInterval(m_time.elapsed()); slotTimeout(); } QString ret; int timeout = 0; + QStringList messages; + messages.reserve(m_messages.size()); foreach (const Message& m, m_messages) { - if (!ret.isEmpty()) - ret += QLatin1String("; "); - - ret += m.text; + messages.append(m.text); if (timeout) timeout = qMin(timeout, m.timeout); else timeout = m.timeout; } - if (!ret.isEmpty()) - QStatusBar::showMessage(ret); + if (!messages.isEmpty()) + QStatusBar::showMessage(messages.join(QLatin1String("; "))); else QStatusBar::clearMessage(); if (timeout) { m_time.start(); m_timer->start(timeout); } } void StatusBar::clearMessage( IStatus* status ) { if (m_messages.contains(status)) { m_messages.remove(status); updateMessage(); } } void StatusBar::showMessage( IStatus* status, const QString & message, int timeout) { if ( m_progressItems.contains(status) ) { ProgressItem* i = m_progressItems[status]; i->setStatus(message); } else { Message m; m.text = message; m.timeout = timeout; m_messages.insert(status, m); updateMessage(); } } void StatusBar::hideProgress( IStatus* status ) { if (m_progressItems.contains(status)) { m_progressItems[status]->setComplete(); m_progressItems.remove(status); } } void StatusBar::showProgress( IStatus* status, int minimum, int maximum, int value) { if (!m_progressItems.contains(status)) { bool canBeCanceled = false; m_progressItems[status] = m_progressController->createProgressItem( ProgressManager::createUniqueID(), status->statusName(), QString(), canBeCanceled); } ProgressItem* i = m_progressItems[status]; m_progressWidget->raise(); m_progressDialog->raise(); if( minimum == 0 && maximum == 0 ) { i->setUsesBusyIndicator( true ); } else { i->setUsesBusyIndicator( false ); i->setProgress( 100*value/maximum ); } } } diff --git a/kdevplatform/shell/tests/test_ktexteditorpluginintegration.cpp b/kdevplatform/shell/tests/test_ktexteditorpluginintegration.cpp index cf20e3d6bb..0903b28e94 100644 --- a/kdevplatform/shell/tests/test_ktexteditorpluginintegration.cpp +++ b/kdevplatform/shell/tests/test_ktexteditorpluginintegration.cpp @@ -1,198 +1,212 @@ /* Copyright 2015 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_ktexteditorpluginintegration.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { template QPointer makeQPointer(T *ptr) { return {ptr}; } IToolViewFactory *findToolView(const QString &id) { const auto uiController = Core::self()->uiControllerInternal(); const auto map = uiController->factoryDocuments(); for (auto it = map.begin(); it != map.end(); ++it) { if (it.key()->id() == id) { return it.key(); } } return nullptr; } class TestPlugin : public KTextEditor::Plugin { Q_OBJECT public: explicit TestPlugin(QObject *parent) : Plugin(parent) { } QObject *createView(KTextEditor::MainWindow * mainWindow) override { return new QObject(mainWindow); } }; } void TestKTextEditorPluginIntegration::initTestCase() { QLoggingCategory::setFilterRules(QStringLiteral("*.debug=false\ndefault.debug=true\n")); AutoTestShell::init({QStringLiteral("katesnippetsplugin")}); TestCore::initialize(); QVERIFY(KTextEditor::Editor::instance()); } void TestKTextEditorPluginIntegration::cleanupTestCase() { auto controller = Core::self()->pluginController(); const auto id = QStringLiteral("katesnippetsplugin"); auto plugin = makeQPointer(controller->loadPlugin(id)); const auto editor = makeQPointer(KTextEditor::Editor::instance()); const auto application = makeQPointer(editor->application()); const auto window = makeQPointer(application->activeMainWindow()); TestCore::shutdown(); QVERIFY(!plugin); + // Test uncovers issue in shutdown behaviour when not triggered by last closed mainwindow, but directly: + // Core::shutdown() deletes itself via deleteLater, for which TestCore::shutdown() adds a QTest::qWait(1) + // so Core instance should be gone after the call returns. + // Core in its destructor deletes the Sublime::Controller instance. + // That one in the destructor deletes any still existing mainwindows, of which we have here in the test one. + // The KTE::MainWindow wrapper trying to outlive the KTE::View instances as needed now is only deleted with + // a deleteLater() from the mainwindow. Thus still living here. + QEXPECT_FAIL("", "Chain of deleteLater too long ATM", Continue); QVERIFY(!window); QVERIFY(!application); + + // workaround for now, remove again if issue of too long deleteLater chain above is fixed + QTest::qWait(1); + QVERIFY(!window); + + // editor lives by design until QCoreApplication terminates, then autodeletes } void TestKTextEditorPluginIntegration::testApplication() { auto app = KTextEditor::Editor::instance()->application(); QVERIFY(app); QVERIFY(app->parent()); QCOMPARE(app->parent()->metaObject()->className(), "KTextEditorIntegration::Application"); QVERIFY(app->activeMainWindow()); QCOMPARE(app->mainWindows().size(), 1); QVERIFY(app->mainWindows().contains(app->activeMainWindow())); } void TestKTextEditorPluginIntegration::testMainWindow() { auto window = KTextEditor::Editor::instance()->application()->activeMainWindow(); QVERIFY(window); QVERIFY(window->parent()); QCOMPARE(window->parent()->metaObject()->className(), "KTextEditorIntegration::MainWindow"); const auto id = QStringLiteral("kte_integration_toolview"); const auto icon = QIcon::fromTheme(QStringLiteral("kdevelop")); const auto text = QStringLiteral("some text"); QVERIFY(!findToolView(id)); auto plugin = new TestPlugin(this); auto toolView = makeQPointer(window->createToolView(plugin, id, KTextEditor::MainWindow::Bottom, icon, text)); QVERIFY(toolView); auto factory = findToolView(id); QVERIFY(factory); // we reuse the same view QWidget parent; auto kdevToolView = makeQPointer(factory->create(&parent)); QCOMPARE(kdevToolView->parentWidget(), &parent); QCOMPARE(toolView->parentWidget(), kdevToolView.data()); // the children are kept alive when the tool view gets destroyed delete kdevToolView; QVERIFY(toolView); kdevToolView = factory->create(&parent); // and we reuse the ktexteditor tool view for the new kdevelop tool view QCOMPARE(toolView->parentWidget(), kdevToolView.data()); delete toolView; delete kdevToolView; delete plugin; QVERIFY(!findToolView(id)); } void TestKTextEditorPluginIntegration::testPlugin() { auto controller = Core::self()->pluginController(); const auto id = QStringLiteral("katesnippetsplugin"); auto plugin = makeQPointer(controller->loadPlugin(id)); if (!plugin) { QSKIP("Cannot continue without katesnippetsplugin, install Kate"); } auto app = KTextEditor::Editor::instance()->application(); auto ktePlugin = makeQPointer(app->plugin(id)); QVERIFY(ktePlugin); auto view = makeQPointer(app->activeMainWindow()->pluginView(id)); QVERIFY(view); const auto rawView = view.data(); QSignalSpy spy(app->activeMainWindow(), &KTextEditor::MainWindow::pluginViewDeleted); QVERIFY(controller->unloadPlugin(id)); QVERIFY(!ktePlugin); QCOMPARE(spy.count(), 1); QCOMPARE(spy.first().count(), 2); QCOMPARE(spy.first().at(0), QVariant::fromValue(id)); QCOMPARE(spy.first().at(1), QVariant::fromValue(rawView)); QVERIFY(!view); } void TestKTextEditorPluginIntegration::testPluginUnload() { auto controller = Core::self()->pluginController(); const auto id = QStringLiteral("katesnippetsplugin"); auto plugin = makeQPointer(controller->loadPlugin(id)); if (!plugin) { QSKIP("Cannot continue without katesnippetsplugin, install Kate"); } auto app = KTextEditor::Editor::instance()->application(); auto ktePlugin = makeQPointer(app->plugin(id)); QVERIFY(ktePlugin); delete ktePlugin; // don't crash plugin->unload(); } QTEST_MAIN(TestKTextEditorPluginIntegration) #include diff --git a/kdevplatform/shell/tests/test_problemmodel.cpp b/kdevplatform/shell/tests/test_problemmodel.cpp index f0fc3f82a5..4c689637cf 100644 --- a/kdevplatform/shell/tests/test_problemmodel.cpp +++ b/kdevplatform/shell/tests/test_problemmodel.cpp @@ -1,571 +1,571 @@ /* * 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 Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testNoGrouping(); void testPathGrouping(); void testSeverityGrouping(); void testPlaceholderText(); 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);; + 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(); } void TestProblemModel::testPlaceholderText() { const QString text1 = QStringLiteral("testPlaceholderText1"); const QString text2 = QStringLiteral("testPlaceholderText2"); const QString empty; m_model->setGrouping(NoGrouping); // Test model with empty placeholder text QCOMPARE(m_model->rowCount(), 0); m_model->setPlaceholderText(empty); QCOMPARE(m_model->rowCount(), 0); m_model->setProblems(m_problems); QCOMPARE(m_model->rowCount(), 3); m_model->clearProblems(); QCOMPARE(m_model->rowCount(), 0); // Test empty model with non-empty placeholder text m_model->setPlaceholderText(text1); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkLabel(0, QModelIndex(), text1)); m_model->setPlaceholderText(text2); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkLabel(0, QModelIndex(), text2)); // Test non-empty model with non-empty placeholder text m_model->setProblems(m_problems); QCOMPARE(m_model->rowCount(), 3); m_model->clearProblems(); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkLabel(0, QModelIndex(), text2)); m_model->addProblem(m_problems[0]); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); m_model->addProblem(m_problems[1]); QCOMPARE(m_model->rowCount(), 2); QVERIFY(checkIsSame(1, QModelIndex(), m_problems[1])); m_model->setPlaceholderText(text1); QCOMPARE(m_model->rowCount(), 2); QVERIFY(checkIsSame(0, QModelIndex(), m_problems[0])); QVERIFY(checkIsSame(1, QModelIndex(), m_problems[1])); m_model->setProblems({}); QCOMPARE(m_model->rowCount(), 1); QVERIFY(checkLabel(0, QModelIndex(), text1)); } // 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/kdevplatform/shell/workingsets/workingset.cpp b/kdevplatform/shell/workingsets/workingset.cpp index 5da25c5ed8..26926461e8 100644 --- a/kdevplatform/shell/workingsets/workingset.cpp +++ b/kdevplatform/shell/workingsets/workingset.cpp @@ -1,565 +1,570 @@ /* Copyright David Nolden This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "workingset.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #define SYNC_OFTEN using namespace KDevelop; bool WorkingSet::m_loading = false; namespace { QIcon generateIcon(const WorkingSetIconParameters& params) { QImage pixmap(16, 16, QImage::Format_ARGB32); // fill the background with a transparent color pixmap.fill(QColor::fromRgba(qRgba(0, 0, 0, 0))); const uint coloredCount = params.coloredCount; // coordinates of the rectangles to draw, for 16x16 icons specifically - QList rects; - rects << QRect(1, 1, 5, 5) << QRect(1, 9, 5, 5) << QRect(9, 1, 5, 5) << QRect(9, 9, 5, 5); + QList rects{ + {1, 1, 5, 5}, + {1, 9, 5, 5}, + {9, 1, 5, 5}, + {9, 9, 5, 5}, + }; if ( params.swapDiagonal ) { rects.swap(1, 2); } QPainter painter(&pixmap); // color for non-colored squares, paint them brighter if the working set is the active one const int inact = 40; QColor darkColor = QColor::fromRgb(inact, inact, inact); // color for colored squares // this code is not fragile, you can just tune the magic formulas at random and see what looks good. // just make sure to keep it within the 0-360 / 0-255 / 0-255 space of the HSV model QColor brightColor = QColor::fromHsv(params.hue, qMin(255, 215 + (params.setId*5) % 150), 205 + (params.setId*11) % 50); // Y'UV "Y" value, the approximate "lightness" of the color // If it is above 0.6, then making the color darker a bit is okay, // if it is below 0.35, then the color should be a bit brighter. float brightY = 0.299 * brightColor.redF() + 0.587 * brightColor.greenF() + 0.114 * brightColor.blueF(); if ( brightY > 0.6 ) { if ( params.setId % 7 < 2 ) { // 2/7 chance to make the color significantly darker brightColor = brightColor.darker(120 + (params.setId*7) % 35); } else if ( params.setId % 5 == 0 ) { // 1/5 chance to make it a bit darker brightColor = brightColor.darker(110 + (params.setId*3) % 10); } } if ( brightY < 0.35 ) { // always make the color brighter to avoid very dark colors (like rgb(0, 0, 255)) brightColor = brightColor.lighter(120 + (params.setId*13) % 55); } int at = 0; foreach ( const QRect rect, rects ) { QColor currentColor; // pick the colored squares; you can get different patterns by re-ordering the "rects" list if ( (at + params.setId*7) % 4 < coloredCount ) { currentColor = brightColor; } else { currentColor = darkColor; } // draw the filling of the square painter.setPen(QColor(currentColor)); painter.setBrush(QBrush(currentColor)); painter.drawRect(rect); // draw a slight set-in shadow for the square -- it's barely recognizeable, // but it looks way better than without painter.setBrush(Qt::NoBrush); painter.setPen(QColor(0, 0, 0, 50)); painter.drawRect(rect); painter.setPen(QColor(0, 0, 0, 25)); painter.drawRect(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2); at += 1; } return QIcon(QPixmap::fromImage(pixmap)); } } WorkingSet::WorkingSet(const QString& id) : QObject() , m_id(id) , m_icon(generateIcon(WorkingSetIconParameters(id))) { } void WorkingSet::saveFromArea( Sublime::Area* a, Sublime::AreaIndex * area, KConfigGroup setGroup, KConfigGroup areaGroup ) { if (area->isSplit()) { setGroup.writeEntry("Orientation", area->orientation() == Qt::Horizontal ? "Horizontal" : "Vertical"); if (area->first()) { saveFromArea(a, area->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0")); } if (area->second()) { saveFromArea(a, area->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1")); } } else { setGroup.writeEntry("View Count", area->viewCount()); areaGroup.writeEntry("View Count", area->viewCount()); int index = 0; foreach (Sublime::View* view, area->views()) { //The working set config gets an updated list of files QString docSpec = view->document()->documentSpecifier(); //only save the documents from protocols KIO understands //otherwise we try to load kdev:// too early if (!KProtocolInfo::isKnownProtocol(QUrl(docSpec))) { continue; } setGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec); //The area specific config stores the working set documents in order along with their state areaGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec); KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(index)); view->writeSessionConfig(viewGroup); ++index; } } } bool WorkingSet::isEmpty() const { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); return !group.hasKey("Orientation") && group.readEntry("View Count", 0) == 0; } struct DisableMainWindowUpdatesFromArea { explicit DisableMainWindowUpdatesFromArea(Sublime::Area* area) : m_area(area) { if(area) { foreach(Sublime::MainWindow* window, Core::self()->uiControllerInternal()->mainWindows()) { if(window->area() == area) { if(window->updatesEnabled()) { wasUpdatesEnabled.insert(window); window->setUpdatesEnabled(false); } } } } } ~DisableMainWindowUpdatesFromArea() { if(m_area) { foreach(Sublime::MainWindow* window, wasUpdatesEnabled) { window->setUpdatesEnabled(wasUpdatesEnabled.contains(window)); } } } Sublime::Area* m_area; QSet wasUpdatesEnabled; }; void loadFileList(QStringList& ret, KConfigGroup group) { if (group.hasKey("Orientation")) { QStringList subgroups = group.groupList(); if (subgroups.contains(QStringLiteral("0"))) { { KConfigGroup subgroup(&group, "0"); loadFileList(ret, subgroup); } if (subgroups.contains(QStringLiteral("1"))) { KConfigGroup subgroup(&group, "1"); loadFileList(ret, subgroup); } } } else { int viewCount = group.readEntry("View Count", 0); + ret.reserve(ret.size() + viewCount); for (int i = 0; i < viewCount; ++i) { QString specifier = group.readEntry(QStringLiteral("View %1").arg(i), QString()); ret << specifier; } } } QStringList WorkingSet::fileList() const { QStringList ret; KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); loadFileList(ret, group); return ret; } void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex) { PushValue enableLoading(m_loading, true); /// We cannot disable the updates here, because (probably) due to a bug in Qt, /// which causes the updates to stay disabled forever after some complex operations /// on the sub-views. This could be reproduced by creating two working-sets with complex /// split-view configurations and switching between them. Re-enabling the updates doesn't help. // DisableMainWindowUpdatesFromArea updatesDisabler(area); qCDebug(SHELL) << "loading working-set" << m_id << "into area" << area; QMultiMap recycle; foreach( Sublime::View* view, area->views() ) recycle.insert( view->document()->documentSpecifier(), area->removeView(view) ); qCDebug(SHELL) << "recycling" << recycle.size() << "old views"; Q_ASSERT( area->views().empty() ); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup setGroup = setConfig.group(m_id); KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title()); loadToArea(area, areaIndex, setGroup, areaGroup, recycle); // Delete views which were not recycled qCDebug(SHELL) << "deleting " << recycle.size() << " old views"; qDeleteAll( recycle ); area->setActiveView(nullptr); //activate view in the working set /// @todo correctly select one out of multiple equal views QString activeView = areaGroup.readEntry("Active View", QString()); foreach (Sublime::View *v, area->views()) { if (v->document()->documentSpecifier() == activeView) { area->setActiveView(v); break; } } if( !area->activeView() && !area->views().isEmpty() ) area->setActiveView( area->views().at(0) ); if( area->activeView() ) { foreach(Sublime::MainWindow* window, Core::self()->uiControllerInternal()->mainWindows()) { if(window->area() == area) { window->activateView( area->activeView() ); } } } } void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex, KConfigGroup setGroup, KConfigGroup areaGroup, QMultiMap& recycle) { Q_ASSERT( !areaIndex->isSplit() ); if (setGroup.hasKey("Orientation")) { QStringList subgroups = setGroup.groupList(); /// @todo also save and restore the ratio if (subgroups.contains(QStringLiteral("0")) && subgroups.contains(QStringLiteral("1"))) { // qCDebug(SHELL) << "has zero, split:" << split; Qt::Orientation orientation = setGroup.readEntry("Orientation", "Horizontal") == QLatin1String("Vertical") ? Qt::Vertical : Qt::Horizontal; if(!areaIndex->isSplit()){ areaIndex->split(orientation); }else{ areaIndex->setOrientation(orientation); } loadToArea(area, areaIndex->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0"), recycle); loadToArea(area, areaIndex->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1"), recycle); if( areaIndex->first()->viewCount() == 0 ) areaIndex->unsplit(areaIndex->first()); else if( areaIndex->second()->viewCount() == 0 ) areaIndex->unsplit(areaIndex->second()); } } else { //Load all documents from the workingset into this areaIndex int viewCount = setGroup.readEntry("View Count", 0); QMap createdViews; for (int i = 0; i < viewCount; ++i) { QString specifier = setGroup.readEntry(QStringLiteral("View %1").arg(i), QString()); if (specifier.isEmpty()) { continue; } Sublime::View* previousView = area->views().empty() ? nullptr : area->views().at(area->views().size() - 1); QMultiMap::iterator it = recycle.find( specifier ); if( it != recycle.end() ) { area->addView( *it, areaIndex, previousView ); recycle.erase( it ); continue; } IDocument* doc = Core::self()->documentControllerInternal()->openDocument(QUrl::fromUserInput(specifier), KTextEditor::Cursor::invalid(), IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView); Sublime::Document *document = dynamic_cast(doc); if (document) { Sublime::View* view = document->createView(); area->addView(view, areaIndex, previousView); createdViews[i] = view; } else { qCWarning(SHELL) << "Unable to create view" << specifier; } } //Load state for (int i = 0; i < viewCount; ++i) { KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(i)); if (viewGroup.exists() && createdViews.contains(i)) createdViews[i]->readSessionConfig(viewGroup); } } } void deleteGroupRecursive(KConfigGroup group) { // qCDebug(SHELL) << "deleting" << group.name(); foreach(const QString& entry, group.entryMap().keys()) { group.deleteEntry(entry); } Q_ASSERT(group.entryMap().isEmpty()); foreach(const QString& subGroup, group.groupList()) { deleteGroupRecursive(group.group(subGroup)); group.deleteGroup(subGroup); } //Why doesn't this work? // Q_ASSERT(group.groupList().isEmpty()); group.deleteGroup(); } void WorkingSet::deleteSet(bool force, bool silent) { if(m_areas.isEmpty() || force) { emit aboutToRemove(this); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); deleteGroupRecursive(group); #ifdef SYNC_OFTEN setConfig.sync(); #endif if(!silent) emit setChangedSignificantly(); } } void WorkingSet::saveFromArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex) { qCDebug(SHELL) << "saving" << m_id << "from area"; bool wasPersistent = isPersistent(); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup setGroup = setConfig.group(m_id); deleteGroupRecursive(setGroup); KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title()); QString lastActiveView = areaGroup.readEntry("Active View", ""); deleteGroupRecursive(areaGroup); if (area->activeView() && area->activeView()->document()) areaGroup.writeEntry("Active View", area->activeView()->document()->documentSpecifier()); else areaGroup.writeEntry("Active View", lastActiveView); saveFromArea(area, areaIndex, setGroup, areaGroup); if(isEmpty()) { deleteGroupRecursive(setGroup); deleteGroupRecursive(areaGroup); } setPersistent(wasPersistent); #ifdef SYNC_OFTEN setConfig.sync(); #endif emit setChangedSignificantly(); } void WorkingSet::areaViewAdded(Sublime::AreaIndex*, Sublime::View*) { Sublime::Area* area = qobject_cast(sender()); Q_ASSERT(area); Q_ASSERT(area->workingSet() == m_id); qCDebug(SHELL) << "added view in" << area << ", id" << m_id; if (m_loading) { qCDebug(SHELL) << "doing nothing because loading"; return; } changed(area); } void WorkingSet::areaViewRemoved(Sublime::AreaIndex*, Sublime::View* view) { Sublime::Area* area = qobject_cast(sender()); Q_ASSERT(area); Q_ASSERT(area->workingSet() == m_id); qCDebug(SHELL) << "removed view in" << area << ", id" << m_id; if (m_loading) { qCDebug(SHELL) << "doing nothing because loading"; return; } foreach(Sublime::Area* otherArea, m_areas) { if(otherArea == area) continue; bool hadDocument = false; foreach(Sublime::View* areaView, otherArea->views()) if(view->document() == areaView->document()) hadDocument = true; if(!hadDocument) { // We do this to prevent UI flicker. The view has already been removed from // one of the connected areas, so the working-set has already recorded the change. return; } } changed(area); } void WorkingSet::setPersistent(bool persistent) { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); group.writeEntry("persistent", persistent); #ifdef SYNC_OFTEN group.sync(); #endif qCDebug(SHELL) << "setting" << m_id << "persistent:" << persistent; } bool WorkingSet::isPersistent() const { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); return group.readEntry("persistent", false); } QIcon WorkingSet::icon() const { return m_icon; } bool WorkingSet::isConnected( Sublime::Area* area ) { return m_areas.contains( area ); } QString WorkingSet::id() const { return m_id; } bool WorkingSet::hasConnectedAreas() const { return !m_areas.isEmpty(); } bool WorkingSet::hasConnectedAreas( QList< Sublime::Area* > areas ) const { foreach( Sublime::Area* area, areas ) if ( m_areas.contains( area ) ) return true; return false; } void WorkingSet::connectArea( Sublime::Area* area ) { if ( m_areas.contains( area ) ) { qCDebug(SHELL) << "tried to double-connect area"; return; } qCDebug(SHELL) << "connecting" << m_id << "to area" << area; // Q_ASSERT(area->workingSet() == m_id); m_areas.push_back( area ); connect( area, &Sublime::Area::viewAdded, this, &WorkingSet::areaViewAdded ); connect( area, &Sublime::Area::viewRemoved, this, &WorkingSet::areaViewRemoved ); } void WorkingSet::disconnectArea( Sublime::Area* area ) { if ( !m_areas.contains( area ) ) { qCDebug(SHELL) << "tried to disconnect not connected area"; return; } qCDebug(SHELL) << "disconnecting" << m_id << "from area" << area; // Q_ASSERT(area->workingSet() == m_id); disconnect( area, &Sublime::Area::viewAdded, this, &WorkingSet::areaViewAdded ); disconnect( area, &Sublime::Area::viewRemoved, this, &WorkingSet::areaViewRemoved ); m_areas.removeAll( area ); } void WorkingSet::changed( Sublime::Area* area ) { if ( m_loading ) { return; } { //Do not capture changes done while loading PushValue enableLoading( m_loading, true ); qCDebug(SHELL) << "recording change done to" << m_id; saveFromArea( area, area->rootIndex() ); for (auto it = m_areas.begin(); it != m_areas.end(); ++it ) { if (( *it ) != area ) { loadToArea(( *it ), ( *it )->rootIndex() ); } } } emit setChangedSignificantly(); } diff --git a/kdevplatform/sublime/area.cpp b/kdevplatform/sublime/area.cpp index 3d7c50d19a..1702df64a2 100644 --- a/kdevplatform/sublime/area.cpp +++ b/kdevplatform/sublime/area.cpp @@ -1,483 +1,485 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "area.h" #include #include #include #include #include #include "view.h" #include "document.h" #include "areaindex.h" #include "controller.h" #include namespace Sublime { // class AreaPrivate class AreaPrivate { public: AreaPrivate() : rootIndex(new RootAreaIndex) , currentIndex(rootIndex.data()) { } AreaPrivate(const AreaPrivate &p) : title(p.title) , rootIndex(new RootAreaIndex(*(p.rootIndex))) , currentIndex(rootIndex.data()) , controller(p.controller) , toolViewPositions() , desiredToolViews(p.desiredToolViews) , shownToolViews(p.shownToolViews) , iconName(p.iconName) , workingSet(p.workingSet) , m_actions(p.m_actions) { } ~AreaPrivate() { } struct ViewFinder { explicit ViewFinder(View *_view): view(_view), index(nullptr) {} Area::WalkerMode operator() (AreaIndex *idx) { if (idx->hasView(view)) { index = idx; return Area::StopWalker; } return Area::ContinueWalker; } View *view; AreaIndex *index; }; struct ViewLister { Area::WalkerMode operator()(AreaIndex *idx) { views += idx->views(); return Area::ContinueWalker; } QList views; }; QString title; QScopedPointer rootIndex; AreaIndex *currentIndex; Controller *controller = nullptr; QList toolViews; QMap toolViewPositions; QMap desiredToolViews; QMap shownToolViews; QString iconName; QString workingSet; QPointer activeView; QList m_actions; }; // class Area Area::Area(Controller *controller, const QString &name, const QString &title) :QObject(controller), d( new AreaPrivate() ) { // FIXME: using objectName seems fishy. Introduce areaType method, // or some such. setObjectName(name); d->title = title; d->controller = controller; d->iconName = QStringLiteral("kdevelop"); d->workingSet.clear(); qCDebug(SUBLIME) << "initial working-set:" << d->workingSet; initialize(); } Area::Area(const Area &area) : QObject(area.controller()), d( new AreaPrivate( *(area.d) ) ) { setObjectName(area.objectName()); //clone tool views d->toolViews.clear(); foreach (View *view, area.toolViews()) addToolView(view->document()->createView(), area.toolViewPosition(view)); initialize(); } void Area::initialize() { connect(this, &Area::viewAdded, d->controller, &Controller::notifyViewAdded); connect(this, &Area::aboutToRemoveView, d->controller, &Controller::notifyViewRemoved); connect(this, &Area::toolViewAdded, d->controller, &Controller::notifyToolViewAdded); connect(this, &Area::aboutToRemoveToolView, d->controller, &Controller::notifyToolViewRemoved); connect(this, &Area::toolViewMoved, d->controller, &Controller::toolViewMoved); /* In theory, ownership is passed to us, so should not bother detecting deletion outside. */ // Functor will be called after destructor has run -> capture controller pointer by value // otherwise we crash because we access the already freed pointer this->d auto controller = d->controller; connect(this, &Area::destroyed, controller, [controller] (QObject* obj) { controller->removeArea(static_cast(obj)); }); } Area::~Area() = default; View* Area::activeView() { return d->activeView.data(); } void Area::setActiveView(View* view) { d->activeView = view; } void Area::addView(View *view, AreaIndex *index, View *after) { //View *after = 0; if (!after && controller()->openAfterCurrent()) { after = activeView(); } index->add(view, after); connect(view, &View::positionChanged, this, &Area::positionChanged); qCDebug(SUBLIME) << "view added in" << this; connect(this, &Area::destroyed, view, &View::deleteLater); emit viewAdded(index, view); } void Area::addView(View *view, View *after) { AreaIndex *index = d->currentIndex; if (after) { AreaIndex *i = indexOf(after); if (i) index = i; } addView(view, index); } void Area::addView(View *view, View *viewToSplit, Qt::Orientation orientation) { AreaIndex *indexToSplit = indexOf(viewToSplit); addView(view, indexToSplit, orientation); } void Area::addView(View* view, AreaIndex* indexToSplit, Qt::Orientation orientation) { indexToSplit->split(view, orientation); emit viewAdded(indexToSplit, view); connect(this, &Area::destroyed, view, &View::deleteLater); } View* Area::removeView(View *view) { AreaIndex *index = indexOf(view); if (!index) return nullptr; emit aboutToRemoveView(index, view); index->remove(view); emit viewRemoved(index, view); return view; } AreaIndex *Area::indexOf(View *view) { AreaPrivate::ViewFinder f(view); walkViews(f, d->rootIndex.data()); return f.index; } RootAreaIndex *Area::rootIndex() const { return d->rootIndex.data(); } void Area::addToolView(View *view, Position defaultPosition) { d->toolViews.append(view); const QString id = view->document()->documentSpecifier(); const Position position = d->desiredToolViews.value(id, defaultPosition); d->desiredToolViews[id] = position; d->toolViewPositions[view] = position; emit toolViewAdded(view, position); } void Sublime::Area::raiseToolView(View * toolView) { emit requestToolViewRaise(toolView); } View* Area::removeToolView(View *view) { if (!d->toolViews.contains(view)) return nullptr; emit aboutToRemoveToolView(view, d->toolViewPositions[view]); QString id = view->document()->documentSpecifier(); qCDebug(SUBLIME) << this << "removed tool view " << id; d->desiredToolViews.remove(id); d->toolViews.removeAll(view); d->toolViewPositions.remove(view); return view; } void Area::moveToolView(View *toolView, Position newPosition) { if (!d->toolViews.contains(toolView)) return; QString id = toolView->document()->documentSpecifier(); d->desiredToolViews[id] = newPosition; d->toolViewPositions[toolView] = newPosition; emit toolViewMoved(toolView, newPosition); } QList &Area::toolViews() const { return d->toolViews; } Position Area::toolViewPosition(View *toolView) const { return d->toolViewPositions[toolView]; } Controller *Area::controller() const { return d->controller; } QList Sublime::Area::views() { AreaPrivate::ViewLister lister; walkViews(lister, d->rootIndex.data()); return lister.views; } QString Area::title() const { return d->title; } void Area::setTitle(const QString &title) { d->title = title; } void Area::save(KConfigGroup& group) const { QStringList desired; + desired.reserve(d->desiredToolViews.size()); QMap::iterator i, e; for (i = d->desiredToolViews.begin(), e = d->desiredToolViews.end(); i != e; ++i) { desired << i.key() + QLatin1Char(':') + QString::number(static_cast(i.value())); } group.writeEntry("desired views", desired); qCDebug(SUBLIME) << "save " << this << "wrote" << group.readEntry("desired views", ""); group.writeEntry("view on left", shownToolViews(Sublime::Left)); group.writeEntry("view on right", shownToolViews(Sublime::Right)); group.writeEntry("view on top", shownToolViews(Sublime::Top)); group.writeEntry("view on bottom", shownToolViews(Sublime::Bottom)); group.writeEntry("working set", d->workingSet); } void Area::load(const KConfigGroup& group) { qCDebug(SUBLIME) << "loading areas config"; d->desiredToolViews.clear(); QStringList desired = group.readEntry("desired views", QStringList()); foreach (const QString &s, desired) { int i = s.indexOf(QLatin1Char(':')); if (i != -1) { QString id = s.left(i); int pos_i = s.midRef(i+1).toInt(); Sublime::Position pos = static_cast(pos_i); if (pos != Sublime::Left && pos != Sublime::Right && pos != Sublime::Top && pos != Sublime::Bottom) { pos = Sublime::Bottom; } d->desiredToolViews[id] = pos; } } setShownToolViews(Sublime::Left, group.readEntry("view on left", QStringList())); setShownToolViews(Sublime::Right, group.readEntry("view on right", QStringList())); setShownToolViews(Sublime::Top, group.readEntry("view on top", QStringList())); setShownToolViews(Sublime::Bottom, group.readEntry("view on bottom", QStringList())); setWorkingSet(group.readEntry("working set", d->workingSet)); } bool Area::wantToolView(const QString& id) { return (d->desiredToolViews.contains(id)); } void Area::setShownToolViews(Sublime::Position pos, const QStringList& ids) { d->shownToolViews[pos] = ids; } QStringList Area::shownToolViews(Sublime::Position pos) const { if (pos == Sublime::AllPositions) { QStringList allIds; + allIds.reserve(d->shownToolViews.size()); std::for_each(d->shownToolViews.constBegin(), d->shownToolViews.constEnd(), [&](const QStringList& ids) { allIds << ids; }); return allIds; } return d->shownToolViews[pos]; } void Area::setDesiredToolViews( const QMap& desiredToolViews) { d->desiredToolViews = desiredToolViews; } QString Area::iconName() const { return d->iconName; } void Area::setIconName(const QString& iconName) { d->iconName = iconName; } void Area::positionChanged(View *view, int newPos) { qCDebug(SUBLIME) << view << newPos; AreaIndex *index = indexOf(view); index->views().move(index->views().indexOf(view), newPos); } QString Area::workingSet() const { return d->workingSet; } void Area::setWorkingSet(const QString& name) { if(name != d->workingSet) { qCDebug(SUBLIME) << this << "setting new working-set" << name; QString oldName = d->workingSet; emit changingWorkingSet(this, oldName, name); d->workingSet = name; emit changedWorkingSet(this, oldName, name); } } bool Area::closeView(View* view, bool silent) { QPointer doc = view->document(); // We don't just delete the view, because if silent is false, we might need to ask the user. if(doc && !silent) { // Do some counting to check whether we need to ask the user for feedback qCDebug(SUBLIME) << "Closing view for" << view->document()->documentSpecifier() << "views" << view->document()->views().size() << "in area" << this; int viewsInCurrentArea = 0; // Number of views for the same document in the current area int viewsInOtherAreas = 0; // Number of views for the same document in other areas int viewsInOtherWorkingSets = 0; // Number of views for the same document in areas with different working-set foreach(View* otherView, doc.data()->views()) { Area* area = controller()->areaForView(otherView); if(area == this) viewsInCurrentArea += 1; if(!area || (area != this)) viewsInOtherAreas += 1; if(area && area != this && area->workingSet() != workingSet()) viewsInOtherWorkingSets += 1; } if(viewsInCurrentArea == 1 && (viewsInOtherAreas == 0 || viewsInOtherWorkingSets == 0)) { // Time to ask the user for feedback, because the document will be completely closed // due to working-set synchronization if( !doc.data()->askForCloseFeedback() ) return false; } } // otherwise we can silently close the view, // the document will still have an opened view somewhere delete removeView(view); return true; } void Area::clearViews(bool silent) { foreach(Sublime::View* view, views()) { closeView(view, silent); } } void Area::clearDocuments() { if (views().isEmpty()) emit clearWorkingSet(this); else clearViews(true); } QList Area::actions() const { return d->m_actions; } void Area::addAction(QAction* action) { Q_ASSERT(!d->m_actions.contains(action)); connect(action, &QAction::destroyed, this, &Area::actionDestroyed); d->m_actions.append(action); } void Area::actionDestroyed(QObject* action) { d->m_actions.removeAll(qobject_cast(action)); } } diff --git a/kdevplatform/sublime/areaindex.cpp b/kdevplatform/sublime/areaindex.cpp index 5490d82070..1714a43e3c 100644 --- a/kdevplatform/sublime/areaindex.cpp +++ b/kdevplatform/sublime/areaindex.cpp @@ -1,249 +1,252 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "areaindex.h" #include #include "view.h" #include "document.h" #include namespace Sublime { // class AreaIndexPrivate class AreaIndexPrivate { public: AreaIndexPrivate() { } ~AreaIndexPrivate() { delete first; delete second; foreach( View* v, views ) { // Do the same as AreaIndex::remove(), seems like deletion of the view is happening elsewhere views.removeAll( v ); } } AreaIndexPrivate(const AreaIndexPrivate &p) { parent = nullptr; orientation = p.orientation; first = p.first ? new AreaIndex(*(p.first)) : nullptr; second = p.second ? new AreaIndex(*(p.second)) : nullptr; } bool isSplit() const { return first || second; } QList views; AreaIndex *parent = nullptr; AreaIndex *first = nullptr; AreaIndex *second = nullptr; Qt::Orientation orientation = Qt::Horizontal; }; // class AreaIndex AreaIndex::AreaIndex() : d(new AreaIndexPrivate) { } AreaIndex::AreaIndex(AreaIndex *parent) : d(new AreaIndexPrivate) { d->parent = parent; } AreaIndex::AreaIndex(const AreaIndex &index) : d(new AreaIndexPrivate( *(index.d) ) ) { qCDebug(SUBLIME) << "copying area index"; if (d->first) d->first->setParent(this); if (d->second) d->second->setParent(this); //clone views in this index d->views.clear(); foreach (View *view, index.views()) add(view->document()->createView()); } AreaIndex::~AreaIndex() = default; void AreaIndex::add(View *view, View *after) { //we can not add views to the areas that have already been split if (d->isSplit()) return; if (after) d->views.insert(d->views.indexOf(after)+1, view); else d->views.append(view); } void AreaIndex::remove(View *view) { if (d->isSplit()) return; d->views.removeAll(view); if (d->parent && (d->views.count() == 0)) d->parent->unsplit(this); } void AreaIndex::split(Qt::Orientation orientation, bool moveViewsToSecond) { //we can not split areas that have already been split if (d->isSplit()) return; d->first = new AreaIndex(this); d->second = new AreaIndex(this); d->orientation = orientation; if(moveViewsToSecond) moveViewsTo(d->second); else moveViewsTo(d->first); } void AreaIndex::split(View *newView, Qt::Orientation orientation) { split(orientation); //make new view as second widget in splitter d->second->add(newView); } void AreaIndex::unsplit(AreaIndex *childToRemove) { if (!d->isSplit()) return; AreaIndex *other = d->first == childToRemove ? d->second : d->first; other->moveViewsTo(this); d->orientation = other->orientation(); d->first = nullptr; d->second = nullptr; other->copyChildrenTo(this); delete other; delete childToRemove; } void AreaIndex::copyChildrenTo(AreaIndex *target) { if (!d->first || !d->second) return; target->d->first = d->first; target->d->second = d->second; target->d->first->setParent(target); target->d->second->setParent(target); d->first = nullptr; d->second = nullptr; } void AreaIndex::moveViewsTo(AreaIndex *target) { target->d->views = d->views; d->views.clear(); } QList &AreaIndex::views() const { return d->views; } View *AreaIndex::viewAt(int position) const { return d->views.value(position, nullptr); } int AreaIndex::viewCount() const { return d->views.count(); } bool AreaIndex::hasView(View *view) const { return d->views.contains(view); } AreaIndex *AreaIndex::parent() const { return d->parent; } void AreaIndex::setParent(AreaIndex *parent) { d->parent = parent; } AreaIndex *AreaIndex::first() const { return d->first; } AreaIndex *AreaIndex::second() const { return d->second; } Qt::Orientation AreaIndex::orientation() const { return d->orientation; } bool Sublime::AreaIndex::isSplit() const { return d->isSplit(); } void Sublime::AreaIndex::setOrientation(Qt::Orientation orientation) const { d->orientation = orientation; } // class RootAreaIndex RootAreaIndex::RootAreaIndex() :AreaIndex() { } QString AreaIndex::print() const { if(isSplit()) return QLatin1String(" [ ") + first()->print() + QLatin1String(orientation() == Qt::Horizontal ? " / " : " - ") + second()->print() + QLatin1String(" ] "); QStringList ret; - foreach(Sublime::View* view, views()) + const auto views = this->views(); + ret.reserve(views.size()); + for (const auto* view : views) { ret << view->document()->title(); + } return ret.join(QLatin1Char(' ')); } } diff --git a/kdevplatform/sublime/controller.cpp b/kdevplatform/sublime/controller.cpp index caf1d41261..6c2dc42ca0 100644 --- a/kdevplatform/sublime/controller.cpp +++ b/kdevplatform/sublime/controller.cpp @@ -1,418 +1,422 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "controller.h" #include #include #include #include #include #include #include "area.h" #include "view.h" #include "document.h" #include "mainwindow.h" #include namespace Sublime { struct WidgetFinder { explicit WidgetFinder(QWidget *_w) :w(_w), view(nullptr) {} Area::WalkerMode operator()(AreaIndex *index) { foreach (View *v, index->views()) { if (v->hasWidget() && (v->widget() == w)) { view = v; return Area::StopWalker; } } return Area::ContinueWalker; } QWidget *w; View *view; }; struct ToolWidgetFinder { explicit ToolWidgetFinder(QWidget *_w) :w(_w), view(nullptr) {} Area::WalkerMode operator()(View *v, Sublime::Position /*position*/) { if (v->hasWidget() && (v->widget() == w)) { view = v; return Area::StopWalker; } return Area::ContinueWalker; } QWidget *w; View *view; }; // class ControllerPrivate class ControllerPrivate { public: ControllerPrivate() { } QList documents; QList areas; QList allAreas; QMap namedAreas; // FIXME: remove this. QMap shownAreas; QList controlledWindows; QVector< QList > mainWindowAreas; bool openAfterCurrent; bool arrangeBuddies; }; // class Controller Controller::Controller(QObject *parent) :QObject(parent), MainWindowOperator(), d( new ControllerPrivate() ) { init(); } void Controller::init() { loadSettings(); qApp->installEventFilter(this); } Controller::~Controller() { qDeleteAll(d->controlledWindows); } void Controller::showArea(Area *area, MainWindow *mainWindow) { Area *areaToShow = nullptr; //if the area is already shown in another mainwindow then we need to clone it if (d->shownAreas.contains(area) && (mainWindow != d->shownAreas[area])) areaToShow = new Area(*area); else areaToShow = area; d->shownAreas[areaToShow] = mainWindow; showAreaInternal(areaToShow, mainWindow); } void Controller::showAreaInternal(Area* area, MainWindow *mainWindow) { /* Disconnect the previous area. We really don't want to mess with main window if an area not visible now is modified. Further, if showAreaInternal is called with the same area as is current now, we don't want to connect the same signals twice. */ MainWindowOperator::setArea(mainWindow, area); } void Controller::removeArea(Area *obj) { d->areas.removeAll(obj); } void Controller::removeDocument(Document *obj) { d->documents.removeAll(obj); } void Controller::showArea(const QString& areaTypeId, MainWindow *mainWindow) { int index = d->controlledWindows.indexOf(mainWindow); Q_ASSERT(index != -1); Area* area = nullptr; foreach (Area* a, d->mainWindowAreas[index]) { qCDebug(SUBLIME) << "Object name: " << a->objectName() << " id " << areaTypeId; if (a->objectName() == areaTypeId) { area = a; break; } } Q_ASSERT (area); showAreaInternal(area, mainWindow); } void Controller::resetCurrentArea(MainWindow *mainWindow) { QString id = mainWindow->area()->objectName(); int areaIndex = 0; Area* def = nullptr; foreach (Area* a, d->areas) { if (a->objectName() == id) { def = a; break; } ++areaIndex; } Q_ASSERT(def); int index = d->controlledWindows.indexOf(mainWindow); Q_ASSERT(index != -1); Area* prev = d->mainWindowAreas[index][areaIndex]; d->mainWindowAreas[index][areaIndex] = new Area(*def); showAreaInternal(d->mainWindowAreas[index][areaIndex], mainWindow); delete prev; } const QList &Controller::defaultAreas() const { return d->areas; } const QList< Area* >& Controller::areas(MainWindow* mainWindow) const { int index = d->controlledWindows.indexOf(mainWindow); Q_ASSERT(index != -1); return areas(index); } const QList &Controller::areas(int mainWindow) const { return d->mainWindowAreas[mainWindow]; } const QList &Controller::allAreas() const { return d->allAreas; } const QList &Controller::documents() const { return d->documents; } void Controller::addDefaultArea(Area *area) { d->areas.append(area); d->allAreas.append(area); d->namedAreas[area->objectName()] = area; emit areaCreated(area); } void Controller::addMainWindow(MainWindow* mainWindow) { Q_ASSERT(mainWindow); Q_ASSERT (!d->controlledWindows.contains(mainWindow)); d->controlledWindows << mainWindow; d->mainWindowAreas.resize(d->controlledWindows.size()); int index = d->controlledWindows.size()-1; - foreach (Area* area, defaultAreas()) - { + auto& mainWindowAreas = d->mainWindowAreas[index]; + const auto& defaultAreas = this->defaultAreas(); + d->allAreas.reserve(d->allAreas.size() + defaultAreas.size()); + mainWindowAreas.reserve(defaultAreas.size()); + + for (const auto* area : defaultAreas) { Area *na = new Area(*area); d->allAreas.append(na); - d->mainWindowAreas[index].push_back(na); + mainWindowAreas.append(na); emit areaCreated(na); } showAreaInternal(d->mainWindowAreas[index][0], mainWindow); emit mainWindowAdded( mainWindow ); } void Controller::addDocument(Document *document) { d->documents.append(document); } void Controller::areaReleased() { MainWindow *w = reinterpret_cast(sender()); qCDebug(SUBLIME) << "marking areas as mainwindow-free" << w << d->controlledWindows.contains(w) << d->shownAreas.keys(w); foreach (Area *area, d->shownAreas.keys(w)) { qCDebug(SUBLIME) << "" << area->objectName(); areaReleased(area); disconnect(area, nullptr, w, nullptr); } d->controlledWindows.removeAll(w); } void Controller::areaReleased(Sublime::Area *area) { d->shownAreas.remove(area); d->namedAreas.remove(area->objectName()); } Area *Controller::defaultArea(const QString &id) const { return d->namedAreas[id]; } Area *Controller::area(int mainWindow, const QString& id) const { foreach (Area* area, areas(mainWindow)) { if (area->objectName() == id) return area; } return nullptr; } Area* Controller::areaForView(View* view) const { foreach (Area* area, allAreas()) if(area->views().contains(view)) return area; return nullptr; } /*We need this to catch activation of views and tool views so that we can always tell what view and tool view is active. "Active" doesn't mean focused. It means that it is focused now or was focused before and no other view/tool view wasn't focused after that."*/ //implementation is based upon KParts::PartManager::eventFilter bool Controller::eventFilter(QObject *obj, QEvent *ev) { if (ev->type() != QEvent::MouseButtonPress && ev->type() != QEvent::MouseButtonDblClick && ev->type() != QEvent::FocusIn) return false; //not a widget? - return if (!obj->isWidgetType()) return false; //is dialog or popup? - return QWidget *w = static_cast(obj); if (((w->windowFlags().testFlag(Qt::Dialog)) && w->isModal()) || (w->windowFlags().testFlag(Qt::Popup)) || (w->windowFlags().testFlag(Qt::Tool))) return false; //not a mouse button that should activate the widget? - return if (ev->type() == QEvent::MouseButtonPress || ev->type() == QEvent::MouseButtonDblClick) { QMouseEvent* mev = static_cast(ev); int activationButtonMask = Qt::LeftButton | Qt::MidButton | Qt::RightButton; if ((mev->button() & activationButtonMask) == 0) return false; } while (w) { //not inside sublime mainwindow MainWindow *mw = qobject_cast(w->topLevelWidget()); if (!mw || !d->controlledWindows.contains(mw)) return false; Area *area = mw->area(); ///@todo adymo: this is extra slow - optimize //find this widget in views WidgetFinder widgetFinder(w); area->walkViews(widgetFinder, area->rootIndex()); if (widgetFinder.view && widgetFinder.view != mw->activeView()) { setActiveView(mw, widgetFinder.view); ///@todo adymo: shall we filter out the event? return false; } //find this widget in tool views ToolWidgetFinder toolFinder(w); area->walkToolViews(toolFinder, Sublime::AllPositions); if (toolFinder.view && toolFinder.view != mw->activeToolView()) { setActiveToolView(mw, toolFinder.view); ///@todo adymo: shall we filter out the event? return false; } w = w->parentWidget(); } return false; } const QList< MainWindow * > & Controller::mainWindows() const { return d->controlledWindows; } void Controller::notifyToolViewRemoved(Sublime::View *view, Sublime::Position) { emit aboutToRemoveToolView(view); } void Controller::notifyToolViewAdded(Sublime::View *view, Sublime::Position) { emit toolViewAdded(view); } void Controller::notifyViewRemoved(Sublime::AreaIndex*, Sublime::View *view) { emit aboutToRemoveView(view); } void Controller::notifyViewAdded(Sublime::AreaIndex*, Sublime::View *view) { emit viewAdded(view); } void Controller::setStatusIcon(Document * document, const QIcon & icon) { document->setStatusIcon(icon); } void Controller::loadSettings() { KConfigGroup uiGroup = KSharedConfig::openConfig()->group("UiSettings"); d->openAfterCurrent = (uiGroup.readEntry("TabBarOpenAfterCurrent", 1) == 1); d->arrangeBuddies = (uiGroup.readEntry("TabBarArrangeBuddies", 1) == 1); } bool Controller::openAfterCurrent() const { return d->openAfterCurrent; } bool Controller::arrangeBuddies() const { return d->arrangeBuddies; } } #include "moc_controller.cpp" diff --git a/kdevplatform/sublime/idealdockwidget.cpp b/kdevplatform/sublime/idealdockwidget.cpp index 6c6aa26772..83fd787f6c 100644 --- a/kdevplatform/sublime/idealdockwidget.cpp +++ b/kdevplatform/sublime/idealdockwidget.cpp @@ -1,212 +1,213 @@ /* Copyright 2007 Roberto Raggi Copyright 2007 Hamish Rodda Copyright 2011 Alexander Dymo Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE KDEVELOP TEAM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "idealdockwidget.h" #include "mainwindow.h" #include "area.h" #include "document.h" #include "view.h" #include #include #include #include #include #include #include #include #include using namespace Sublime; IdealDockWidget::IdealDockWidget(IdealController *controller, Sublime::MainWindow *parent) : QDockWidget(parent), m_area(nullptr), m_view(nullptr), m_docking_area(Qt::NoDockWidgetArea), m_controller(controller) { setAutoFillBackground(true); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &IdealDockWidget::customContextMenuRequested, this, &IdealDockWidget::contextMenuRequested); QAbstractButton *closeButton = findChild(QStringLiteral("qt_dockwidget_closebutton")); if (closeButton) { disconnect(closeButton, &QAbstractButton::clicked, nullptr, nullptr); connect(closeButton, &QAbstractButton::clicked, this, &IdealDockWidget::closeRequested); } setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); // do not allow to move docks to the top dock area (no buttonbar there in our current UI) setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::BottomDockWidgetArea); } IdealDockWidget::~IdealDockWidget() { } Area *IdealDockWidget::area() const { return m_area; } void IdealDockWidget::setArea(Area *area) { m_area = area; } View *IdealDockWidget::view() const { return m_view; } void IdealDockWidget::setView(View *view) { m_view = view; } Qt::DockWidgetArea IdealDockWidget::dockWidgetArea() const { return m_docking_area; } void IdealDockWidget::setDockWidgetArea(Qt::DockWidgetArea dockingArea) { m_docking_area = dockingArea; } void IdealDockWidget::slotRemove() { m_area->removeToolView(m_view); } void IdealDockWidget::contextMenuRequested(const QPoint &point) { QWidget* senderWidget = qobject_cast(sender()); Q_ASSERT(senderWidget); QMenu menu; menu.addSection(windowIcon(), m_view->document()->title()); QList< QAction* > viewActions = m_view->contextMenuActions(); if(!viewActions.isEmpty()) { // add the view's actions to the context menu, // checking each if it can be represented foreach (const auto action, viewActions) { if (!action->text().isEmpty() && !action->iconText().isEmpty()) { // avoid adding empty menu items menu.addAction(action); } } menu.addSeparator(); } ///TODO: can this be cleaned up? if(QToolBar* toolBar = widget()->findChild()) { menu.addAction(toolBar->toggleViewAction()); menu.addSeparator(); } /// start position menu QMenu* positionMenu = menu.addMenu(i18n("Tool View Position")); QActionGroup* g = new QActionGroup(positionMenu); QAction *left = new QAction(i18nc("tool view position", "Left"), g); QAction *bottom = new QAction(i18nc("tool view position", "Bottom"), g); QAction *right = new QAction(i18nc("tool view position", "Right"), g); QAction *detach = new QAction(i18nc("tool view position", "Detached"), g); for (auto action : {left, bottom, right, detach}) { positionMenu->addAction(action); action->setCheckable(true); } if (isFloating()) { detach->setChecked(true); } else if (m_docking_area == Qt::BottomDockWidgetArea) bottom->setChecked(true); else if (m_docking_area == Qt::LeftDockWidgetArea) left->setChecked(true); else if (m_docking_area == Qt::RightDockWidgetArea) right->setChecked(true); /// end position menu menu.addSeparator(); QAction *setShortcut = menu.addAction(QIcon::fromTheme(QStringLiteral("configure-shortcuts")), i18n("Assign Shortcut...")); setShortcut->setToolTip(i18n("Use this shortcut to trigger visibility of the tool view.")); menu.addSeparator(); QAction* remove = menu.addAction(QIcon::fromTheme(QStringLiteral("dialog-close")), i18n("Remove Tool View")); QAction* triggered = menu.exec(senderWidget->mapToGlobal(point)); if (triggered) { if ( triggered == remove ) { slotRemove(); return; } else if ( triggered == setShortcut ) { QDialog* dialog(new QDialog(this)); dialog->setWindowTitle(i18n("Assign Shortcut For '%1' Tool View", m_view->document()->title())); KShortcutWidget *w = new KShortcutWidget(dialog); w->setShortcut(m_controller->actionForView(m_view)->shortcuts()); QVBoxLayout* dialogLayout = new QVBoxLayout(dialog); dialogLayout->addWidget(w); QDialogButtonBox* buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); dialogLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); if (dialog->exec() == QDialog::Accepted) { m_controller->actionForView(m_view)->setShortcuts(w->shortcut()); //save shortcut config KConfigGroup config = KSharedConfig::openConfig()->group("UI"); - QStringList shortcuts; - shortcuts << w->shortcut().value(0).toString(); - shortcuts << w->shortcut().value(1).toString(); + const QStringList shortcuts{ + w->shortcut().value(0).toString(), + w->shortcut().value(1).toString(), + }; config.writeEntry(QStringLiteral("Shortcut for %1").arg(m_view->document()->title()), shortcuts); config.sync(); } delete dialog; return; } else if ( triggered == detach ) { setFloating(true); m_area->raiseToolView(m_view); return; } if (isFloating()) { setFloating(false); } Sublime::Position pos; if (triggered == left) pos = Sublime::Left; else if (triggered == bottom) pos = Sublime::Bottom; else if (triggered == right) pos = Sublime::Right; else return; Area *area = m_area; View *view = m_view; /* This call will delete *this, so we no longer can access member variables. */ m_area->moveToolView(m_view, pos); area->raiseToolView(view); } } diff --git a/kdevplatform/sublime/mainwindow.cpp b/kdevplatform/sublime/mainwindow.cpp index 963ed044e2..97fc60d91c 100644 --- a/kdevplatform/sublime/mainwindow.cpp +++ b/kdevplatform/sublime/mainwindow.cpp @@ -1,445 +1,445 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "mainwindow.h" #include "mainwindow_p.h" #include #include #include #include #include #include #include #include #include "area.h" #include "view.h" #include "controller.h" #include "container.h" #include "idealbuttonbarwidget.h" #include "idealcontroller.h" #include "holdupdates.h" #include namespace Sublime { MainWindow::MainWindow(Controller *controller, Qt::WindowFlags flags) : KParts::MainWindow(nullptr, flags), d(new MainWindowPrivate(this, controller)) { connect(this, &MainWindow::destroyed, controller, static_cast(&Controller::areaReleased)); loadGeometry(KSharedConfig::openConfig()->group("Main Window")); // don't allow AllowTabbedDocks - that doesn't make sense for "ideal" UI setDockOptions(QMainWindow::AnimatedDocks); } bool MainWindow::containsView(View* view) const { foreach(Area* area, areas()) if(area->views().contains(view)) return true; return false; } QList< Area* > MainWindow::areas() const { QList< Area* > areas = controller()->areas(const_cast(this)); if(areas.isEmpty()) areas = controller()->defaultAreas(); return areas; } MainWindow::~MainWindow() { qCDebug(SUBLIME) << "destroying mainwindow"; } void MainWindow::reconstructViews(const QList& topViews) { d->reconstructViews(topViews); } QList MainWindow::topViews() const { QList topViews; foreach(View* view, d->area->views()) { if(view->hasWidget()) { QWidget* widget = view->widget(); if(widget->parent() && widget->parent()->parent()) { Container* container = qobject_cast(widget->parent()->parent()); if(container->currentWidget() == widget) topViews << view; } } } return topViews; } QList MainWindow::containers() const { return d->viewContainers.values(); } void MainWindow::setArea(Area *area) { if (d->area) disconnect(d->area, nullptr, d.data(), nullptr); bool differentArea = (area != d->area); /* All views will be removed from dock area now. However, this does not mean those are removed from area, so prevent slotDockShown from recording those views as no longer shown in the area. */ d->ignoreDockShown = true; if (d->autoAreaSettingsSave && differentArea) saveSettings(); HoldUpdates hu(this); if (d->area) clearArea(); d->area = area; d->reconstruct(); if(d->area->activeView()) activateView(d->area->activeView()); else d->activateFirstVisibleView(); initializeStatusBar(); emit areaChanged(area); d->ignoreDockShown = false; hu.stop(); loadSettings(); connect(area, &Area::viewAdded, d.data(), &MainWindowPrivate::viewAdded); connect(area, &Area::viewRemoved, d.data(), &MainWindowPrivate::viewRemovedInternal); connect(area, &Area::requestToolViewRaise, d.data(), &MainWindowPrivate::raiseToolView); connect(area, &Area::aboutToRemoveView, d.data(), &MainWindowPrivate::aboutToRemoveView); connect(area, &Area::toolViewAdded, d.data(), &MainWindowPrivate::toolViewAdded); connect(area, &Area::aboutToRemoveToolView, d.data(), &MainWindowPrivate::aboutToRemoveToolView); connect(area, &Area::toolViewMoved, d.data(), &MainWindowPrivate::toolViewMoved); } void MainWindow::initializeStatusBar() { //nothing here, reimplement in the subclasses if you want to have status bar //inside the bottom tool view buttons row } void MainWindow::resizeEvent(QResizeEvent* event) { return KParts::MainWindow::resizeEvent(event); } void MainWindow::clearArea() { emit areaCleared(d->area); d->clearArea(); } QList MainWindow::toolDocks() const { return d->docks; } Area *Sublime::MainWindow::area() const { return d->area; } Controller *MainWindow::controller() const { return d->controller; } View *MainWindow::activeView() const { return d->activeView; } View *MainWindow::activeToolView() const { return d->activeToolView; } void MainWindow::activateView(Sublime::View* view, bool focus) { if (!d->viewContainers.contains(view)) return; if (d->activeView == view) { if (focus && view && !view->widget()->hasFocus()) view->widget()->setFocus(); return; } d->viewContainers[view]->setCurrentWidget(view->widget()); setActiveView(view, focus); d->area->setActiveView(view); } void MainWindow::setActiveView(View *view, bool focus) { View* oldActiveView = d->activeView; d->activeView = view; if (focus && view && !view->widget()->hasFocus()) view->widget()->setFocus(); if(d->activeView != oldActiveView) emit activeViewChanged(view); } void Sublime::MainWindow::setActiveToolView(View *view) { d->activeToolView = view; emit activeToolViewChanged(view); } void MainWindow::saveSettings() { d->disableConcentrationMode(); QString group = QStringLiteral("MainWindow"); if (area()) group += QLatin1Char('_') + area()->objectName(); KConfigGroup cg = KSharedConfig::openConfig()->group(group); // This will try to save also the window size and the enabled state of the statusbar. // But it's OK, since we won't use this information when loading. saveMainWindowSettings(cg); //debugToolBar visibility is stored separately to allow a area dependent default value foreach (KToolBar* toolbar, toolBars()) { if (toolbar->objectName() == QLatin1String("debugToolBar")) { cg.writeEntry("debugToolBarVisibility", toolbar->isVisibleTo(this)); } } d->idealController->leftBarWidget->saveOrderSettings(cg); d->idealController->bottomBarWidget->saveOrderSettings(cg); d->idealController->rightBarWidget->saveOrderSettings(cg); cg.sync(); } void MainWindow::loadSettings() { HoldUpdates hu(this); qCDebug(SUBLIME) << "loading settings for " << (area() ? area()->objectName() : QString()); QString group = QStringLiteral("MainWindow"); if (area()) group += QLatin1Char('_') + area()->objectName(); KConfigGroup cg = KSharedConfig::openConfig()->group(group); // What follows is copy-paste from applyMainWindowSettings. Unfortunately, // we don't really want that one to try restoring window size, and we also // cannot stop it from doing that in any clean way. // We also do not want that one do it for the enabled state of the statusbar: // KMainWindow scans the widget tree for a QStatusBar-inheriting instance and // set enabled state by the config value stored by the key "StatusBar", // while the QStatusBar subclass used in sublime should always be enabled. QMenuBar* mb = findChild(); if (mb) { QString entry = cg.readEntry ("MenuBar", "Enabled"); if ( entry == QLatin1String("Disabled") ) mb->hide(); else mb->show(); } if ( !autoSaveSettings() || cg.name() == autoSaveGroup() ) { QString entry = cg.readEntry ("ToolBarsMovable", "Enabled"); if ( entry == QLatin1String("Disabled") ) KToolBar::setToolBarsLocked(true); else KToolBar::setToolBarsLocked(false); } // Utilise the QMainWindow::restoreState() functionality // Note that we're fixing KMainWindow bug here -- the original // code has this fragment above restoring toolbar properties. // As result, each save/restore would move the toolbar a bit to // the left. if (cg.hasKey("State")) { QByteArray state; state = cg.readEntry("State", state); state = QByteArray::fromBase64(state); // One day will need to load the version number, but for now, assume 0 restoreState(state); } else { // If there's no state we use a default size of 870x650 // Resize only when showing "code" area. If we do that for other areas, // then we'll hit bug https://bugs.kde.org/show_bug.cgi?id=207990 // TODO: adymo: this is more like a hack, we need a proper first-start initialization if (area() && area()->objectName() == QLatin1String("code")) resize(870,650); } int n = 1; // Toolbar counter. toolbars are counted from 1, foreach (KToolBar* toolbar, toolBars()) { QString group(QStringLiteral("Toolbar")); // Give a number to the toolbar, but prefer a name if there is one, // because there's no real guarantee on the ordering of toolbars - group += (toolbar->objectName().isEmpty() ? QString::number(n) : QStringLiteral(" ")+toolbar->objectName()); + group += (toolbar->objectName().isEmpty() ? QString::number(n) : QLatin1Char(' ')+toolbar->objectName()); KConfigGroup toolbarGroup(&cg, group); toolbar->applySettings(toolbarGroup); if (toolbar->objectName() == QLatin1String("debugToolBar")) { //debugToolBar visibility is stored separately to allow a area dependent default value bool visibility = cg.readEntry("debugToolBarVisibility", area()->objectName() == QLatin1String("debug")); toolbar->setVisible(visibility); } n++; } const bool tabBarHidden = !Container::configTabBarVisible(); foreach (Container *container, d->viewContainers) { container->setTabBarHidden(tabBarHidden); } hu.stop(); d->idealController->leftBarWidget->loadOrderSettings(cg); d->idealController->bottomBarWidget->loadOrderSettings(cg); d->idealController->rightBarWidget->loadOrderSettings(cg); emit settingsLoaded(); d->disableConcentrationMode(); } bool MainWindow::queryClose() { // saveSettings(); KConfigGroup config(KSharedConfig::openConfig(), "Main Window"); saveGeometry(config); config.sync(); return KParts::MainWindow::queryClose(); } QString MainWindow::screenKey() const { const int scnum = QApplication::desktop()->screenNumber(parentWidget()); QRect desk = QApplication::desktop()->screenGeometry(scnum); // if the desktop is virtual then use virtual screen size if (QApplication::desktop()->isVirtualDesktop()) desk = QApplication::desktop()->screenGeometry(QApplication::desktop()->screen()); return QStringLiteral("Desktop %1 %2") .arg(desk.width()).arg(desk.height()); } void MainWindow::saveGeometry(KConfigGroup &config) { config.writeEntry(screenKey(), geometry()); } void MainWindow::loadGeometry(const KConfigGroup &config) { // The below code, essentially, is copy-paste from // KMainWindow::restoreWindowSize. Right now, that code is buggy, // as per http://permalink.gmane.org/gmane.comp.kde.devel.core/52423 // so we implement a less theoretically correct, but working, version // below QRect g = config.readEntry(screenKey(), QRect()); if (!g.isEmpty()) setGeometry(g); } void MainWindow::enableAreaSettingsSave() { d->autoAreaSettingsSave = true; } QWidget *MainWindow::statusBarLocation() const { return d->idealController->statusBarLocation(); } ViewBarContainer *MainWindow::viewBarContainer() const { return d->viewBarContainer; } void MainWindow::setTabBarLeftCornerWidget(QWidget* widget) { d->setTabBarLeftCornerWidget(widget); } void MainWindow::tabDoubleClicked(View* view) { Q_UNUSED(view); d->toggleDocksShown(); } void MainWindow::tabContextMenuRequested(View* , QMenu* ) { // do nothing } void MainWindow::tabToolTipRequested(View*, Container*, int) { // do nothing } void MainWindow::newTabRequested() { } void MainWindow::dockBarContextMenuRequested(Qt::DockWidgetArea , const QPoint& ) { // do nothing } View* MainWindow::viewForPosition(const QPoint& globalPos) const { foreach(Container* container, d->viewContainers) { QRect globalGeom = QRect(container->mapToGlobal(QPoint(0,0)), container->mapToGlobal(QPoint(container->width(), container->height()))); if(globalGeom.contains(globalPos)) { return d->widgetToView[container->currentWidget()]; } } return nullptr; } void MainWindow::setBackgroundCentralWidget(QWidget* w) { d->setBackgroundCentralWidget(w); } } #include "moc_mainwindow.cpp" diff --git a/kdevplatform/sublime/mainwindow_p.cpp b/kdevplatform/sublime/mainwindow_p.cpp index ba6485c3f8..9bbc868833 100644 --- a/kdevplatform/sublime/mainwindow_p.cpp +++ b/kdevplatform/sublime/mainwindow_p.cpp @@ -1,817 +1,818 @@ /*************************************************************************** * Copyright 2006-2009 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "mainwindow_p.h" #include #include #include #include #include #include #include #include #include #include "area.h" #include "view.h" #include "areaindex.h" #include "document.h" #include "container.h" #include "controller.h" #include "mainwindow.h" #include "viewbarcontainer.h" #include "idealcontroller.h" #include "holdupdates.h" #include "idealbuttonbarwidget.h" #include class IdealToolBar : public QToolBar { Q_OBJECT public: explicit IdealToolBar(const QString& title, bool hideWhenEmpty, Sublime::IdealButtonBarWidget* buttons, QMainWindow* parent) : QToolBar(title, parent) , m_buttons(buttons) , m_hideWhenEmpty(hideWhenEmpty) { setMovable(false); setFloatable(false); setObjectName(title); layout()->setMargin(0); addWidget(m_buttons); if (m_hideWhenEmpty) { connect(m_buttons, &Sublime::IdealButtonBarWidget::emptyChanged, this, &IdealToolBar::updateVisibilty); } } private Q_SLOTS: void updateVisibilty() { setVisible(!m_buttons->isEmpty()); } private: Sublime::IdealButtonBarWidget* m_buttons; const bool m_hideWhenEmpty; }; namespace Sublime { MainWindowPrivate::MainWindowPrivate(MainWindow *w, Controller* controller) :controller(controller), area(nullptr), activeView(nullptr), activeToolView(nullptr), bgCentralWidget(nullptr), ignoreDockShown(false), autoAreaSettingsSave(false), m_mainWindow(w) { KActionCollection *ac = m_mainWindow->actionCollection(); m_concentrationModeAction = new QAction(i18n("Concentration Mode"), this); m_concentrationModeAction->setIcon(QIcon::fromTheme(QStringLiteral("page-zoom"))); m_concentrationModeAction->setToolTip(i18n("Removes most of the controls so you can focus on what matters.")); m_concentrationModeAction->setCheckable(true); m_concentrationModeAction->setChecked(false); ac->setDefaultShortcut(m_concentrationModeAction, Qt::META | Qt::Key_C); connect(m_concentrationModeAction, &QAction::toggled, this, &MainWindowPrivate::restoreConcentrationMode); ac->addAction(QStringLiteral("toggle_concentration_mode"), m_concentrationModeAction); QAction* action = new QAction(i18n("Show Left Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Left); connect(action, &QAction::toggled, this, &MainWindowPrivate::showLeftDock); ac->addAction(QStringLiteral("show_left_dock"), action); action = new QAction(i18n("Show Right Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Right); connect(action, &QAction::toggled, this, &MainWindowPrivate::showRightDock); ac->addAction(QStringLiteral("show_right_dock"), action); action = new QAction(i18n("Show Bottom Dock"), this); action->setCheckable(true); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Down); connect(action, &QAction::toggled, this, &MainWindowPrivate::showBottomDock); ac->addAction(QStringLiteral("show_bottom_dock"), action); action = new QAction(i18nc("@action", "Focus Editor"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_E); connect(action, &QAction::triggered, this, &MainWindowPrivate::focusEditor); ac->addAction(QStringLiteral("focus_editor"), action); action = new QAction(i18n("Hide/Restore Docks"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_Up); connect(action, &QAction::triggered, this, &MainWindowPrivate::toggleDocksShown); ac->addAction(QStringLiteral("hide_all_docks"), action); action = new QAction(i18n("Next Tool View"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_N); action->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); connect(action, &QAction::triggered, this, &MainWindowPrivate::selectNextDock); ac->addAction(QStringLiteral("select_next_dock"), action); action = new QAction(i18n("Previous Tool View"), this); ac->setDefaultShortcut(action, Qt::META | Qt::CTRL | Qt::Key_P); action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); connect(action, &QAction::triggered, this, &MainWindowPrivate::selectPreviousDock); ac->addAction(QStringLiteral("select_previous_dock"), action); action = new KActionMenu(i18n("Tool Views"), this); ac->addAction(QStringLiteral("docks_submenu"), action); idealController = new IdealController(m_mainWindow); m_leftToolBar = new IdealToolBar(i18n("Left Button Bar"), true, idealController->leftBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::LeftToolBarArea, m_leftToolBar); m_rightToolBar = new IdealToolBar(i18n("Right Button Bar"), true, idealController->rightBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::RightToolBarArea, m_rightToolBar); m_bottomToolBar = new IdealToolBar(i18n("Bottom Button Bar"), false, idealController->bottomBarWidget, m_mainWindow); m_mainWindow->addToolBar(Qt::BottomToolBarArea, m_bottomToolBar); // adymo: intentionally do not add a toolbar for top buttonbar // this doesn't work well with toolbars added via xmlgui centralWidget = new QWidget; centralWidget->setObjectName(QStringLiteral("centralWidget")); QVBoxLayout* layout = new QVBoxLayout(centralWidget); layout->setMargin(0); centralWidget->setLayout(layout); splitterCentralWidget = new QSplitter(centralWidget); // take as much space as possible splitterCentralWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(splitterCentralWidget, 2); // this view bar container is used for the ktexteditor integration to show // all view bars at a central place, esp. for split view configurations viewBarContainer = new ViewBarContainer; viewBarContainer->setObjectName(QStringLiteral("viewBarContainer")); // hide by default viewBarContainer->setVisible(false); // only take as much as needed viewBarContainer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); layout->addWidget(viewBarContainer); m_mainWindow->setCentralWidget(centralWidget); connect(idealController, &IdealController::dockShown, this, &MainWindowPrivate::slotDockShown); connect(idealController, &IdealController::widgetResized, this, &MainWindowPrivate::widgetResized); connect(idealController, &IdealController::dockBarContextMenuRequested, m_mainWindow, &MainWindow::dockBarContextMenuRequested); } MainWindowPrivate::~MainWindowPrivate() { delete m_leftTabbarCornerWidget.data(); m_leftTabbarCornerWidget.clear(); } void MainWindowPrivate::disableConcentrationMode() { m_concentrationModeAction->setChecked(false); restoreConcentrationMode(); } void MainWindowPrivate::restoreConcentrationMode() { const bool concentrationModeOn = m_concentrationModeAction->isChecked(); QWidget* cornerWidget = nullptr; if (m_concentrateToolBar) { QLayout* l = m_concentrateToolBar->layout(); QLayoutItem* li = l->takeAt(1); //ensure the cornerWidget isn't destroyed with the toolbar if (li) { cornerWidget = li->widget(); delete li; } m_concentrateToolBar->deleteLater(); } m_mainWindow->menuBar()->setVisible(!concentrationModeOn); m_bottomToolBar->setVisible(!concentrationModeOn); m_leftToolBar->setVisible(!concentrationModeOn); m_rightToolBar->setVisible(!concentrationModeOn); if (concentrationModeOn) { m_concentrateToolBar = new QToolBar(m_mainWindow); m_concentrateToolBar->setObjectName(QStringLiteral("concentrateToolBar")); m_concentrateToolBar->addAction(m_concentrationModeAction); m_concentrateToolBar->toggleViewAction()->setVisible(false); QWidgetAction *action = new QWidgetAction(this); action->setDefaultWidget(m_mainWindow->menuBar()->cornerWidget(Qt::TopRightCorner)); m_concentrateToolBar->addAction(action); m_concentrateToolBar->setMovable(false); m_mainWindow->addToolBar(Qt::TopToolBarArea, m_concentrateToolBar); m_mainWindow->menuBar()->setCornerWidget(nullptr, Qt::TopRightCorner); } else if (cornerWidget) { m_mainWindow->menuBar()->setCornerWidget(cornerWidget, Qt::TopRightCorner); cornerWidget->show(); } if (concentrationModeOn) { m_mainWindow->installEventFilter(this); } else { m_mainWindow->removeEventFilter(this); } } bool MainWindowPrivate::eventFilter(QObject* obj, QEvent* event) { Q_ASSERT(m_mainWindow == obj); if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { const auto ev = static_cast(event); Qt::KeyboardModifiers modifiers = ev->modifiers(); //QLineEdit banned mostly so that alt navigation can be used from QuickOpen const bool visible = modifiers == Qt::AltModifier && ev->type() == QEvent::KeyPress && !qApp->focusWidget()->inherits("QLineEdit"); m_mainWindow->menuBar()->setVisible(visible); } return false; } void MainWindowPrivate::showLeftDock(bool b) { idealController->showLeftDock(b); } void MainWindowPrivate::showBottomDock(bool b) { idealController->showBottomDock(b); } void MainWindowPrivate::showRightDock(bool b) { idealController->showRightDock(b); } void MainWindowPrivate::setBackgroundCentralWidget(QWidget* w) { delete bgCentralWidget; QLayout* l=m_mainWindow->centralWidget()->layout(); l->addWidget(w); bgCentralWidget=w; setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::setBackgroundVisible(bool v) { if(!bgCentralWidget) return; bgCentralWidget->setVisible(v); splitterCentralWidget->setVisible(!v); } void MainWindowPrivate::focusEditor() { if (View* view = m_mainWindow->activeView()) if (view->hasWidget()) view->widget()->setFocus(Qt::ShortcutFocusReason); } void MainWindowPrivate::toggleDocksShown() { idealController->toggleDocksShown(); } void MainWindowPrivate::selectNextDock() { idealController->goPrevNextDock(IdealController::NextDock); } void MainWindowPrivate::selectPreviousDock() { idealController->goPrevNextDock(IdealController::PrevDock); } Area::WalkerMode MainWindowPrivate::IdealToolViewCreator::operator() (View *view, Sublime::Position position) { if (!d->docks.contains(view)) { d->docks << view; //add view d->idealController->addView(d->positionToDockArea(position), view); } return Area::ContinueWalker; } Area::WalkerMode MainWindowPrivate::ViewCreator::operator() (AreaIndex *index) { QSplitter *splitter = d->m_indexSplitters.value(index); if (!splitter) { //no splitter - we shall create it and populate with views if (!index->parent()) { qCDebug(SUBLIME) << "reconstructing root area"; //this is root area splitter = d->splitterCentralWidget; d->m_indexSplitters[index] = splitter; } else { if (!d->m_indexSplitters.value(index->parent())) { // can happen in working set code, as that adds a view to a child index first // hence, recursively reconstruct the parent indizes first operator()(index->parent()); } QSplitter *parent = d->m_indexSplitters.value(index->parent()); splitter = new QSplitter(parent); d->m_indexSplitters[index] = splitter; if(index == index->parent()->first()) parent->insertWidget(0, splitter); else parent->addWidget(splitter); } Q_ASSERT(splitter); } if (index->isSplit()) //this is a visible splitter splitter->setOrientation(index->orientation()); else { Container *container = nullptr; while(splitter->count() && qobject_cast(splitter->widget(0))) { // After unsplitting, we might have to remove old splitters QWidget* widget = splitter->widget(0); qCDebug(SUBLIME) << "deleting" << widget; widget->setParent(nullptr); delete widget; } if (!splitter->widget(0)) { //we need to create view container container = new Container(splitter); connect(container, &Container::activateView, d->m_mainWindow, &MainWindow::activateViewAndFocus); connect(container, &Container::tabDoubleClicked, d->m_mainWindow, &MainWindow::tabDoubleClicked); connect(container, &Container::tabContextMenuRequested, d->m_mainWindow, &MainWindow::tabContextMenuRequested); connect(container, &Container::tabToolTipRequested, d->m_mainWindow, &MainWindow::tabToolTipRequested); connect(container, static_cast(&Container::requestClose), d, &MainWindowPrivate::widgetCloseRequest, Qt::QueuedConnection); connect(container, &Container::newTabRequested, d->m_mainWindow, &MainWindow::newTabRequested); splitter->addWidget(container); } else container = qobject_cast(splitter->widget(0)); container->show(); int position = 0; bool hadActiveView = false; Sublime::View* activeView = d->activeView; foreach (View *view, index->views()) { QWidget *widget = view->widget(container); if (widget) { if(!container->hasWidget(widget)) { container->addWidget(view, position); d->viewContainers[view] = container; d->widgetToView[widget] = view; } if(activeView == view) { hadActiveView = true; container->setCurrentWidget(widget); }else if(topViews.contains(view) && !hadActiveView) container->setCurrentWidget(widget); } position++; } } return Area::ContinueWalker; } void MainWindowPrivate::reconstructViews(const QList& topViews) { ViewCreator viewCreator(this, topViews); area->walkViews(viewCreator, area->rootIndex()); setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::reconstruct() { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } IdealToolViewCreator toolViewCreator(this); area->walkToolViews(toolViewCreator, Sublime::AllPositions); reconstructViews(); { QSignalBlocker blocker(m_mainWindow); qCDebug(SUBLIME) << "RECONSTRUCT" << area << area->shownToolViews(Sublime::Left); foreach (View *view, area->toolViews()) { QString id = view->document()->documentSpecifier(); if (!id.isEmpty()) { Sublime::Position pos = area->toolViewPosition(view); if (area->shownToolViews(pos).contains(id)) idealController->raiseView(view, IdealController::GroupWithOtherViews); } } } setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); } void MainWindowPrivate::clearArea() { if(m_leftTabbarCornerWidget) m_leftTabbarCornerWidget->setParent(nullptr); //reparent tool view widgets to nullptr to prevent their deletion together with dockwidgets foreach (View *view, area->toolViews()) { // FIXME should we really delete here?? bool nonDestructive = true; idealController->removeView(view, nonDestructive); if (view->hasWidget()) view->widget()->setParent(nullptr); } docks.clear(); //reparent all view widgets to 0 to prevent their deletion together with central //widget. this reparenting is necessary when switching areas inside the same mainwindow foreach (View *view, area->views()) { if (view->hasWidget()) view->widget()->setParent(nullptr); } cleanCentralWidget(); m_mainWindow->setActiveView(nullptr); m_indexSplitters.clear(); area = nullptr; viewContainers.clear(); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); } void MainWindowPrivate::cleanCentralWidget() { while(splitterCentralWidget->count()) delete splitterCentralWidget->widget(0); setBackgroundVisible(true); } struct ShownToolViewFinder { ShownToolViewFinder() {} Area::WalkerMode operator()(View *v, Sublime::Position /*position*/) { if (v->hasWidget() && v->widget()->isVisible()) views << v; return Area::ContinueWalker; } QList views; }; void MainWindowPrivate::slotDockShown(Sublime::View* /*view*/, Sublime::Position pos, bool /*shown*/) { if (ignoreDockShown) return; ShownToolViewFinder finder; m_mainWindow->area()->walkToolViews(finder, pos); QStringList ids; + ids.reserve(finder.views.size()); foreach (View *v, finder.views) { ids << v->document()->documentSpecifier(); } area->setShownToolViews(pos, ids); } void MainWindowPrivate::viewRemovedInternal(AreaIndex* index, View* view) { Q_UNUSED(index); Q_UNUSED(view); setBackgroundVisible(area->views().isEmpty()); } void MainWindowPrivate::viewAdded(Sublime::AreaIndex *index, Sublime::View *view) { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } // Remove container objects in the hierarchy from the parents, // because they are not needed anymore, and might lead to broken splitter hierarchy and crashes. for(Sublime::AreaIndex* current = index; current; current = current->parent()) { QSplitter *splitter = m_indexSplitters[current]; if (current->isSplit() && splitter) { // Also update the orientation splitter->setOrientation(current->orientation()); for(int w = 0; w < splitter->count(); ++w) { Container *container = qobject_cast(splitter->widget(w)); //we need to remove extra container before reconstruction //first reparent widgets in container so that they are not deleted if(container) { while (container->count()) { container->widget(0)->setParent(nullptr); } //and then delete the container delete container; } } } } ViewCreator viewCreator(this); area->walkViews(viewCreator, index); emit m_mainWindow->viewAdded( view ); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); setBackgroundVisible(false); } void Sublime::MainWindowPrivate::raiseToolView(Sublime::View * view) { idealController->raiseView(view); } void MainWindowPrivate::aboutToRemoveView(Sublime::AreaIndex *index, Sublime::View *view) { QSplitter *splitter = m_indexSplitters[index]; if (!splitter) return; qCDebug(SUBLIME) << "index " << index << " root " << area->rootIndex(); qCDebug(SUBLIME) << "splitter " << splitter << " container " << splitter->widget(0); qCDebug(SUBLIME) << "structure: " << index->print() << " whole structure: " << area->rootIndex()->print(); //find the container for the view and remove the widget Container *container = qobject_cast(splitter->widget(0)); if (!container) { qCWarning(SUBLIME) << "Splitter does not have a left widget!"; return; } emit m_mainWindow->aboutToRemoveView( view ); if (view->widget()) widgetToView.remove(view->widget()); viewContainers.remove(view); const bool wasActive = m_mainWindow->activeView() == view; if (container->count() > 1) { //container is not empty or this is a root index //just remove a widget if( view->widget() ) { container->removeWidget(view->widget()); view->widget()->setParent(nullptr); //activate what is visible currently in the container if the removed view was active if (wasActive) return m_mainWindow->setActiveView(container->viewForWidget(container->currentWidget())); } } else { if(m_leftTabbarCornerWidget) { m_leftTabbarCornerWidget->hide(); m_leftTabbarCornerWidget->setParent(nullptr); } // We've about to remove the last view of this container. It will // be empty, so have to delete it, as well. // If we have a container, then it should be the only child of // the splitter. Q_ASSERT(splitter->count() == 1); container->removeWidget(view->widget()); if (view->widget()) view->widget()->setParent(nullptr); else qCWarning(SUBLIME) << "View does not have a widget!"; Q_ASSERT(container->count() == 0); // We can be called from signal handler of container // (which is tab widget), so defer deleting it. container->deleteLater(); container->setParent(nullptr); /* If we're not at the top level, we get to collapse split views. */ if (index->parent()) { /* The splitter used to have container as the only child, now it's time to get rid of it. Make sure deleting splitter does not delete container -- per above comment, we'll delete it later. */ container->setParent(nullptr); m_indexSplitters.remove(index); delete splitter; AreaIndex *parent = index->parent(); QSplitter *parentSplitter = m_indexSplitters[parent]; AreaIndex *sibling = parent->first() == index ? parent->second() : parent->first(); QSplitter *siblingSplitter = m_indexSplitters[sibling]; if(siblingSplitter) { HoldUpdates du(parentSplitter); //save sizes and orientation of the sibling splitter parentSplitter->setOrientation(siblingSplitter->orientation()); QList sizes = siblingSplitter->sizes(); /* Parent has two children -- 'index' that we've deleted and 'sibling'. We move all children of 'sibling' into parent, and delete 'sibling'. sibling either contains a single Container instance, or a bunch of further QSplitters. */ while (siblingSplitter->count() > 0) { //reparent contents into parent splitter QWidget *siblingWidget = siblingSplitter->widget(0); siblingWidget->setParent(parentSplitter); parentSplitter->addWidget(siblingWidget); } m_indexSplitters.remove(sibling); delete siblingSplitter; parentSplitter->setSizes(sizes); } qCDebug(SUBLIME) << "after deleation " << parent << " has " << parentSplitter->count() << " elements"; //find the container somewhere to activate Container *containerToActivate = parentSplitter->findChild(); //activate the current view there if (containerToActivate) { m_mainWindow->setActiveView(containerToActivate->viewForWidget(containerToActivate->currentWidget())); setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); return; } } } setTabBarLeftCornerWidget(m_leftTabbarCornerWidget.data()); if ( wasActive ) { m_mainWindow->setActiveView(nullptr); } } void MainWindowPrivate::toolViewAdded(Sublime::View* /*toolView*/, Sublime::Position position) { IdealToolViewCreator toolViewCreator(this); area->walkToolViews(toolViewCreator, position); } void MainWindowPrivate::aboutToRemoveToolView(Sublime::View *toolView, Sublime::Position /*position*/) { if (!docks.contains(toolView)) return; idealController->removeView(toolView); // TODO are Views unique? docks.removeAll(toolView); } void MainWindowPrivate::toolViewMoved( Sublime::View *toolView, Sublime::Position position) { if (!docks.contains(toolView)) return; idealController->moveView(toolView, positionToDockArea(position)); } Qt::DockWidgetArea MainWindowPrivate::positionToDockArea(Position position) { switch (position) { case Sublime::Left: return Qt::LeftDockWidgetArea; case Sublime::Right: return Qt::RightDockWidgetArea; case Sublime::Bottom: return Qt::BottomDockWidgetArea; case Sublime::Top: return Qt::TopDockWidgetArea; default: return Qt::LeftDockWidgetArea; } } void MainWindowPrivate::switchToArea(QAction *action) { qCDebug(SUBLIME) << "for" << action; controller->showArea(m_actionAreas.value(action), m_mainWindow); } void MainWindowPrivate::updateAreaSwitcher(Sublime::Area *area) { QAction* action = m_areaActions.value(area); if (action) action->setChecked(true); } void MainWindowPrivate::activateFirstVisibleView() { QList views = area->views(); if (views.count() > 0) m_mainWindow->activateView(views.first()); } void MainWindowPrivate::widgetResized(Qt::DockWidgetArea /*dockArea*/, int /*thickness*/) { //TODO: adymo: remove all thickness business } void MainWindowPrivate::widgetCloseRequest(QWidget* widget) { if (View *view = widgetToView.value(widget)) { area->closeView(view); } } void MainWindowPrivate::setTabBarLeftCornerWidget(QWidget* widget) { if(widget != m_leftTabbarCornerWidget.data()) { delete m_leftTabbarCornerWidget.data(); m_leftTabbarCornerWidget.clear(); } m_leftTabbarCornerWidget = widget; if(!widget || !area || viewContainers.isEmpty()) return; AreaIndex* putToIndex = area->rootIndex(); QSplitter* splitter = m_indexSplitters[putToIndex]; while(putToIndex->isSplit()) { putToIndex = putToIndex->first(); splitter = m_indexSplitters[putToIndex]; } // Q_ASSERT(splitter || putToIndex == area->rootIndex()); Container* c = nullptr; if(splitter) { c = qobject_cast(splitter->widget(0)); }else{ c = *viewContainers.constBegin(); } Q_ASSERT(c); c->setLeftCornerWidget(widget); } } #include "mainwindow_p.moc" #include "moc_mainwindow_p.cpp" diff --git a/kdevplatform/template/filters/kdevfilters.cpp b/kdevplatform/template/filters/kdevfilters.cpp index d81ae462b2..8f6112691c 100644 --- a/kdevplatform/template/filters/kdevfilters.cpp +++ b/kdevplatform/template/filters/kdevfilters.cpp @@ -1,180 +1,181 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdevfilters.h" #include #include #include #include #include using namespace KDevelop; static QString safeString(const QVariant& variant) { if (variant.canConvert()) { return variant.value().get(); } else { return variant.toString(); } } QStringList words(const QVariant& input) { QString string = safeString(input); if (string == string.toLower() && !string.contains(QLatin1Char('_'))) { return QStringList(string); } if (string.contains(QLatin1Char('_'))) { return string.toLower().split(QLatin1Char('_')); } int n = string.size(); QStringList ret; int last = 0; for (int i = 1; i < n; ++i) { if (string[i].isUpper()) { ret << string.mid(last, i-last).toLower(); last = i; } } ret << string.mid(last).toLower(); return ret; } QVariant CamelCaseFilter::doFilter(const QVariant& input, const QVariant& /*argument*/, bool /*autoescape*/) const { QString ret; foreach (const QString& word, words(input)) { QString w = word; w[0] = w[0].toUpper(); ret += w; } return Grantlee::SafeString(ret); } QVariant LowerCamelCaseFilter::doFilter(const QVariant& input, const QVariant& /*argument*/, bool /*autoescape*/) const { QString ret; foreach (const QString& word, words(input)) { QString w = word; w[0] = w[0].toUpper(); ret += w; } if (!ret.isEmpty()) { ret[0] = ret[0].toUpper(); } return Grantlee::SafeString(ret); } QVariant UnderscoreFilter::doFilter(const QVariant& input, const QVariant& /*argument*/, bool /*autoescape*/) const { QString ret = words(input).join(QLatin1Char('_')); return Grantlee::SafeString(ret); } QVariant UpperFirstFilter::doFilter(const QVariant& input, const QVariant& /*argument*/, bool /*autoescape*/) const { QString in = safeString(input); if (!in.isEmpty()) { in[0] = in[0].toUpper(); } return Grantlee::SafeString(in); } QVariant SplitLinesFilter::doFilter(const QVariant& input, const QVariant& argument, bool /*autoescape*/) const { QStringList retLines; QString start = safeString(argument); - foreach (const QString& line, safeString(input).split(QLatin1Char('\n'), QString::KeepEmptyParts)) - { + const auto lines = safeString(input).split(QLatin1Char('\n'), QString::KeepEmptyParts); + retLines.reserve(lines.size()); + for (const auto& line : lines) { retLines << start + line; } return Grantlee::SafeString(retLines.join(QLatin1Char('\n'))); } QVariant ArgumentTypeFilter::doFilter (const QVariant& input, const QVariant& /*argument*/, bool /*autoescape*/) const { QString type = safeString(input); DUChainReadLocker locker(DUChain::lock()); PersistentSymbolTable::Declarations decl = PersistentSymbolTable::self().getDeclarations(IndexedQualifiedIdentifier(QualifiedIdentifier(type))); for(PersistentSymbolTable::Declarations::Iterator it = decl.iterator(); it; ++it) { DeclarationPointer declaration = DeclarationPointer(it->declaration()); if(declaration->isForwardDeclaration()) { continue; } // Check if it's a class/struct/etc if(declaration->type()) { QString refType = QStringLiteral("const %1&").arg(type); return Grantlee::SafeString(refType); } } return Grantlee::SafeString(type); } KDevFilters::KDevFilters(QObject* parent, const QVariantList &) : QObject(parent) { } KDevFilters::~KDevFilters() { } QHash< QString, Grantlee::Filter* > KDevFilters::filters(const QString& name) { Q_UNUSED(name); QHash< QString, Grantlee::Filter* > filters; filters[QStringLiteral("camel_case")] = new CamelCaseFilter(); filters[QStringLiteral("camel_case_lower")] = new LowerCamelCaseFilter(); filters[QStringLiteral("underscores")] = new UnderscoreFilter(); filters[QStringLiteral("lines_prepend")] = new SplitLinesFilter(); filters[QStringLiteral("upper_first")] = new UpperFirstFilter(); filters[QStringLiteral("arg_type")] = new ArgumentTypeFilter(); return filters; } diff --git a/kdevplatform/tests/json/declarationvalidator.cpp b/kdevplatform/tests/json/declarationvalidator.cpp index a51ac9ab98..8908789f65 100644 --- a/kdevplatform/tests/json/declarationvalidator.cpp +++ b/kdevplatform/tests/json/declarationvalidator.cpp @@ -1,87 +1,87 @@ /* This file is part of KDevelop Copyright 2012 Olivier de Gaalon 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 "language/duchain/declaration.h" #include "declarationvalidator.h" #include "testsuite.h" #include "jsondeclarationtests.h" #include namespace KDevelop { class DeclarationValidatorPrivate { public: DeclarationValidatorPrivate() : testsPassed(true) {} bool testsPassed; }; QByteArray preprocess(QByteArray json) { int commentIndex = json.indexOf('#', 0); while (commentIndex > -1) { int commentEnd = json.indexOf('\n', commentIndex); if (commentEnd == -1) { json.truncate(commentIndex); break; } json.remove(commentIndex, commentEnd - commentIndex); commentIndex = json.indexOf('#', commentIndex); } - return json.prepend('{').append('}'); + return '{' + json + '}'; } DeclarationValidator::DeclarationValidator() : d(new DeclarationValidatorPrivate) { } DeclarationValidator::~DeclarationValidator() { } bool DeclarationValidator::testsPassed() { return d->testsPassed; } void DeclarationValidator::visit(DUContext*) { } void DeclarationValidator::visit(Declaration* declaration) { QJsonParseError error; const auto json = preprocess(declaration->comment()); QJsonDocument doc = QJsonDocument::fromJson(json, &error); if (error.error == 0) { QVariantMap testData = doc.toVariant().toMap(); if (!KDevelop::runTests(testData, declaration)) d->testsPassed = false; } else { d->testsPassed = false; QMessageLogger logger(declaration->topContext()->url().byteArray().constData(), declaration->range().start.line, nullptr); logger.warning() << "Error parsing JSON test data:" << error.errorString() << "at offset" << error.offset << "JSON input was:\n" << json; } } } diff --git a/kdevplatform/util/commandexecutor.cpp b/kdevplatform/util/commandexecutor.cpp index 7e6795fc7f..a5aba455a4 100644 --- a/kdevplatform/util/commandexecutor.cpp +++ b/kdevplatform/util/commandexecutor.cpp @@ -1,166 +1,167 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "commandexecutor.h" #include "processlinemaker.h" #include #include #include #include #include namespace KDevelop { class CommandExecutorPrivate { public: explicit CommandExecutorPrivate( CommandExecutor* cmd ) : m_exec(cmd), m_useShell(false) { } CommandExecutor* m_exec; KProcess* m_process; ProcessLineMaker* m_lineMaker; QString m_command; QStringList m_args; QString m_workDir; QMap m_env; bool m_useShell; void procError( QProcess::ProcessError error ) { Q_UNUSED(error) m_lineMaker->flushBuffers(); emit m_exec->failed( error ); } void procFinished( int code, QProcess::ExitStatus status ) { m_lineMaker->flushBuffers(); if( status == QProcess::NormalExit ) emit m_exec->completed(code); } }; CommandExecutor::CommandExecutor( const QString& command, QObject* parent ) : QObject(parent), d(new CommandExecutorPrivate(this)) { d->m_process = new KProcess(this); d->m_process->setOutputChannelMode( KProcess::SeparateChannels ); d->m_lineMaker = new ProcessLineMaker( d->m_process ); d->m_command = command; connect( d->m_lineMaker, &ProcessLineMaker::receivedStdoutLines, this, &CommandExecutor::receivedStandardOutput ); connect( d->m_lineMaker, &ProcessLineMaker::receivedStderrLines, this, &CommandExecutor::receivedStandardError ); connect( d->m_process, static_cast(&KProcess::error), this, [&] (QProcess::ProcessError error) { d->procError(error); } ); connect( d->m_process, static_cast(&KProcess::finished), this, [&] (int code, QProcess::ExitStatus status) { d->procFinished(code, status); } ); } CommandExecutor::~CommandExecutor() { delete d->m_process; delete d->m_lineMaker; } void CommandExecutor::setEnvironment( const QMap& env ) { d->m_env = env; } void CommandExecutor::setEnvironment( const QStringList& env ) { QMap envmap; foreach( const QString& var, env ) { int sep = var.indexOf(QLatin1Char('=')); envmap.insert( var.left( sep ), var.mid( sep + 1 ) ); } d->m_env = envmap; } void CommandExecutor::setArguments( const QStringList& args ) { d->m_args = args; } void CommandExecutor::setWorkingDirectory( const QString& dir ) { d->m_workDir = dir; } bool CommandExecutor::useShell() const { return d->m_useShell; } void CommandExecutor::setUseShell( bool shell ) { d->m_useShell = shell; } void CommandExecutor::start() { for(auto it = d->m_env.constBegin(), itEnd = d->m_env.constEnd(); it!=itEnd; ++it) { d->m_process->setEnv( it.key(), it.value() ); } d->m_process->setWorkingDirectory( d->m_workDir ); if( !d->m_useShell ) { d->m_process->setProgram( d->m_command, d->m_args ); } else { QStringList arguments; + arguments.reserve(d->m_args.size()); Q_FOREACH( const QString &a, d->m_args ) arguments << KShell::quoteArg( a ); d->m_process->setShellCommand(d->m_command + QLatin1Char(' ') + arguments.join(QLatin1Char(' '))); } d->m_process->start(); } void CommandExecutor::setCommand( const QString& command ) { d->m_command = command; } void CommandExecutor::kill() { d->m_process->kill(); } QString CommandExecutor::command() const { return d->m_command; } QStringList CommandExecutor::arguments() const { return d->m_args; } QString CommandExecutor::workingDirectory() const { return d->m_workDir; } } #include "moc_commandexecutor.cpp" diff --git a/kdevplatform/util/environmentprofilelist.cpp b/kdevplatform/util/environmentprofilelist.cpp index 8a14fd3a17..8de6f91aa9 100644 --- a/kdevplatform/util/environmentprofilelist.cpp +++ b/kdevplatform/util/environmentprofilelist.cpp @@ -1,219 +1,220 @@ /* This file is part of KDevelop Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "environmentprofilelist.h" #include #include #include #include #include #include namespace KDevelop { class EnvironmentProfileListPrivate { public: QMap > m_profiles; QString m_defaultProfileName; }; } using namespace KDevelop; namespace { namespace Strings { // TODO: migrate to more consistent key term "Default Environment Profile" inline QString defaultEnvProfileKey() { return QStringLiteral("Default Environment Group"); } inline QString envGroup() { return QStringLiteral("Environment Settings"); } // TODO: migrate to more consistent key term "Profile List" inline QString profileListKey() { return QStringLiteral("Group List"); } inline QString defaultProfileName() { return QStringLiteral("default"); } } void decode(KConfig* config, EnvironmentProfileListPrivate* d) { KConfigGroup cfg( config, Strings::envGroup() ); d->m_defaultProfileName = cfg.readEntry(Strings::defaultEnvProfileKey(), Strings::defaultProfileName()); const QStringList profileNames = cfg.readEntry(Strings::profileListKey(), QStringList{Strings::defaultProfileName()}); for (const auto& profileName : profileNames) { KConfigGroup envgrp(&cfg, profileName); QMap variables; foreach( const QString &varname, envgrp.keyList() ) { variables[varname] = envgrp.readEntry( varname, QString() ); } d->m_profiles.insert(profileName, variables); } } void encode(KConfig* config, EnvironmentProfileListPrivate* d) { KConfigGroup cfg( config, Strings::envGroup() ); cfg.writeEntry(Strings::defaultEnvProfileKey(), d->m_defaultProfileName); cfg.writeEntry(Strings::profileListKey(), d->m_profiles.keys()); foreach( const QString &group, cfg.groupList() ) { if (!d->m_profiles.contains(group)) { cfg.deleteGroup( group ); } } for (auto it = d->m_profiles.cbegin(), itEnd = d->m_profiles.cend(); it!=itEnd; ++it) { KConfigGroup envgrp( &cfg, it.key() ); envgrp.deleteGroup(); const auto val = it.value(); for(auto it2 = val.cbegin(), it2End = val.cend(); it2!=it2End; ++it2) { envgrp.writeEntry( it2.key(), *it2 ); } } cfg.sync(); } } EnvironmentProfileList::EnvironmentProfileList(const EnvironmentProfileList& rhs) : d(new EnvironmentProfileListPrivate(*rhs.d)) { } EnvironmentProfileList& EnvironmentProfileList::operator=(const EnvironmentProfileList& rhs) { *d = *rhs.d; return *this; } EnvironmentProfileList::EnvironmentProfileList(const KSharedConfigPtr& config) : d( new EnvironmentProfileListPrivate ) { decode(config.data(), d.data()); } EnvironmentProfileList::EnvironmentProfileList(KConfig* config) : d(new EnvironmentProfileListPrivate) { decode(config, d.data()); } EnvironmentProfileList::~EnvironmentProfileList() = default; QMap EnvironmentProfileList::variables(const QString& profileName) const { return d->m_profiles[profileName.isEmpty() ? d->m_defaultProfileName : profileName]; } QMap& EnvironmentProfileList::variables(const QString& profileName) { return d->m_profiles[profileName.isEmpty() ? d->m_defaultProfileName : profileName]; } QString EnvironmentProfileList::defaultProfileName() const { return d->m_defaultProfileName; } void EnvironmentProfileList::setDefaultProfile(const QString& profileName) { if (profileName.isEmpty() || !d->m_profiles.contains(profileName)) { return; } d->m_defaultProfileName = profileName; } void EnvironmentProfileList::saveSettings(KConfig* config) const { encode(config, d.data()); config->sync(); } void EnvironmentProfileList::loadSettings(KConfig* config) { d->m_profiles.clear(); decode(config, d.data()); } QStringList EnvironmentProfileList::profileNames() const { return d->m_profiles.keys(); } void EnvironmentProfileList::removeProfile(const QString& profileName) { d->m_profiles.remove(profileName); } EnvironmentProfileList::EnvironmentProfileList() : d(new EnvironmentProfileListPrivate) { } QStringList EnvironmentProfileList::createEnvironment(const QString& profileName, const QStringList& defaultEnvironment) const { QMap retMap; foreach( const QString &line, defaultEnvironment ) { QString varName = line.section(QLatin1Char('='), 0, 0); QString varValue = line.section(QLatin1Char('='), 1); retMap.insert( varName, varValue ); } if (!profileName.isEmpty()) { const auto userMap = variables(profileName); for( QMap::const_iterator it = userMap.constBegin(); it != userMap.constEnd(); ++it ) { retMap.insert( it.key(), it.value() ); } } QStringList env; + env.reserve(retMap.size()); for( QMap::const_iterator it = retMap.constBegin(); it != retMap.constEnd(); ++it ) { env << it.key() + QLatin1Char('=') + it.value(); } return env; } void KDevelop::expandVariables(QMap& variables, const QProcessEnvironment& environment) { QRegularExpression rVar(QStringLiteral("(? 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "formattinghelpers.h" #include "debug.h" #include namespace KDevelop { ///Matches the given prefix to the given text, ignoring all whitespace ///Returns -1 if mismatched, else the position in @p text where the @p prefix match ends -int matchPrefixIgnoringWhitespace(QString text, QString prefix, const QString& fuzzyCharacters) +int matchPrefixIgnoringWhitespace(const QString& text, const QString& prefix, const QString& fuzzyCharacters) { int prefixPos = 0; int textPos = 0; while (prefixPos < prefix.length() && textPos < text.length()) { skipWhiteSpace: while (prefixPos < prefix.length() && prefix[prefixPos].isSpace()) ++prefixPos; while (textPos < text.length() && text[textPos].isSpace()) ++textPos; if(prefixPos == prefix.length() || textPos == text.length()) break; if(prefix[prefixPos] != text[textPos]) { bool skippedFuzzy = false; while( prefixPos < prefix.length() && fuzzyCharacters.indexOf(prefix[prefixPos]) != -1 ) { ++prefixPos; skippedFuzzy = true; } while( textPos < text.length() && fuzzyCharacters.indexOf(text[textPos]) != -1 ) { ++textPos; skippedFuzzy = true; } if( skippedFuzzy ) goto skipWhiteSpace; return -1; } ++prefixPos; ++textPos; } return textPos; } static QString reverse( const QString& str ) { QString ret; + ret.reserve(str.length()); for(int a = str.length()-1; a >= 0; --a) ret.append(str[a]); return ret; } // Returns the text start position with all whitespace that is redundant in the given context skipped -int skipRedundantWhiteSpace( QString context, QString text, int tabWidth ) +int skipRedundantWhiteSpace(const QString& context, const QString& text, int tabWidth) { if( context.isEmpty() || !context[context.size()-1].isSpace() || text.isEmpty() || !text[0].isSpace() ) return 0; int textPosition = 0; // Extract trailing whitespace in the context int contextPosition = context.size()-1; while( contextPosition > 0 && context[contextPosition-1].isSpace() ) --contextPosition; int textWhitespaceEnd = 0; while(textWhitespaceEnd < text.size() && text[textWhitespaceEnd].isSpace()) ++textWhitespaceEnd; QString contextWhiteSpace = context.mid(contextPosition); contextPosition = 0; QString textWhiteSpace = text.left(textWhitespaceEnd); // Step 1: Remove redundant newlines while(contextWhiteSpace.contains(QLatin1Char('\n')) && textWhiteSpace.contains(QLatin1Char('\n'))) { int contextOffset = contextWhiteSpace.indexOf(QLatin1Char('\n'))+1; int textOffset = textWhiteSpace.indexOf(QLatin1Char('\n'))+1; contextPosition += contextOffset; contextWhiteSpace.remove(0, contextOffset); textPosition += textOffset; textWhiteSpace.remove(0, textOffset); } int contextOffset = 0; int textOffset = 0; // Skip redundant ordinary whitespace while(contextOffset < contextWhiteSpace.size() && textOffset < textWhiteSpace.size() && contextWhiteSpace[contextOffset].isSpace() && contextWhiteSpace[contextOffset] != QLatin1Char('\n') && textWhiteSpace[textOffset].isSpace() && textWhiteSpace[textOffset] != QLatin1Char('\n')) { bool contextWasTab = contextWhiteSpace[contextOffset] == QLatin1Char('\t'); bool textWasTab = textWhiteSpace[contextOffset] == QLatin1Char('\t'); ++contextOffset; ++textOffset; if( contextWasTab != textWasTab ) { // Problem: We have a mismatch of tabs and/or ordinary whitespaces if( contextWasTab ) { for( int s = 1; s < tabWidth; ++s ) if (textOffset < textWhiteSpace.size() && textWhiteSpace[textOffset] == QLatin1Char(' ')) ++textOffset; }else if( textWasTab ) { for( int s = 1; s < tabWidth; ++s ) if (contextOffset < contextWhiteSpace.size() && contextWhiteSpace[contextOffset] == QLatin1Char(' ')) ++contextOffset; } } } return textPosition+textOffset; } QString extractFormattedTextFromContext( const QString& _formattedMergedText, const QString& text, const QString& leftContext, const QString& rightContext, int tabWidth, const QString& fuzzyCharacters) { QString formattedMergedText = _formattedMergedText; //Now remove "leftContext" and "rightContext" from the sides if(!leftContext.isEmpty()) { int endOfLeftContext = matchPrefixIgnoringWhitespace( formattedMergedText, leftContext, QString() ); if(endOfLeftContext == -1) { // Try 2: Ignore the fuzzy characters while matching endOfLeftContext = matchPrefixIgnoringWhitespace( formattedMergedText, leftContext, fuzzyCharacters ); if(endOfLeftContext == -1) { qCWarning(UTIL) << "problem matching the left context"; return text; } } int startOfWhiteSpace = endOfLeftContext; // Include all leading whitespace while(startOfWhiteSpace > 0 && formattedMergedText[startOfWhiteSpace-1].isSpace()) --startOfWhiteSpace; formattedMergedText = formattedMergedText.mid(startOfWhiteSpace); int skip = skipRedundantWhiteSpace( leftContext, formattedMergedText, tabWidth ); formattedMergedText = formattedMergedText.mid(skip); } if(!rightContext.isEmpty()) { //Add a whitespace behind the text for matching, so that we definitely capture all trailing whitespace int endOfText = matchPrefixIgnoringWhitespace(formattedMergedText, text + QLatin1Char(' '), QString()); if(endOfText == -1) { // Try 2: Ignore the fuzzy characters while matching endOfText = matchPrefixIgnoringWhitespace(formattedMergedText, text + QLatin1Char(' '), fuzzyCharacters); if(endOfText == -1) { qCWarning(UTIL) << "problem matching the text while formatting"; return text; } } formattedMergedText = formattedMergedText.left(endOfText); int skip = skipRedundantWhiteSpace( reverse(rightContext), reverse(formattedMergedText), tabWidth ); formattedMergedText = formattedMergedText.left(formattedMergedText.size() - skip); } return formattedMergedText; } } diff --git a/kdevplatform/util/kdevstringhandler.cpp b/kdevplatform/util/kdevstringhandler.cpp index 4a9211ca50..198d4a1186 100644 --- a/kdevplatform/util/kdevstringhandler.cpp +++ b/kdevplatform/util/kdevstringhandler.cpp @@ -1,246 +1,244 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. This file mostly code takes from Qt's QSettings class, the copyright header from that file follows: **************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial Usage ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain ** additional rights. These rights are described in the Nokia Qt LGPL ** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this ** package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://www.qtsoftware.com/contact. ** $QT_END_LICENSE$ ** **************************************************************************** */ #include "kdevstringhandler.h" #include #include #include #include #include #include #include #include namespace KDevelop { QString joinWithEscaping( const QStringList& input, const QChar& joinchar, const QChar& escapechar ) { QStringList tmp = input; return tmp.replaceInStrings( joinchar, QString( joinchar ) + QString( escapechar ) ).join( joinchar ); } QStringList splitWithEscaping( const QString& input, const QChar& splitchar, const QChar& escapechar ) { enum State { Normal, SeenEscape } state; state = Normal; QStringList result; QString currentstring; for( int i = 0; i < input.size(); i++ ) { switch( state ) { case Normal: if( input[i] == escapechar ) { state = SeenEscape; } else if( input[i] == splitchar ) { result << currentstring; currentstring.clear(); } else { currentstring += input[i]; } break; case SeenEscape: currentstring += input[i]; state = Normal; break; } } if( !currentstring.isEmpty() ) { result << currentstring; } return result; } QVariant stringToQVariant(const QString& s) { // Taken from qsettings.cpp, stringToVariant() if (s.startsWith(QLatin1Char('@'))) { if (s.endsWith(QLatin1Char(')'))) { if (s.startsWith(QLatin1String("@Variant("))) { QByteArray a(s.toLatin1().mid(9)); QDataStream stream(&a, QIODevice::ReadOnly); stream.setVersion(QDataStream::Qt_4_4); QVariant result; stream >> result; return result; } } } return QVariant(); } QString qvariantToString(const QVariant& variant) { // Taken from qsettings.cpp, variantToString() QByteArray a; { QDataStream s(&a, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_4); s << variant; } - QString result = QStringLiteral("@Variant("); - result += QString::fromLatin1(a.constData(), a.size()); - result += QLatin1Char(')'); + QString result = QStringLiteral("@Variant(") + QString::fromLatin1(a.constData(), a.size()) + QLatin1Char(')'); return result; } QString htmlToPlainText(const QString& s, HtmlToPlainTextMode mode) { switch (mode) { case FastMode: { QString result(s); result.remove(QRegExp(QStringLiteral("<[^>]+>"))); return result; } case CompleteMode: { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } } return QString(); // never reached } } QString KDevelop::stripAnsiSequences(const QString& str) { if (str.isEmpty()) { return QString(); // fast path } enum { PLAIN, ANSI_START, ANSI_CSI, ANSI_SEQUENCE, ANSI_WAITING_FOR_ST, ANSI_ST_STARTED } state = PLAIN; QString result; result.reserve(str.count()); foreach (const QChar c, str) { const auto val = c.unicode(); switch (state) { case PLAIN: if (val == 27) // 'ESC' state = ANSI_START; else if (val == 155) // equivalent to 'ESC'-'[' state = ANSI_CSI; else result.append(c); break; case ANSI_START: if (val == 91) // [ state = ANSI_CSI; else if (val == 80 || val == 93 || val == 94 || val == 95) // 'P', ']', '^' and '_' state = ANSI_WAITING_FOR_ST; else if (val >= 64 && val <= 95) state = PLAIN; else state = ANSI_SEQUENCE; break; case ANSI_CSI: if (val >= 64 && val <= 126) // Anything between '@' and '~' state = PLAIN; break; case ANSI_SEQUENCE: if (val >= 64 && val <= 95) // Anything between '@' and '_' state = PLAIN; break; case ANSI_WAITING_FOR_ST: if (val == 7) // 'BEL' state = PLAIN; else if (val == 27) // 'ESC' state = ANSI_ST_STARTED; break; case ANSI_ST_STARTED: if (val == 92) // '\' state = PLAIN; else state = ANSI_WAITING_FOR_ST; break; } } return result; } void KDevelop::normalizeLineEndings(QByteArray& text) { for (int i = 0, s = text.size(); i < s; ++i) { if (text[i] != '\r') { continue; } if (i + 1 < s && text[i + 1] == '\n') { text.remove(i, 1); } else { text[i] = '\n'; } } } diff --git a/kdevplatform/util/multilevellistview.cpp b/kdevplatform/util/multilevellistview.cpp index b4b47f7795..783c29973a 100644 --- a/kdevplatform/util/multilevellistview.cpp +++ b/kdevplatform/util/multilevellistview.cpp @@ -1,463 +1,466 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "multilevellistview.h" #include #include #include #include /** * Interface to set the label of a model. */ class LabeledProxy { public: virtual ~LabeledProxy() { } void setLabel(const QString& label) { m_label = label; } QVariant header(QAbstractItemModel* model, int section, Qt::Orientation orientation, int role) const { if (model && section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { return m_label; } else { return QVariant(); } } protected: QString m_label; }; /** * The left-most view's model which only contains the root nodes of the source model. */ class RootProxyModel : public QSortFilterProxyModel, public LabeledProxy { Q_OBJECT public: explicit RootProxyModel( QObject* parent = nullptr ) : QSortFilterProxyModel( parent ) { } bool filterAcceptsRow( int /*source_row*/, const QModelIndex& source_parent ) const override { return !source_parent.isValid(); } QVariant headerData( int section, Qt::Orientation orientation, int role ) const override { return header(sourceModel(), section, orientation, role); } }; /** * A class that automatically updates its contents based on the selection in another view. */ class SubTreeProxyModel : public KSelectionProxyModel, public LabeledProxy { Q_OBJECT public: explicit SubTreeProxyModel( QItemSelectionModel* selectionModel, QObject* parent = nullptr ) : KSelectionProxyModel( selectionModel, parent ) {} QVariant headerData( int section, Qt::Orientation orientation, int role ) const override { return header(sourceModel(), section, orientation, role); } Qt::ItemFlags flags(const QModelIndex& index) const override { Qt::ItemFlags ret = KSelectionProxyModel::flags(index); if (filterBehavior() == KSelectionProxyModel::SubTreesWithoutRoots && hasChildren(index)) { // we want to select child items ret &= ~Qt::ItemIsSelectable; } return ret; } }; using namespace KDevelop; class KDevelop::MultiLevelListViewPrivate { public: explicit MultiLevelListViewPrivate(MultiLevelListView* view); ~MultiLevelListViewPrivate(); void viewSelectionChanged(const QModelIndex& current, const QModelIndex& previous); void lastViewsContentsChanged(); void ensureViewSelected(QTreeView* view); /** * @param index index in any of our proxy models * @return an index in the source model */ QModelIndex mapToSource(QModelIndex index); /** * @param index an index in the source model * @return an index in the view's model at level @p level */ QModelIndex mapFromSource(QModelIndex index, int level); MultiLevelListView* view; int levels; QList views; QList proxies; QList layouts; QAbstractItemModel* model; }; MultiLevelListViewPrivate::MultiLevelListViewPrivate(MultiLevelListView* view_) : view(view_) , levels(0) , model(nullptr) { } MultiLevelListViewPrivate::~MultiLevelListViewPrivate() { } void MultiLevelListViewPrivate::viewSelectionChanged(const QModelIndex& current, const QModelIndex& previous) { if (!current.isValid()) { // ignore, as we should always have some kind of selection return; } // figure out which proxy this signal belongs to QAbstractProxyModel* proxy = qobject_cast( const_cast(current.model())); Q_ASSERT(proxy); // what level is this proxy in int level = -1; for(int i = 0; i < levels; ++i) { if (views.at(i)->model() == proxy) { level = i; break; } } Q_ASSERT(level >= 0 && level < levels); if (level + 1 == levels) { // right-most view if (proxy->hasIndex(0, 0, current)) { // select the first leaf node for this view QModelIndex idx = current; QModelIndex child = proxy->index(0, 0, idx); while(child.isValid()) { idx = child; child = proxy->index(0, 0, idx); } views.last()->setCurrentIndex(idx); return; } // signal that our actual selection has changed emit view->currentIndexChanged(mapToSource(current), mapToSource(previous)); } else { // some leftish view // ensure the next view's first item is selected QTreeView* treeView = views.at(level + 1); // we need to delay the call, because at this point the child view // will still have its old data which is going to be invalidated // right after this method exits // be we must not set the index to 0,0 here directly, since e.g. // MultiLevelListView::setCurrentIndex might have been used, which // sets a proper index already. QMetaObject::invokeMethod(view, "ensureViewSelected", Qt::QueuedConnection, Q_ARG(QTreeView*, treeView)); } } void MultiLevelListViewPrivate::lastViewsContentsChanged() { views.last()->expandAll(); } void MultiLevelListViewPrivate::ensureViewSelected(QTreeView* view) { if (!view->currentIndex().isValid()) { view->setCurrentIndex(view->model()->index(0, 0)); } } QModelIndex MultiLevelListViewPrivate::mapToSource(QModelIndex index) { if (!index.isValid()) { return index; } while(index.model() != model) { QAbstractProxyModel* proxy = qobject_cast( const_cast(index.model())); Q_ASSERT(proxy); index = proxy->mapToSource(index); Q_ASSERT(index.isValid()); } return index; } QModelIndex MultiLevelListViewPrivate::mapFromSource(QModelIndex index, int level) { if (!index.isValid()) { return index; } Q_ASSERT(index.model() == model); QAbstractProxyModel* proxy = qobject_cast(views.at(level)->model()); Q_ASSERT(proxy); // find all proxies between the source and our view QVector proxies; proxies << proxy; forever { QAbstractProxyModel* child = qobject_cast(proxy->sourceModel()); if (child) { proxy = child; proxies << proxy; } else { Q_ASSERT(proxy->sourceModel() == model); break; } } // iterate in reverse order to find the view's index for(int i = proxies.size() - 1; i >= 0; --i) { proxy = proxies.at(i); index = proxy->mapFromSource(index); Q_ASSERT(index.isValid()); } return index; } MultiLevelListView::MultiLevelListView(QWidget* parent, Qt::WindowFlags f) : QWidget(parent, f) , d(new MultiLevelListViewPrivate(this)) { setLayout(new QHBoxLayout()); layout()->setContentsMargins(0, 0, 0, 0); qRegisterMetaType("QTreeView*"); } MultiLevelListView::~MultiLevelListView() = default; int MultiLevelListView::levels() const { return d->levels; } void MultiLevelListView::setLevels(int levels) { qDeleteAll(d->views); qDeleteAll(d->proxies); qDeleteAll(d->layouts); d->views.clear(); d->proxies.clear(); d->layouts.clear(); d->levels = levels; + d->views.reserve(levels); + d->proxies.reserve(levels); + d->layouts.reserve(levels); QTreeView* previousView = nullptr; for (int i = 0; i < d->levels; ++i) { QVBoxLayout* levelLayout = new QVBoxLayout(); QTreeView* view = new QTreeView(this); view->setContentsMargins(0, 0, 0, 0); // only the right-most view is decorated view->setRootIsDecorated(i + 1 == d->levels); view->setHeaderHidden(false); view->setSelectionMode(QAbstractItemView::SingleSelection); if (!previousView) { // the root, i.e. left-most view RootProxyModel* root = new RootProxyModel(this); root->setDynamicSortFilter(true); d->proxies << root; root->setSourceModel(d->model); view->setModel(root); } else { SubTreeProxyModel* subTreeProxy = new SubTreeProxyModel(previousView->selectionModel(), this); if (i + 1 < d->levels) { // middel views only shows children of selection subTreeProxy->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection); } else { // right-most view shows the rest subTreeProxy->setFilterBehavior(KSelectionProxyModel::SubTreesWithoutRoots); } d->proxies << subTreeProxy; subTreeProxy->setSourceModel(d->model); // sorting requires another proxy in-between QSortFilterProxyModel* sortProxy = new QSortFilterProxyModel(subTreeProxy); sortProxy->setSourceModel(subTreeProxy); sortProxy->setDynamicSortFilter(true); view->setModel(sortProxy); } // view->setModel creates the selection model connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, [&] (const QModelIndex& current, const QModelIndex& previous) { d->viewSelectionChanged(current, previous); }); if (i + 1 == d->levels) { connect(view->model(), &QAbstractItemModel::rowsInserted, this, [&] { d->lastViewsContentsChanged(); }); } view->setSortingEnabled(true); view->sortByColumn(0, Qt::AscendingOrder); levelLayout->addWidget(view); layout()->addItem(levelLayout); d->layouts << levelLayout; d->views << view; previousView = view; } setModel(d->model); } QAbstractItemModel* MultiLevelListView::model() const { return d->model; } void MultiLevelListView::setModel(QAbstractItemModel* model) { d->model = model; foreach (LabeledProxy* proxy, d->proxies) { dynamic_cast(proxy)->setSourceModel(model); } if (model && !d->views.isEmpty()) { d->views.first()->setCurrentIndex(d->views.first()->model()->index(0, 0)); } } QTreeView* MultiLevelListView::viewForLevel( int level ) const { return d->views[level]; } void MultiLevelListView::addWidget(int level, QWidget* widget) { Q_ASSERT(level < d->levels); d->layouts[level]->addWidget(widget); } QModelIndex MultiLevelListView::currentIndex() const { return d->mapToSource(d->views.last()->currentIndex()); } void MultiLevelListView::setCurrentIndex(const QModelIndex& index) { // incoming index is for the original model Q_ASSERT(!index.isValid() || index.model() == d->model); const QModelIndex previous = currentIndex(); QModelIndex idx(index); QVector indexes; while (idx.isValid()) { indexes.prepend(idx); idx = idx.parent(); } for (int i = 0; i < d->levels; ++i) { QTreeView* view = d->views.at(i); if (indexes.size() <= i) { // select first item by default view->setCurrentIndex(view->model()->index(0, 0)); continue; } QModelIndex index; if (i + 1 == d->levels) { // select the very last index in the list (i.e. might be deep down in the actual tree) index = indexes.last(); } else { // select the first index for that level index = indexes.at(i); } view->setCurrentIndex(d->mapFromSource(index, i)); } emit currentIndexChanged(index, previous); } void MultiLevelListView::setRootIndex(const QModelIndex& index) { Q_ASSERT(!index.isValid() || index.model() == d->model); d->views.first()->setRootIndex(index); } void MultiLevelListView::setHeaderLabels(const QStringList& labels) { int n = qMin(d->levels, labels.size()); for (int i = 0; i < n; ++i) { d->proxies.at(i)->setLabel(labels[i]); } } static KSelectionProxyModel::FilterBehavior toSelectionProxyModelFilterBehavior(MultiLevelListView::LastLevelViewMode mode) { switch (mode) { case MultiLevelListView::SubTrees: return KSelectionProxyModel::SubTreesWithoutRoots; case MultiLevelListView::DirectChildren: return KSelectionProxyModel::ChildrenOfExactSelection; } Q_UNREACHABLE(); } void MultiLevelListView::setLastLevelViewMode(LastLevelViewMode mode) { if (d->proxies.isEmpty()) { return; } const auto filterBehavior = toSelectionProxyModelFilterBehavior(mode); dynamic_cast(d->proxies.last())->setFilterBehavior(filterBehavior); } #include "multilevellistview.moc" #include "moc_multilevellistview.cpp" diff --git a/kdevplatform/util/path.cpp b/kdevplatform/util/path.cpp index 3d700b4cad..a11d98bc0b 100644 --- a/kdevplatform/util/path.cpp +++ b/kdevplatform/util/path.cpp @@ -1,513 +1,511 @@ /* * This file is part of KDevelop * Copyright 2012 Milian Wolff * Copyright 2015 Kevin Funk * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "path.h" #include #include #include using namespace KDevelop; namespace { inline bool isWindowsDriveLetter(const QString& segment) { #ifdef Q_OS_WIN return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == QLatin1Char(':'); #else Q_UNUSED(segment); return false; #endif } inline bool isAbsolutePath(const QString& path) { if (path.startsWith(QLatin1Char('/'))) { return true; // Even on Windows: Potentially a path of a remote URL } #ifdef Q_OS_WIN return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == QLatin1Char(':'); #else return false; #endif } } QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options) { const auto str = url.toString(options | QUrl::PreferLocalFile); #ifdef Q_OS_WIN // potentially strip leading slash if (url.isLocalFile() && !str.isEmpty() && str[0] == QLatin1Char('/')) { return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first } #endif return str; } Path::Path() { } Path::Path(const QString& pathOrUrl) : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution)) { } Path::Path(const QUrl& url) { if (!url.isValid()) { // empty or invalid Path return; } // we do not support urls with: // - fragments // - sub urls // - query // nor do we support relative urls if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) { // invalid qWarning("Path::init: invalid/unsupported Path encountered: \"%s\"", qPrintable(url.toDisplayString(QUrl::PreferLocalFile))); return; } if (!url.isLocalFile()) { // handle remote urls - QString urlPrefix; - urlPrefix += url.scheme(); - urlPrefix += QLatin1String("://"); + QString urlPrefix = url.scheme() + QLatin1String("://"); const QString user = url.userName(); if (!user.isEmpty()) { - urlPrefix += user; - urlPrefix += QLatin1Char('@'); + urlPrefix += user + QLatin1Char('@'); } urlPrefix += url.host(); if (url.port() != -1) { urlPrefix += QLatin1Char(':') + QString::number(url.port()); } m_data << urlPrefix; } addPath(url.isLocalFile() ? url.toLocalFile() : url.path()); // support for root paths, they are valid but don't really contain any data if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) { m_data << QString(); } } Path::Path(const Path& other, const QString& child) : m_data(other.m_data) { if (isAbsolutePath(child)) { // absolute path: only share the remote part of @p other m_data.resize(isRemote() ? 1 : 0); } else if (!other.isValid() && !child.isEmpty()) { qWarning("Path::Path: tried to append relative path \"%s\" to invalid base", qPrintable(child)); return; } addPath(child); } static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector& data) { // more or less a copy of QtPrivate::QStringList_join const int size = data.size(); if (size == 0) { return QString(); } int totalLength = 0; // separators: '/' totalLength += size; // skip Path segment if we only want the path int start = (onlyPath && !isLocalFile) ? 1 : 0; // path and url prefix for (int i = start; i < size; ++i) { totalLength += data.at(i).size(); } // build string representation QString res; res.reserve(totalLength); #ifdef Q_OS_WIN if (start == 0 && isLocalFile) { Q_ASSERT(data.at(0).endsWith(QLatin1Char(':'))); // assume something along "C:" res += data.at(0); start++; } #endif for (int i = start; i < size; ++i) { if (i || isLocalFile) { res += QLatin1Char('/'); } res += data.at(i); } return res; } QString Path::pathOrUrl() const { return generatePathOrUrl(false, isLocalFile(), m_data); } QString Path::path() const { return generatePathOrUrl(true, isLocalFile(), m_data); } QString Path::toLocalFile() const { return isLocalFile() ? path() : QString(); } QString Path::relativePath(const Path& path) const { if (!path.isValid()) { return QString(); } if (!isValid() || remotePrefix() != path.remotePrefix()) { // different remote destinations or we are invalid, return input as-is return path.pathOrUrl(); } // while I'd love to use QUrl::relativePath here, it seems to behave pretty // strangely, and adds unexpected "./" at the start for example // so instead, do it on our own based on _relativePath in kurl.cpp // this should also be more performant I think // Find where they meet int level = isRemote() ? 1 : 0; const int maxLevel = qMin(m_data.count(), path.m_data.count()); while(level < maxLevel && m_data.at(level) == path.m_data.at(level)) { ++level; } // Need to go down out of our path to the common branch. // but keep in mind that e.g. '/' paths have an empty name int backwardSegments = m_data.count() - level; if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) { --backwardSegments; } // Now up up from the common branch to the second path. int forwardSegmentsLength = 0; for (int i = level; i < path.m_data.count(); ++i) { forwardSegmentsLength += path.m_data.at(i).length(); // slashes if (i + 1 != path.m_data.count()) { forwardSegmentsLength += 1; } } QString relativePath; relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength); for(int i = 0; i < backwardSegments; ++i) { relativePath.append(QLatin1String("../")); } for (int i = level; i < path.m_data.count(); ++i) { relativePath.append(path.m_data.at(i)); if (i + 1 != path.m_data.count()) { relativePath.append(QLatin1Char('/')); } } Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength)); return relativePath; } static bool isParentPath(const QVector& parent, const QVector& child, bool direct) { if (direct && child.size() != parent.size() + 1) { return false; } else if (!direct && child.size() <= parent.size()) { return false; } for (int i = 0; i < parent.size(); ++i) { if (child.at(i) != parent.at(i)) { // support for trailing '/' if (i + 1 == parent.size() && parent.at(i).isEmpty()) { return true; } // otherwise we take a different branch here return false; } } return true; } bool Path::isParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, false); } bool Path::isDirectParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, true); } QString Path::remotePrefix() const { return isRemote() ? m_data.first() : QString(); } bool Path::operator<(const Path& other) const { const int size = m_data.size(); const int otherSize = other.m_data.size(); const int toCompare = qMin(size, otherSize); // compare each Path segment in turn and try to return early for (int i = 0; i < toCompare; ++i) { int comparison = m_data.at(i).compare(other.m_data.at(i)); if (comparison == 0) { // equal, try next segment continue; } else { // return whether our segment is less then the other one return comparison < 0; } } // when we reach this point, all elements that we compared where equal // thus return whether we have less items than the other Path return size < otherSize; } QUrl Path::toUrl() const { return QUrl::fromUserInput(pathOrUrl()); } bool Path::isLocalFile() const { // if the first data element contains a '/' it is a Path prefix return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/')); } bool Path::isRemote() const { return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/')); } QString Path::lastPathSegment() const { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { return QString(); } return m_data.last(); } void Path::setLastPathSegment(const QString& name) { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { // append the name to empty Paths or remote Paths only containing the Path prefix m_data.append(name); } else { // overwrite the last data member m_data.last() = name; } } static void cleanPath(QVector* data, const bool isRemote) { if (data->isEmpty()) { return; } const int startOffset = isRemote ? 1 : 0; const auto start = data->begin() + startOffset; auto it = start; while(it != data->end()) { if (*it == QLatin1String("..")) { if (it == start) { it = data->erase(it); } else { if (isWindowsDriveLetter(*(it - 1))) { it = data->erase(it); // keep the drive letter } else { it = data->erase(it - 1, it + 1); } } } else if (*it == QLatin1String(".")) { it = data->erase(it); } else { ++it; } } if (data->count() == startOffset) { data->append(QString()); } } // Optimized QString::split code for the specific Path use-case static QVarLengthArray splitPath(const QString &source) { QVarLengthArray list; int start = 0; int end = 0; while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) { if (start != end) { list.append(source.mid(start, end - start)); } start = end + 1; } if (start != source.size()) { list.append(source.mid(start, -1)); } return list; } void Path::addPath(const QString& path) { if (path.isEmpty()) { return; } const auto& newData = splitPath(path); if (newData.isEmpty()) { if (m_data.size() == (isRemote() ? 1 : 0)) { // this represents the root path, we just turned an invalid path into it m_data << QString(); } return; } auto it = newData.begin(); if (!m_data.isEmpty() && m_data.last().isEmpty()) { // the root item is empty, set its contents and continue appending m_data.last() = *it; ++it; } std::copy(it, newData.end(), std::back_inserter(m_data)); cleanPath(&m_data, isRemote()); } Path Path::parent() const { if (m_data.isEmpty()) { return Path(); } Path ret(*this); if (m_data.size() == (1 + (isRemote() ? 1 : 0))) { // keep the root item, but clear it, otherwise we'd make the path invalid // or a URL a local path auto& root = ret.m_data.last(); if (!isWindowsDriveLetter(root)) { root.clear(); } } else { ret.m_data.pop_back(); } return ret; } bool Path::hasParent() const { const int rootIdx = isRemote() ? 1 : 0; return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty(); } void Path::clear() { m_data.clear(); } Path Path::cd(const QString& dir) const { if (!isValid()) { return Path(); } return Path(*this, dir); } namespace KDevelop { uint qHash(const Path& path) { KDevHash hash; foreach (const QString& segment, path.segments()) { hash << qHash(segment); } return hash; } template static Path::List toPathList_impl(const Container& list) { Path::List ret; ret.reserve(list.size()); foreach (const auto& entry, list) { Path path(entry); if (path.isValid()) { ret << path; } } + ret.squeeze(); return ret; } Path::List toPathList(const QList& list) { return toPathList_impl(list); } Path::List toPathList(const QList< QString >& list) { return toPathList_impl(list); } } QDebug operator<<(QDebug s, const Path& string) { s.nospace() << string.pathOrUrl(); return s.space(); } namespace QTest { template<> char *toString(const Path &path) { return qstrdup(qPrintable(path.pathOrUrl())); } } diff --git a/kdevplatform/vcs/models/vcsfilechangesmodel.cpp b/kdevplatform/vcs/models/vcsfilechangesmodel.cpp index ac93843980..74e579d841 100644 --- a/kdevplatform/vcs/models/vcsfilechangesmodel.cpp +++ b/kdevplatform/vcs/models/vcsfilechangesmodel.cpp @@ -1,295 +1,297 @@ /* This file is part of KDevelop Copyright 2010 Aleix Pol Split into separate class Copyright 2011 Andrey Batyiev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "vcsfilechangesmodel.h" #include "debug.h" #include #include #include #include #include #include namespace KDevelop { static QString stateToString(KDevelop::VcsStatusInfo::State state) { switch(state) { case KDevelop::VcsStatusInfo::ItemAdded: return i18nc("file was added to versioncontrolsystem", "Added"); case KDevelop::VcsStatusInfo::ItemDeleted: return i18nc("file was deleted from versioncontrolsystem", "Deleted"); case KDevelop::VcsStatusInfo::ItemHasConflicts: return i18nc("file is confilicting (versioncontrolsystem)", "Has Conflicts"); case KDevelop::VcsStatusInfo::ItemModified: return i18nc("version controlled file was modified", "Modified"); case KDevelop::VcsStatusInfo::ItemUpToDate: return i18nc("file is up to date in versioncontrolsystem", "Up To Date"); case KDevelop::VcsStatusInfo::ItemUnknown: case KDevelop::VcsStatusInfo::ItemUserState: return i18nc("file is not known to versioncontrolsystem", "Unknown"); } return i18nc("Unknown VCS file status, probably a backend error", "?"); } static QIcon stateToIcon(KDevelop::VcsStatusInfo::State state) { switch(state) { case KDevelop::VcsStatusInfo::ItemAdded: return QIcon::fromTheme(QStringLiteral("vcs-added")); case KDevelop::VcsStatusInfo::ItemDeleted: return QIcon::fromTheme(QStringLiteral("vcs-removed")); case KDevelop::VcsStatusInfo::ItemHasConflicts: return QIcon::fromTheme(QStringLiteral("vcs-conflicting")); case KDevelop::VcsStatusInfo::ItemModified: return QIcon::fromTheme(QStringLiteral("vcs-locally-modified")); case KDevelop::VcsStatusInfo::ItemUpToDate: return QIcon::fromTheme(QStringLiteral("vcs-normal")); case KDevelop::VcsStatusInfo::ItemUnknown: case KDevelop::VcsStatusInfo::ItemUserState: return QIcon::fromTheme(QStringLiteral("unknown")); } return QIcon::fromTheme(QStringLiteral("dialog-error")); } VcsFileChangesSortProxyModel::VcsFileChangesSortProxyModel(QObject* parent) : QSortFilterProxyModel(parent) { } bool VcsFileChangesSortProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { const auto leftStatus = source_left.data(VcsFileChangesModel::StateRole).value(); const auto rightStatus = source_right.data(VcsFileChangesModel::StateRole).value(); if (leftStatus != rightStatus) { return leftStatus < rightStatus; } const QString leftPath = source_left.data(VcsFileChangesModel::UrlRole).toString(); const QString rightPath = source_right.data(VcsFileChangesModel::UrlRole).toString(); return QString::localeAwareCompare(leftPath, rightPath) < 0; } class VcsStatusInfoItem : public QStandardItem { public: explicit VcsStatusInfoItem(const VcsStatusInfo& info) : QStandardItem() , m_info(info) {} void setStatus(const VcsStatusInfo& info) { m_info = info; emitDataChanged(); } QVariant data(int role) const override { switch(role) { case Qt::DisplayRole: return stateToString(m_info.state()); case Qt::DecorationRole: return stateToIcon(m_info.state()); case VcsFileChangesModel::VcsStatusInfoRole: return QVariant::fromValue(m_info); case VcsFileChangesModel::UrlRole: return m_info.url(); case VcsFileChangesModel::StateRole: return QVariant::fromValue(m_info.state()); } return {}; } private: VcsStatusInfo m_info; }; class VcsFileChangesModelPrivate { public: bool allowSelection; }; VcsFileChangesModel::VcsFileChangesModel(QObject *parent, bool allowSelection) : QStandardItemModel(parent), d(new VcsFileChangesModelPrivate {allowSelection} ) { setColumnCount(2); setHeaderData(0, Qt::Horizontal, i18n("Filename")); setHeaderData(1, Qt::Horizontal, i18n("Status")); } VcsFileChangesModel::~VcsFileChangesModel() { } int VcsFileChangesModel::updateState(QStandardItem *parent, const KDevelop::VcsStatusInfo &status) { if(status.state()==VcsStatusInfo::ItemUnknown || status.state()==VcsStatusInfo::ItemUpToDate) { removeUrl(status.url()); return -1; } else { QStandardItem* item = fileItemForUrl(parent, status.url()); if(!item) { QString path = ICore::self()->projectController()->prettyFileName(status.url(), KDevelop::IProjectController::FormatPlain); QMimeType mime = status.url().isLocalFile() ? QMimeDatabase().mimeTypeForFile(status.url().toLocalFile(), QMimeDatabase::MatchExtension) : QMimeDatabase().mimeTypeForUrl(status.url()); QIcon icon = QIcon::fromTheme(mime.iconName()); item = new QStandardItem(icon, path); auto itStatus = new VcsStatusInfoItem(status); if(d->allowSelection) { item->setCheckable(true); item->setCheckState(status.state() == VcsStatusInfo::ItemUnknown ? Qt::Unchecked : Qt::Checked); } parent->appendRow({ item, itStatus }); } else { QStandardItem *parent = item->parent(); if(parent == nullptr) parent = invisibleRootItem(); auto statusInfoItem = static_cast(parent->child(item->row(), 1)); statusInfoItem->setStatus(status); } return item->row(); } } QVariant VcsFileChangesModel::data(const QModelIndex &index, int role) const { if (role >= VcsStatusInfoRole && index.column()==0) { return QStandardItemModel::data(index.sibling(index.row(), 1), role); } return QStandardItemModel::data(index, role); } QStandardItem* VcsFileChangesModel::fileItemForUrl(QStandardItem* parent, const QUrl& url) const { Q_ASSERT(parent); if (!parent) { qCWarning(VCS) << "null QStandardItem passed to" << Q_FUNC_INFO; return nullptr; } for(int i=0, c=parent->rowCount(); ichild(i); if(indexFromItem(item).data(UrlRole).toUrl() == url) { return parent->child(i); } } return nullptr; } void VcsFileChangesModel::setAllChecked(bool checked) { if(!d->allowSelection) return; QStandardItem* parent = invisibleRootItem(); for(int i = 0, c = parent->rowCount(); i < c; i++) { QStandardItem* item = parent->child(i); item->setCheckState(checked ? Qt::Checked : Qt::Unchecked); } } QList VcsFileChangesModel::checkedUrls(QStandardItem *parent) const { Q_ASSERT(parent); if (!parent) { qCWarning(VCS) << "null QStandardItem passed to" << Q_FUNC_INFO; return {}; } QList ret; for(int i = 0, c = parent->rowCount(); i < c; i++) { QStandardItem* item = parent->child(i); if(!d->allowSelection || item->checkState() == Qt::Checked) { ret << indexFromItem(item).data(UrlRole).toUrl(); } } return ret; } QList VcsFileChangesModel::urls(QStandardItem *parent) const { Q_ASSERT(parent); if (!parent) { qCWarning(VCS) << "null QStandardItem passed to" << Q_FUNC_INFO; return {}; } QList ret; - for(int i = 0, c = parent->rowCount(); i < c; i++) { + const int c = parent->rowCount(); + ret.reserve(c); + for (int i = 0; i < c; i++) { QStandardItem* item = parent->child(i); ret << indexFromItem(item).data(UrlRole).toUrl(); } return ret; } void VcsFileChangesModel::checkUrls(QStandardItem *parent, const QList& urls) const { Q_ASSERT(parent); if (!parent) { qCWarning(VCS) << "null QStandardItem passed to" << Q_FUNC_INFO; return; } if(!d->allowSelection) return; QSet urlSet(urls.toSet()); for(int i = 0, c = parent->rowCount(); i < c; i++) { QStandardItem* item = parent->child(i); item->setCheckState(urlSet.contains(indexFromItem(item).data(UrlRole).toUrl()) ? Qt::Checked : Qt::Unchecked); } } void VcsFileChangesModel::setIsCheckbable(bool checkable) { d->allowSelection = checkable; } bool VcsFileChangesModel::isCheckable() const { return d->allowSelection; } bool VcsFileChangesModel::removeUrl(const QUrl& url) { const auto matches = match(index(0, 0), UrlRole, url, 1, Qt::MatchExactly); if (matches.isEmpty()) return false; const auto& idx = matches.first(); return removeRow(idx.row(), idx.parent()); } } diff --git a/kdevplatform/vcs/models/vcsitemeventmodel.cpp b/kdevplatform/vcs/models/vcsitemeventmodel.cpp index c145817121..cab3e1f9fc 100644 --- a/kdevplatform/vcs/models/vcsitemeventmodel.cpp +++ b/kdevplatform/vcs/models/vcsitemeventmodel.cpp @@ -1,114 +1,115 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "vcsitemeventmodel.h" #include #include #include #include #include #include #include #include #include "../vcsrevision.h" #include "../vcsevent.h" namespace KDevelop { VcsItemEventModel::VcsItemEventModel( QObject* parent ) : QStandardItemModel( parent ) { setColumnCount(2); } VcsItemEventModel::~VcsItemEventModel() {} void VcsItemEventModel::addItemEvents( const QList& list ) { if(rowCount()==0) setColumnCount(2); bool copySource = false; QMimeDatabase mimeDataBase; foreach(const KDevelop::VcsItemEvent& ev, list) { KDevelop::VcsItemEvent::Actions act = ev.actions(); QStringList actionStrings; if( act & KDevelop::VcsItemEvent::Added ) actionStrings << i18n("Added"); else if( act & KDevelop::VcsItemEvent::Deleted ) actionStrings << i18n("Deleted"); else if( act & KDevelop::VcsItemEvent::Modified ) actionStrings << i18n("Modified"); else if( act & KDevelop::VcsItemEvent::Copied ) actionStrings << i18n("Copied"); else if( act & KDevelop::VcsItemEvent::Replaced ) actionStrings << i18n("Replaced"); QUrl repoUrl = QUrl::fromLocalFile(ev.repositoryLocation()); QMimeType mime = repoUrl.isLocalFile() ? mimeDataBase.mimeTypeForFile(repoUrl.toLocalFile(), QMimeDatabase::MatchExtension) : mimeDataBase.mimeTypeForUrl(repoUrl); - QList rowItems = QList() - << new QStandardItem(QIcon::fromTheme(mime.iconName()), ev.repositoryLocation()) - << new QStandardItem(actionStrings.join(i18nc("separes an action list", ", "))); + QList rowItems{ + new QStandardItem(QIcon::fromTheme(mime.iconName()), ev.repositoryLocation()), + new QStandardItem(actionStrings.join(i18nc("separes an action list", ", "))), + }; QString loc = ev.repositoryCopySourceLocation(); if(!loc.isEmpty()) { //according to the documentation, those are optional. don't force them on the UI rowItems << new QStandardItem(ev.repositoryCopySourceLocation()); VcsRevision rev = ev.repositoryCopySourceRevision(); if(rev.revisionType()!=VcsRevision::Invalid) { rowItems << new QStandardItem(ev.repositoryCopySourceRevision().revisionValue().toString()); } copySource = true; } rowItems.first()->setData(qVariantFromValue(ev)); appendRow(rowItems); } if(copySource) setColumnCount(4); } QVariant VcsItemEventModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role==Qt::DisplayRole) { switch(section) { case 0: return i18n("Location"); case 1: return i18n("Actions"); case 2: return i18n("Source Location"); case 3: return i18n("Source Revision"); } } return QStandardItemModel::headerData(section, orientation, role); } KDevelop::VcsItemEvent VcsItemEventModel::itemEventForIndex( const QModelIndex& idx ) const { return itemFromIndex(idx)->data().value(); } } diff --git a/kdevplatform/vcs/widgets/vcsdiffpatchsources.cpp b/kdevplatform/vcs/widgets/vcsdiffpatchsources.cpp index 00ddd48b0e..5224a441e8 100644 --- a/kdevplatform/vcs/widgets/vcsdiffpatchsources.cpp +++ b/kdevplatform/vcs/widgets/vcsdiffpatchsources.cpp @@ -1,320 +1,321 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "vcsdiffpatchsources.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vcsdiff.h" #include "vcsjob.h" #include "debug.h" using namespace KDevelop; VCSCommitDiffPatchSource::VCSCommitDiffPatchSource(VCSDiffUpdater* updater) : VCSDiffPatchSource(updater), m_vcs(updater->vcs()) { Q_ASSERT(m_vcs); m_commitMessageWidget = new QWidget; QVBoxLayout* layout = new QVBoxLayout(m_commitMessageWidget.data()); layout->setMargin(0); m_commitMessageEdit = new KTextEdit; m_commitMessageEdit.data()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commitMessageEdit.data()->setLineWrapMode(QTextEdit::NoWrap); m_vcs->setupCommitMessageEditor(updater->url(), m_commitMessageEdit.data()); QHBoxLayout* titleLayout = new QHBoxLayout; titleLayout->addWidget(new QLabel(i18n("Commit Message:"))); m_oldMessages = new KComboBox(m_commitMessageWidget.data()); m_oldMessages->addItem(i18n("Old Messages")); foreach(const QString& message, oldMessages()) m_oldMessages->addItem(message, message); m_oldMessages->setMaximumWidth(200); connect(m_oldMessages, static_cast(&KComboBox::currentIndexChanged), this, &VCSCommitDiffPatchSource::oldMessageChanged); titleLayout->addWidget(m_oldMessages); layout->addLayout(titleLayout); layout->addWidget(m_commitMessageEdit.data()); connect(this, &VCSCommitDiffPatchSource::reviewCancelled, this, &VCSCommitDiffPatchSource::addMessageToHistory); connect(this, &VCSCommitDiffPatchSource::reviewFinished, this, &VCSCommitDiffPatchSource::addMessageToHistory); } QStringList VCSCommitDiffPatchSource::oldMessages() const { KConfigGroup vcsGroup(ICore::self()->activeSession()->config(), "VCS"); return vcsGroup.readEntry("OldCommitMessages", QStringList()); } void VCSCommitDiffPatchSource::addMessageToHistory(const QString& message) { if(ICore::self()->shuttingDown()) return; KConfigGroup vcsGroup(ICore::self()->activeSession()->config(), "VCS"); const int maxMessages = 10; QStringList oldMessages = vcsGroup.readEntry("OldCommitMessages", QStringList()); oldMessages.removeAll(message); oldMessages.push_front(message); oldMessages = oldMessages.mid(0, maxMessages); vcsGroup.writeEntry("OldCommitMessages", oldMessages); } void VCSCommitDiffPatchSource::oldMessageChanged(const QString& text) { if(m_oldMessages->currentIndex() != 0) { m_oldMessages->setCurrentIndex(0); m_commitMessageEdit.data()->setText(text); } } void VCSCommitDiffPatchSource::jobFinished(KJob *job) { if (!job || job->error() != 0 ) { QString details = job ? job->errorText() : QString(); if (details.isEmpty()) { //errorText may be empty details = i18n("For more detailed information please see the Version Control tool view."); } KMessageBox::detailedError(nullptr, i18n("Unable to commit"), details, i18n("Commit unsuccessful")); } deleteLater(); } VCSDiffPatchSource::VCSDiffPatchSource(VCSDiffUpdater* updater) : m_updater(updater) { update(); KDevelop::IBasicVersionControl* vcs = m_updater->vcs(); QUrl url = m_updater->url(); QScopedPointer statusJob(vcs->status(QList() << url)); QVariant varlist; if( statusJob->exec() && statusJob->status() == VcsJob::JobSucceeded ) { varlist = statusJob->fetchResults(); - foreach( const QVariant &var, varlist.toList() ) - { + const auto vars = varlist.toList(); + m_infos.reserve(m_infos.size() + vars.size()); + for (const auto& var : vars) { VcsStatusInfo info = var.value(); m_infos += info; if(info.state()!=VcsStatusInfo::ItemUpToDate) m_selectable[info.url()] = info.state(); } } else qCDebug(VCS) << "Couldn't get status for urls: " << url; } VCSDiffPatchSource::VCSDiffPatchSource(const KDevelop::VcsDiff& diff) : m_updater(nullptr) { updateFromDiff(diff); } VCSDiffPatchSource::~VCSDiffPatchSource() { QFile::remove(m_file.toLocalFile()); delete m_updater; } QUrl VCSDiffPatchSource::baseDir() const { return m_base; } QUrl VCSDiffPatchSource::file() const { return m_file; } QString VCSDiffPatchSource::name() const { return m_name; } uint VCSDiffPatchSource::depth() const { return m_depth; } void VCSDiffPatchSource::updateFromDiff(const VcsDiff& vcsdiff) { if(!m_file.isValid()) { QTemporaryFile temp2(QDir::tempPath() + QLatin1String("/kdevelop_XXXXXX.patch")); temp2.setAutoRemove(false); temp2.open(); QTextStream t2(&temp2); t2 << vcsdiff.diff(); qCDebug(VCS) << "filename:" << temp2.fileName(); m_file = QUrl::fromLocalFile(temp2.fileName()); temp2.close(); }else{ QFile file(m_file.path()); file.open(QIODevice::WriteOnly); QTextStream t2(&file); t2 << vcsdiff.diff(); } qCDebug(VCS) << "using file" << m_file << vcsdiff.diff() << "base" << vcsdiff.baseDiff(); m_name = QStringLiteral("VCS Diff"); m_base = vcsdiff.baseDiff(); m_depth = vcsdiff.depth(); emit patchChanged(); } void VCSDiffPatchSource::update() { if(!m_updater) return; updateFromDiff(m_updater->update()); } VCSCommitDiffPatchSource::~VCSCommitDiffPatchSource() { delete m_commitMessageWidget.data(); } bool VCSCommitDiffPatchSource::canSelectFiles() const { return true; } QMap< QUrl, KDevelop::VcsStatusInfo::State> VCSDiffPatchSource::additionalSelectableFiles() const { return m_selectable; } QWidget* VCSCommitDiffPatchSource::customWidget() const { return m_commitMessageWidget.data(); } QString VCSCommitDiffPatchSource::finishReviewCustomText() const { return i18nc("@action:button To make a commit", "Commit"); } bool VCSCommitDiffPatchSource::canCancel() const { return true; } void VCSCommitDiffPatchSource::cancelReview() { QString message; if (m_commitMessageEdit) message = m_commitMessageEdit.data()->toPlainText(); emit reviewCancelled(message); deleteLater(); } bool VCSCommitDiffPatchSource::finishReview(const QList& selection) { QString message; if (m_commitMessageEdit) message = m_commitMessageEdit.data()->toPlainText(); qCDebug(VCS) << "Finishing with selection" << selection; QString files; foreach(const QUrl& url, selection) files += QLatin1String("
  • ") + ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain) + QLatin1String("
  • "); QString text = i18n("Files will be committed:\n
      %1
    \nWith message:\n
    %2
    ", files, message); int res = KMessageBox::warningContinueCancel(nullptr, text, i18n("About to commit to repository"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShouldAskConfirmCommit")); if (res != KMessageBox::Continue) { return false; } emit reviewFinished(message, selection); VcsJob* job = m_vcs->commit(message, selection, KDevelop::IBasicVersionControl::NonRecursive); if (!job) { return false; } connect (job, &VcsJob::finished, this, &VCSCommitDiffPatchSource::jobFinished); ICore::self()->runController()->registerJob(job); return true; } bool showVcsDiff(IPatchSource* vcsDiff) { KDevelop::IPatchReview* patchReview = ICore::self()->pluginController()->extensionForPlugin(QStringLiteral("org.kdevelop.IPatchReview")); if( patchReview ) { patchReview->startReview(vcsDiff); return true; } else { qCWarning(VCS) << "Patch review plugin not found"; return false; } } VcsDiff VCSStandardDiffUpdater::update() const { QScopedPointer diffJob(m_vcs->diff(m_url, KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Base), KDevelop::VcsRevision::createSpecialRevision(KDevelop::VcsRevision::Working))); const bool success = diffJob ? diffJob->exec() : false; if (!success) { KMessageBox::error(nullptr, i18n("Could not create a patch for the current version.")); return {}; } return diffJob->fetchResults().value(); } VCSStandardDiffUpdater::VCSStandardDiffUpdater(IBasicVersionControl* vcs, QUrl url) : m_vcs(vcs), m_url(url) { } VCSStandardDiffUpdater::~VCSStandardDiffUpdater() { } VCSDiffUpdater::~VCSDiffUpdater() { } diff --git a/plugins/appwizard/appwizardplugin.cpp b/plugins/appwizard/appwizardplugin.cpp index 4fa82eaf8c..0797e428d6 100644 --- a/plugins/appwizard/appwizardplugin.cpp +++ b/plugins/appwizard/appwizardplugin.cpp @@ -1,565 +1,566 @@ /*************************************************************************** * Copyright 2001 Bernd Gehrmann * * Copyright 2004-2005 Sascha Cunz * * Copyright 2005 Ian Reinhart Geiser * * Copyright 2007 Alexander Dymo * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "appwizardplugin.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 "appwizarddialog.h" #include "projectselectionpage.h" #include "projectvcspage.h" #include "projecttemplatesmodel.h" #include "debug.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(AppWizardFactory, "kdevappwizard.json", registerPlugin();) AppWizardPlugin::AppWizardPlugin(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevappwizard"), parent) , m_templatesModel(nullptr) { setXMLFile(QStringLiteral("kdevappwizard.rc")); m_newFromTemplate = actionCollection()->addAction(QStringLiteral("project_new")); m_newFromTemplate->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); m_newFromTemplate->setText(i18n("New From Template...")); connect(m_newFromTemplate, &QAction::triggered, this, &AppWizardPlugin::slotNewProject); m_newFromTemplate->setToolTip( i18n("Generate a new project from a template") ); m_newFromTemplate->setWhatsThis( i18n("This starts KDevelop's application wizard. " "It helps you to generate a skeleton for your " "application from a set of templates.") ); } AppWizardPlugin::~AppWizardPlugin() { } void AppWizardPlugin::slotNewProject() { model()->refresh(); ScopedDialog dlg(core()->pluginController(), m_templatesModel); if (dlg->exec() == QDialog::Accepted) { QString project = createProject( dlg->appInfo() ); if (!project.isEmpty()) { core()->projectController()->openProject(QUrl::fromLocalFile(project)); KConfig templateConfig(dlg->appInfo().appTemplate); KConfigGroup general(&templateConfig, "General"); const QStringList fileArgs = general.readEntry("ShowFilesAfterGeneration").split(QLatin1Char(','), QString::SkipEmptyParts); for (const auto& fileArg : fileArgs) { QString file = KMacroExpander::expandMacros(fileArg.trimmed(), m_variables); if (QDir::isRelativePath(file)) { file = m_variables[QStringLiteral("PROJECTDIR")] + QLatin1Char('/') + file; } core()->documentController()->openDocument(QUrl::fromUserInput(file)); } } else { KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n("Could not create project from template\n"), i18n("Failed to create project") ); } } } namespace { IDistributedVersionControl* toDVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } ICentralizedVersionControl* toCVCS(IPlugin* plugin) { Q_ASSERT(plugin); return plugin->extension(); } /*! Trouble while initializing version control. Show failure message to user. */ void vcsError(const QString &errorMsg, QTemporaryDir &tmpdir, const QUrl &dest, const QString &details = QString()) { QString displayDetails = details; if (displayDetails.isEmpty()) { displayDetails = i18n("Please see the Version Control tool view."); } KMessageBox::detailedError(nullptr, errorMsg, displayDetails, i18n("Version Control System Error")); KIO::del(dest, KIO::HideProgressInfo)->exec(); tmpdir.remove(); } /*! Setup distributed version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeDVCS(IDistributedVersionControl* dvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(dvcs); qCDebug(PLUGIN_APPWIZARD) << "DVCS system is used, just initializing DVCS"; const QUrl& dest = info.location; //TODO: check if we want to handle KDevelop project files (like now) or only SRC dir VcsJob* job = dvcs->init(dest); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not initialize DVCS repository"), scratchArea, dest); return false; } qCDebug(PLUGIN_APPWIZARD) << "Initializing DVCS repository:" << dest; job = dvcs->add({dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not add files to the DVCS repository"), scratchArea, dest); return false; } job = dvcs->commit(info.importCommitMessage, {dest}, KDevelop::IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded) { vcsError(i18n("Could not import project into %1.", dvcs->name()), scratchArea, dest, job ? job->errorString() : QString()); return false; } return true; // We're good } /*! Setup version control for a new project defined by @p info. Use @p scratchArea for temporary files */ bool initializeCVCS(ICentralizedVersionControl* cvcs, const ApplicationInfo& info, QTemporaryDir& scratchArea) { Q_ASSERT(cvcs); qCDebug(PLUGIN_APPWIZARD) << "Importing" << info.sourceLocation << "to" << info.repository.repositoryServer(); VcsJob* job = cvcs->import( info.importCommitMessage, QUrl::fromLocalFile(scratchArea.path()), info.repository); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not import project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } qCDebug(PLUGIN_APPWIZARD) << "Checking out"; job = cvcs->createWorkingCopy( info.repository, info.location, IBasicVersionControl::Recursive); if (!job || !job->exec() || job->status() != VcsJob::JobSucceeded ) { vcsError(i18n("Could not checkout imported project"), scratchArea, QUrl::fromUserInput(info.repository.repositoryServer())); return false; } return true; // initialization phase complete } QString generateIdentifier( const QString& appname ) { QString tmp = appname; QRegExp re("[^a-zA-Z0-9_]"); return tmp.replace(re, QStringLiteral("_")); } } // end anonymous namespace QString AppWizardPlugin::createProject(const ApplicationInfo& info) { QFileInfo templateInfo(info.appTemplate); if (!templateInfo.exists()) { qCWarning(PLUGIN_APPWIZARD) << "Project app template does not exist:" << info.appTemplate; return QString(); } QString templateName = templateInfo.baseName(); qCDebug(PLUGIN_APPWIZARD) << "Searching archive for template name:" << templateName; QString templateArchive; const QStringList filters = {templateName + QStringLiteral(".*")}; const QStringList matchesPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdevappwizard/templates/"), QStandardPaths::LocateDirectory); foreach(const QString& matchesPath, matchesPaths) { const QStringList files = QDir(matchesPath).entryList(filters); if(!files.isEmpty()) { templateArchive = matchesPath + files.first(); } } if(templateArchive.isEmpty()) { qCWarning(PLUGIN_APPWIZARD) << "Template name does not exist in the template list"; return QString(); } qCDebug(PLUGIN_APPWIZARD) << "Using template archive:" << templateArchive; QUrl dest = info.location; //prepare variable substitution hash m_variables.clear(); m_variables[QStringLiteral("APPNAME")] = info.name; m_variables[QStringLiteral("APPNAMEUC")] = info.name.toUpper(); m_variables[QStringLiteral("APPNAMELC")] = info.name.toLower(); m_variables[QStringLiteral("APPNAMEID")] = generateIdentifier(info.name); m_variables[QStringLiteral("PROJECTDIR")] = dest.toLocalFile(); // backwards compatibility m_variables[QStringLiteral("dest")] = m_variables[QStringLiteral("PROJECTDIR")]; m_variables[QStringLiteral("PROJECTDIRNAME")] = dest.fileName(); m_variables[QStringLiteral("VERSIONCONTROLPLUGIN")] = info.vcsPluginName; KArchive* arch = nullptr; if( templateArchive.endsWith(QLatin1String(".zip")) ) { arch = new KZip(templateArchive); } else { arch = new KTar(templateArchive, QStringLiteral("application/x-bzip")); } if (arch->open(QIODevice::ReadOnly)) { QTemporaryDir tmpdir; QString unpackDir = tmpdir.path(); //the default value for all Centralized VCS IPlugin* plugin = core()->pluginController()->loadPlugin( info.vcsPluginName ); if( info.vcsPluginName.isEmpty() || ( plugin && plugin->extension() ) ) { if( !QFileInfo::exists( dest.toLocalFile() ) ) { QDir::root().mkpath( dest.toLocalFile() ); } unpackDir = dest.toLocalFile(); //in DVCS we unpack template directly to the project's directory } else { QUrl url = KIO::upUrl(dest); if(!QFileInfo::exists(url.toLocalFile())) { QDir::root().mkpath(url.toLocalFile()); } } // estimate metadata files which should not be copied QStringList metaDataFileNames; // try by same name const KArchiveEntry *templateEntry = arch->directory()->entry(templateName + QLatin1String(".kdevtemplate")); // but could be different name, if e.g. downloaded, so make a guess if (!templateEntry || !templateEntry->isFile()) { for (const auto& entryName : arch->directory()->entries()) { if (entryName.endsWith(QLatin1String(".kdevtemplate"))) { templateEntry = arch->directory()->entry(entryName); break; } } } if (templateEntry && templateEntry->isFile()) { metaDataFileNames << templateEntry->name(); // check if a preview file is to be ignored const KArchiveFile *templateFile = static_cast(templateEntry); QTemporaryDir temporaryDir; templateFile->copyTo(temporaryDir.path()); KConfig config(temporaryDir.path() + QLatin1Char('/') + templateEntry->name()); KConfigGroup group(&config, "General"); if (group.hasKey("Icon")) { const KArchiveEntry* iconEntry = arch->directory()->entry(group.readEntry("Icon")); if (iconEntry && iconEntry->isFile()) { metaDataFileNames << iconEntry->name(); } } } if (!unpackArchive(arch->directory(), unpackDir, metaDataFileNames)) { QString errorMsg = i18n("Could not create new project"); vcsError(errorMsg, tmpdir, QUrl::fromLocalFile(unpackDir)); return QString(); } if( !info.vcsPluginName.isEmpty() ) { if (!plugin) { // Red Alert, serious program corruption. // This should never happen, the vcs dialog presented a list of vcs // systems and now the chosen system doesn't exist anymore?? tmpdir.remove(); return QString(); } IDistributedVersionControl* dvcs = toDVCS(plugin); ICentralizedVersionControl* cvcs = toCVCS(plugin); bool success = false; if (dvcs) { success = initializeDVCS(dvcs, info, tmpdir); } else if (cvcs) { success = initializeCVCS(cvcs, info, tmpdir); } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel(nullptr, QStringLiteral("Failed to initialize version control system, " "plugin is neither VCS nor DVCS."))) success = true; } if (!success) return QString(); } tmpdir.remove(); }else { qCDebug(PLUGIN_APPWIZARD) << "failed to open template archive"; return QString(); } QString projectFileName = QDir::cleanPath( dest.toLocalFile() + '/' + info.name + ".kdev4" ); // Loop through the new project directory and try to detect the first .kdev4 file. // If one is found this file will be used. So .kdev4 file can be stored in any subdirectory and the // project templates can be more complex. QDirIterator it(QDir::cleanPath( dest.toLocalFile()), QStringList() << QStringLiteral("*.kdev4"), QDir::NoFilter, QDirIterator::Subdirectories); if(it.hasNext() == true) { projectFileName = it.next(); } qCDebug(PLUGIN_APPWIZARD) << "Returning" << projectFileName << QFileInfo::exists( projectFileName ) ; const QFileInfo projectFileInfo(projectFileName); if (!projectFileInfo.exists()) { qCDebug(PLUGIN_APPWIZARD) << "creating .kdev4 file"; KSharedConfigPtr cfg = KSharedConfig::openConfig( projectFileName, KConfig::SimpleConfig ); KConfigGroup project = cfg->group( "Project" ); project.writeEntry( "Name", info.name ); QString manager = QStringLiteral("KDevGenericManager"); QDir d( dest.toLocalFile() ); auto data = ICore::self()->pluginController()->queryExtensionPlugins(QStringLiteral("org.kdevelop.IProjectFileManager")); foreach(const KPluginMetaData& info, data) { QStringList filter = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-ProjectFilesFilter")); if (!filter.isEmpty()) { if (!d.entryList(filter).isEmpty()) { manager = info.pluginId(); break; } } } project.writeEntry( "Manager", manager ); project.sync(); cfg->sync(); KConfigGroup project2 = cfg->group( "Project" ); qCDebug(PLUGIN_APPWIZARD) << "kdev4 file contents:" << project2.readEntry("Name", "") << project2.readEntry("Manager", "" ); } // create developer .kde4 file const QString developerProjectFileName = projectFileInfo.canonicalPath() + QLatin1String("/.kdev4/") + projectFileInfo.fileName(); qCDebug(PLUGIN_APPWIZARD) << "creating developer .kdev4 file:" << developerProjectFileName; KSharedConfigPtr developerCfg = KSharedConfig::openConfig(developerProjectFileName, KConfig::SimpleConfig); KConfigGroup developerProjectGroup = developerCfg->group("Project"); developerProjectGroup.writeEntry("VersionControlSupport", info.vcsPluginName); developerProjectGroup.sync(); developerCfg->sync(); return projectFileName; } bool AppWizardPlugin::unpackArchive(const KArchiveDirectory* dir, const QString& dest, const QStringList& skipList) { qCDebug(PLUGIN_APPWIZARD) << "unpacking dir:" << dir->name() << "to" << dest; const QStringList entries = dir->entries(); qCDebug(PLUGIN_APPWIZARD) << "entries:" << entries.join(QLatin1Char(',')); //This extra tempdir is needed just for the files files have special names, //which may contain macros also files contain content with macros. So the //easiest way to extract the files from the archive and then rename them //and replace the macros is to use a tempdir and copy the file (and //replacing while copying). This also allows one to easily remove all files, //by just unlinking the tempdir QTemporaryDir tdir; bool ret = true; for (const auto& entryName : entries) { if (skipList.contains(entryName)) { continue; } const auto entry = dir->entry(entryName); if (entry->isDirectory()) { const KArchiveDirectory* subdir = static_cast(entry); QString newdest = dest + '/' + KMacroExpander::expandMacros(subdir->name(), m_variables); if( !QFileInfo::exists( newdest ) ) { QDir::root().mkdir( newdest ); } ret |= unpackArchive(subdir, newdest); } else if (entry->isFile()) { const KArchiveFile* file = static_cast(entry); file->copyTo(tdir.path()); QString destName = dest + '/' + file->name(); if (!copyFileAndExpandMacros(QDir::cleanPath(tdir.path()+'/'+file->name()), KMacroExpander::expandMacros(destName, m_variables))) { KMessageBox::sorry(nullptr, i18n("The file %1 cannot be created.", dest)); return false; } } } tdir.remove(); return ret; } bool AppWizardPlugin::copyFileAndExpandMacros(const QString &source, const QString &dest) { qCDebug(PLUGIN_APPWIZARD) << "copy:" << source << "to" << dest; QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(source); if( !mime.inherits(QStringLiteral("text/plain")) ) { KIO::CopyJob* job = KIO::copy( QUrl::fromUserInput(source), QUrl::fromUserInput(dest), KIO::HideProgressInfo ); if( !job->exec() ) { return false; } return true; } else { QFile inputFile(source); QFile outputFile(dest); if (inputFile.open(QFile::ReadOnly) && outputFile.open(QFile::WriteOnly)) { QTextStream input(&inputFile); input.setCodec(QTextCodec::codecForName("UTF-8")); QTextStream output(&outputFile); output.setCodec(QTextCodec::codecForName("UTF-8")); while(!input.atEnd()) { QString line = input.readLine(); output << KMacroExpander::expandMacros(line, m_variables) << "\n"; } #ifndef Q_OS_WIN // Preserve file mode... QT_STATBUF statBuf; QT_FSTAT(inputFile.handle(), &statBuf); // Unix only, won't work in Windows, maybe KIO::chmod could be used ::fchmod(outputFile.handle(), statBuf.st_mode); #endif return true; } else { inputFile.close(); outputFile.close(); return false; } } } KDevelop::ContextMenuExtension AppWizardPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { Q_UNUSED(parent); KDevelop::ContextMenuExtension ext; if ( context->type() != KDevelop::Context::ProjectItemContext || !static_cast(context)->items().isEmpty() ) { return ext; } ext.addAction(KDevelop::ContextMenuExtension::ProjectGroup, m_newFromTemplate); return ext; } ProjectTemplatesModel* AppWizardPlugin::model() { if(!m_templatesModel) m_templatesModel = new ProjectTemplatesModel(this); return m_templatesModel; } QAbstractItemModel* AppWizardPlugin::templatesModel() { return model(); } QString AppWizardPlugin::knsConfigurationFile() const { return QStringLiteral("kdevappwizard.knsrc"); } QStringList AppWizardPlugin::supportedMimeTypes() const { - QStringList types; - types << QStringLiteral("application/x-desktop"); - types << QStringLiteral("application/x-bzip-compressed-tar"); - types << QStringLiteral("application/zip"); + const QStringList types{ + QStringLiteral("application/x-desktop"), + QStringLiteral("application/x-bzip-compressed-tar"), + QStringLiteral("application/zip"), + }; return types; } QIcon AppWizardPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("project-development-new-template")); } QString AppWizardPlugin::name() const { return i18n("Project Templates"); } void AppWizardPlugin::loadTemplate(const QString& fileName) { model()->loadTemplateFile(fileName); } void AppWizardPlugin::reload() { model()->refresh(); } #include "appwizardplugin.moc" diff --git a/plugins/appwizard/projectselectionpage.cpp b/plugins/appwizard/projectselectionpage.cpp index 22f597ed54..b1aced0692 100644 --- a/plugins/appwizard/projectselectionpage.cpp +++ b/plugins/appwizard/projectselectionpage.cpp @@ -1,366 +1,366 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * Copyright 2011 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "projectselectionpage.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_projectselectionpage.h" #include "projecttemplatesmodel.h" using namespace KDevelop; ProjectSelectionPage::ProjectSelectionPage(ProjectTemplatesModel *templatesModel, AppWizardDialog *wizardDialog) : AppWizardPageWidget(wizardDialog), m_templatesModel(templatesModel) { ui = new Ui::ProjectSelectionPage(); ui->setupUi(this); setContentsMargins(0,0,0,0); ui->descriptionContent->setBackgroundRole(QPalette::Base); ui->descriptionContent->setForegroundRole(QPalette::Text); ui->locationUrl->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly ); ui->locationUrl->setUrl(KDevelop::ICore::self()->projectController()->projectsBaseDirectory()); ui->locationValidWidget->hide(); ui->locationValidWidget->setMessageType(KMessageWidget::Error); ui->locationValidWidget->setCloseButtonVisible(false); connect( ui->locationUrl->lineEdit(), &KLineEdit::textEdited, this, &ProjectSelectionPage::urlEdited); connect( ui->locationUrl, &KUrlRequester::urlSelected, this, &ProjectSelectionPage::urlEdited); connect( ui->projectNameEdit, &QLineEdit::textEdited, this, &ProjectSelectionPage::nameChanged ); ui->listView->setLevels(2); - ui->listView->setHeaderLabels(QStringList() << i18n("Category") << i18n("Project Type")); + ui->listView->setHeaderLabels(QStringList{i18n("Category"), i18n("Project Type")}); ui->listView->setModel(templatesModel); ui->listView->setLastLevelViewMode(MultiLevelListView::DirectChildren); connect (ui->listView, &MultiLevelListView::currentIndexChanged, this, &ProjectSelectionPage::typeChanged); typeChanged(ui->listView->currentIndex()); connect( ui->templateType, static_cast(&QComboBox::currentIndexChanged), this, &ProjectSelectionPage::templateChanged ); QPushButton* getMoreButton = new QPushButton(i18n("Get More Templates"), ui->listView); getMoreButton->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); connect (getMoreButton, &QPushButton::clicked, this, &ProjectSelectionPage::moreTemplatesClicked); ui->listView->addWidget(0, getMoreButton); QPushButton* loadButton = new QPushButton(ui->listView); loadButton->setText(i18n("Load Template From File")); loadButton->setIcon(QIcon::fromTheme(QStringLiteral("application-x-archive"))); connect (loadButton, &QPushButton::clicked, this, &ProjectSelectionPage::loadFileClicked); ui->listView->addWidget(0, loadButton); m_wizardDialog = wizardDialog; } void ProjectSelectionPage::nameChanged() { validateData(); emit locationChanged( location() ); } ProjectSelectionPage::~ProjectSelectionPage() { delete ui; } void ProjectSelectionPage::typeChanged(const QModelIndex& idx) { if (!idx.model()) { qCDebug(PLUGIN_APPWIZARD) << "Index with no model"; return; } int children = idx.model()->rowCount(idx); ui->templateType->setVisible(children); ui->templateType->setEnabled(children > 1); if (children) { ui->templateType->setModel(m_templatesModel); ui->templateType->setRootModelIndex(idx); ui->templateType->setCurrentIndex(0); itemChanged(idx.model()->index(0, 0, idx)); } else { itemChanged(idx); } } void ProjectSelectionPage::templateChanged(int current) { QModelIndex idx=m_templatesModel->index(current, 0, ui->templateType->rootModelIndex()); itemChanged(idx); } void ProjectSelectionPage::itemChanged( const QModelIndex& current) { TemplatePreviewIcon icon = current.data(KDevelop::TemplatesModel::PreviewIconRole).value(); QPixmap pixmap = icon.pixmap(); ui->icon->setPixmap(pixmap); ui->icon->setFixedHeight(pixmap.height()); // header name is either from this index directly or the parents if we show the combo box const QVariant headerData = ui->templateType->isVisible() ? current.parent().data() : current.data(); ui->header->setText(QStringLiteral("

    %1

    ").arg(headerData.toString().trimmed())); ui->description->setText(current.data(KDevelop::TemplatesModel::CommentRole).toString()); validateData(); ui->propertiesBox->setEnabled(true); } QString ProjectSelectionPage::selectedTemplate() { QStandardItem *item = currentItem(); if (item) return item->data().toString(); else return QString(); } QUrl ProjectSelectionPage::location() { QUrl url = ui->locationUrl->url().adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + '/' + encodedProjectName()); return url; } QString ProjectSelectionPage::projectName() { return ui->projectNameEdit->text(); } void ProjectSelectionPage::urlEdited() { validateData(); emit locationChanged( location() ); } void ProjectSelectionPage::validateData() { QUrl url = ui->locationUrl->url(); if( !url.isLocalFile() || url.isEmpty() ) { ui->locationValidWidget->setText( i18n("Invalid location") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } if (projectName().isEmpty()) { ui->locationValidWidget->setText( i18n("Empty project name") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } if (!projectName().isEmpty()) { QString projectName = this->projectName(); QString templatefile = m_wizardDialog->appInfo().appTemplate; // Read template file KConfig config(templatefile); KConfigGroup configgroup(&config, "General"); QString pattern = configgroup.readEntry( "ValidProjectName" , "^[a-zA-Z][a-zA-Z0-9_]+$" ); // Validation int pos = 0; QRegExp regex( pattern ); QRegExpValidator validator( regex ); if( validator.validate(projectName, pos) == QValidator::Invalid ) { ui->locationValidWidget->setText( i18n("Invalid project name") ); emit invalid(); return; } } QDir tDir(url.toLocalFile()); while (!tDir.exists() && !tDir.isRoot()) { if (!tDir.cdUp()) { break; } } if (tDir.exists()) { QFileInfo tFileInfo(tDir.absolutePath()); if (!tFileInfo.isWritable() || !tFileInfo.isExecutable()) { ui->locationValidWidget->setText( i18n("Unable to create subdirectories, " "missing permissions on: %1", tDir.absolutePath()) ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } } QStandardItem* item = currentItem(); if( item && !item->hasChildren() ) { ui->locationValidWidget->animatedHide(); emit valid(); } else { ui->locationValidWidget->setText( i18n("Invalid project template, please choose a leaf item") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } // Check for non-empty target directory. Not an error, but need to display a warning. url.setPath( url.path() + '/' + encodedProjectName() ); QFileInfo fi( url.toLocalFile() ); if( fi.exists() && fi.isDir() ) { if( !QDir( fi.absoluteFilePath()).entryList( QDir::NoDotAndDotDot | QDir::AllEntries ).isEmpty() ) { ui->locationValidWidget->setText( i18n("Path already exists and contains files. Open it as a project.") ); ui->locationValidWidget->animatedShow(); emit invalid(); return; } } } QByteArray ProjectSelectionPage::encodedProjectName() { // : < > * ? / \ | " are invalid on windows QByteArray tEncodedName = projectName().toUtf8(); for (int i = 0; i < tEncodedName.size(); ++i) { QChar tChar(tEncodedName.at( i )); if (tChar.isDigit() || tChar.isSpace() || tChar.isLetter() || tChar == '%') continue; QByteArray tReplace = QUrl::toPercentEncoding( tChar ); tEncodedName.replace( tEncodedName.at( i ) ,tReplace ); i = i + tReplace.size() - 1; } return tEncodedName; } QStandardItem* ProjectSelectionPage::currentItem() const { QStandardItem* item = m_templatesModel->itemFromIndex( ui->listView->currentIndex() ); if ( item && item->hasChildren() ) { const int currect = ui->templateType->currentIndex(); const QModelIndex idx = m_templatesModel->index( currect, 0, ui->templateType->rootModelIndex() ); item = m_templatesModel->itemFromIndex(idx); } return item; } bool ProjectSelectionPage::shouldContinue() { QFileInfo fi(location().toLocalFile()); if (fi.exists() && fi.isDir()) { if (!QDir(fi.absoluteFilePath()).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) { int res = KMessageBox::questionYesNo(this, i18n("The specified path already exists and contains files. " "Are you sure you want to proceed?")); return res == KMessageBox::Yes; } } return true; } void ProjectSelectionPage::loadFileClicked() { const QStringList supportedMimeTypes { QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), QStringLiteral("application/zip") }; ScopedDialog fileDialog(this, i18n("Load Template From File")); fileDialog->setMimeTypeFilters(supportedMimeTypes); fileDialog->setFileMode(QFileDialog::ExistingFiles); if (!fileDialog->exec()) { return; } for (const auto& fileName : fileDialog->selectedFiles()) { QString destination = m_templatesModel->loadTemplateFile(fileName); QModelIndexList indexes = m_templatesModel->templateIndexes(destination); if (indexes.size() > 2) { ui->listView->setCurrentIndex(indexes.at(1)); ui->templateType->setCurrentIndex(indexes.at(2).row()); } } } void ProjectSelectionPage::moreTemplatesClicked() { ScopedDialog dialog(QStringLiteral("kdevappwizard.knsrc"), this); if (!dialog->exec()) return; auto entries = dialog->changedEntries(); if (entries.isEmpty()) { return; } m_templatesModel->refresh(); bool updated = false; foreach (const KNS3::Entry& entry, entries) { if (!entry.installedFiles().isEmpty()) { updated = true; setCurrentTemplate(entry.installedFiles().at(0)); break; } } if (!updated) { ui->listView->setCurrentIndex(QModelIndex()); } } void ProjectSelectionPage::setCurrentTemplate (const QString& fileName) { QModelIndexList indexes = m_templatesModel->templateIndexes(fileName); if (indexes.size() > 1) { ui->listView->setCurrentIndex(indexes.at(1)); } if (indexes.size() > 2) { ui->templateType->setCurrentIndex(indexes.at(2).row()); } } diff --git a/plugins/bazaar/bazaarplugin.cpp b/plugins/bazaar/bazaarplugin.cpp index ff2ffbf001..f5f25534da 100644 --- a/plugins/bazaar/bazaarplugin.cpp +++ b/plugins/bazaar/bazaarplugin.cpp @@ -1,345 +1,349 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * 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 "bazaarplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include "bazaarutils.h" #include "bzrannotatejob.h" #include "copyjob.h" #include "diffjob.h" using namespace KDevelop; BazaarPlugin::BazaarPlugin(QObject* parent, const QVariantList& args) : IPlugin(QStringLiteral("kdevbazaar"), parent), m_vcsPluginHelper(new KDevelop::VcsPluginHelper(this, this)) { Q_UNUSED(args); // What is this? if (QStandardPaths::findExecutable(QStringLiteral("bzr")).isEmpty()) { setErrorDescription(i18n("Unable to find Bazaar (bzr) executable. Is it installed on the system?")); return; } setObjectName(QStringLiteral("Bazaar")); } BazaarPlugin::~BazaarPlugin() { } QString BazaarPlugin::name() const { return QStringLiteral("Bazaar"); } bool BazaarPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) { const QString scheme = remoteLocation.scheme(); if (scheme == QLatin1String("bzr") || scheme == QLatin1String("bzr+ssh") || scheme == QLatin1String("lp")) { return true; } return false; } VcsJob* BazaarPlugin::add(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Add); *job << "bzr" << "add"; if(recursion == NonRecursive) *job << "--no-recurse"; *job << localLocations; return job; } VcsJob* BazaarPlugin::annotate(const QUrl& localLocation, const VcsRevision& rev) { VcsJob* job = new BzrAnnotateJob(BazaarUtils::workingCopy(localLocation), BazaarUtils::getRevisionSpec(rev), localLocation, this, KDevelop::OutputJob::Silent); return job; } VcsJob* BazaarPlugin::commit(const QString& message, const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { QDir dir = BazaarUtils::workingCopy(localLocations[0]); DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); *job << "bzr" << "commit" << BazaarUtils::handleRecursion(localLocations, recursion) << "-m" << message; return job; } VcsJob* BazaarPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { return new CopyJob(localLocationSrc, localLocationDstn, this); } VcsImportMetadataWidget* BazaarPlugin::createImportMetadataWidget(QWidget* parent) { return new DvcsImportMetadataWidget(parent); } VcsJob* BazaarPlugin::createWorkingCopy(const VcsLocation& sourceRepository, const QUrl& destinationDirectory, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); // What is the purpose of recursion parameter? DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(sourceRepository.localUrl()), this); job->setType(VcsJob::Import); *job << "bzr" << "branch" << sourceRepository.localUrl().url() << destinationDirectory; return job; } VcsJob* BazaarPlugin::diff(const QUrl& fileOrDirectory, const VcsRevision& srcRevision, const VcsRevision& dstRevision, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); VcsJob* job = new DiffJob(BazaarUtils::workingCopy(fileOrDirectory), BazaarUtils::getRevisionSpecRange(srcRevision, dstRevision), fileOrDirectory, this); return job; } VcsJob* BazaarPlugin::init(const QUrl& localRepositoryRoot) { DVcsJob* job = new DVcsJob(BazaarUtils::toQDir(localRepositoryRoot), this); job->setType(VcsJob::Import); *job << "bzr" << "init"; return job; } bool BazaarPlugin::isVersionControlled(const QUrl& localLocation) { QDir workCopy = BazaarUtils::workingCopy(localLocation); DVcsJob* job = new DVcsJob(workCopy, this, OutputJob::Silent); job->setType(VcsJob::Unknown); job->setIgnoreError(true); *job << "bzr" << "ls" << "--from-root" << "-R" << "-V"; job->exec(); if (job->status() == VcsJob::JobSucceeded) { QList filesAndDirectoriesList; - foreach (const QString& fod, job->output().split('\n')) { + const auto output = job->output().split('\n'); + filesAndDirectoriesList.reserve(output.size()); + for (const auto& fod : output) { filesAndDirectoriesList.append(QFileInfo(workCopy.absolutePath() + QDir::separator() + fod)); } QFileInfo fi(localLocation.toLocalFile()); if (fi.isDir() || fi.isFile()) { QFileInfo file(localLocation.toLocalFile()); return filesAndDirectoriesList.contains(file); } } return false; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, long unsigned int limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(rev) << "-l" << QString::number(limit); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } VcsJob* BazaarPlugin::log(const QUrl& localLocation, const VcsRevision& rev, const VcsRevision& limit) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::Log); *job << "bzr" << "log" << "--long" << "-v" << localLocation << BazaarUtils::getRevisionSpecRange(limit, rev); connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrLog); return job; } void BazaarPlugin::parseBzrLog(DVcsJob* job) { QVariantList result; auto parts = job->output().split(QStringLiteral("------------------------------------------------------------"), QString::SkipEmptyParts); foreach (const QString& part, parts) { auto event = BazaarUtils::parseBzrLogPart(part); if (event.revision().revisionType() != VcsRevision::Invalid) result.append(QVariant::fromValue(event)); } job->setResults(result); } VcsJob* BazaarPlugin::move(const QUrl& localLocationSrc, const QUrl& localLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocationSrc), this); job->setType(VcsJob::JobType::Move); *job << "bzr" << "move" << localLocationSrc << localLocationDst; return job; } VcsJob* BazaarPlugin::pull(const VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { // API describes hg pull which is git fetch equivalent // bzr has pull, but it succeeds only if fast-forward is possible // in other cases bzr merge should be used instead (bzr pull would fail) // Information about repository must be provided at least once. DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Pull); *job << "bzr" << "pull"; if (!localOrRepoLocationSrc.localUrl().isEmpty()) { *job << localOrRepoLocationSrc.localUrl(); } // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::push(const QUrl& localRepositoryLocation, const VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localRepositoryLocation), this); job->setType(VcsJob::JobType::Push); *job << "bzr" << "push" << localOrRepoLocationDst.localUrl(); // localUrl always makes sense. Even on remote repositores which are handled // transparently. return job; } VcsJob* BazaarPlugin::remove(const QList& localLocations) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Remove); *job << "bzr" << "remove" << localLocations; return job; } VcsJob* BazaarPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocation), this); job->setType(VcsJob::JobType::Unknown); *job << "bzr" << "root" << localLocation; // It is only to make sure connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrRoot); return job; } void BazaarPlugin::parseBzrRoot(DVcsJob* job) { QString filename = job->dvcsCommand().at(2); QString rootDirectory = job->output(); QString localFilename = QFileInfo(QUrl::fromLocalFile(filename).toLocalFile()).absoluteFilePath(); QString result = localFilename.mid(localFilename.indexOf(rootDirectory) + rootDirectory.length()); job->setResults(QVariant::fromValue(result)); } VcsJob* BazaarPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); // How to provide "a conflict solving dialog to the user"? // In any case this plugin is unable to make any conflict. } VcsJob* BazaarPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::JobType::Revert); *job << "bzr" << "revert" << BazaarUtils::handleRecursion(localLocations, recursion); return job; } VcsJob* BazaarPlugin::status(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { Q_UNUSED(recursion); DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); job->setType(VcsJob::Status); *job << "bzr" << "status" << "--short" << "--no-pending" << "--no-classify" << localLocations; connect(job, &DVcsJob::readyForParsing, this, &BazaarPlugin::parseBzrStatus); return job; } void BazaarPlugin::parseBzrStatus(DVcsJob* job) { QVariantList result; QSet filesWithStatus; QDir workingCopy = job->directory(); - foreach (const QString& line, job->output().split('\n')) { + const auto output = job->output().split('\n'); + result.reserve(output.size()); + for (const auto& line : output) { auto status = BazaarUtils::parseVcsStatusInfoLine(line); result.append(QVariant::fromValue(status)); filesWithStatus.insert(BazaarUtils::concatenatePath(workingCopy, status.url())); } QStringList command = job->dvcsCommand(); for (auto it = command.constBegin() + command.indexOf(QStringLiteral("--no-classify")) + 1, itEnd = command.constEnd(); it != itEnd; ++it) { QString path = QFileInfo(*it).absoluteFilePath(); if (!filesWithStatus.contains(path)) { filesWithStatus.insert(path); KDevelop::VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUpToDate); status.setUrl(QUrl::fromLocalFile(*it)); result.append(QVariant::fromValue(status)); } } job->setResults(result); } VcsJob* BazaarPlugin::update(const QList& localLocations, const VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { // bzr update is stronger than API (it's effectively merge) // the best approximation is bzr pull DVcsJob* job = new DVcsJob(BazaarUtils::workingCopy(localLocations[0]), this); Q_UNUSED(recursion); // recursion and file locations are ignored - we can update only whole // working copy job->setType(VcsJob::JobType::Update); *job << "bzr" << "pull" << BazaarUtils::getRevisionSpec(rev); return job; } VcsLocationWidget* BazaarPlugin::vcsLocation(QWidget* parent) const { return new KDevelop::StandardVcsLocationWidget(parent); } ContextMenuExtension BazaarPlugin::contextMenuExtension(Context* context, QWidget* parent) { m_vcsPluginHelper->setupFromContext(context); QList const& ctxUrlList = m_vcsPluginHelper->contextUrlList(); bool isWorkingDirectory = false; for (const QUrl & url : ctxUrlList) { if (BazaarUtils::isValidDirectory(url)) { isWorkingDirectory = true; break; } } if (!isWorkingDirectory) { // Not part of a repository return ContextMenuExtension(); } QMenu* menu = m_vcsPluginHelper->commonActions(parent); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, menu->menuAction()); return menuExt; } diff --git a/plugins/bazaar/bazaarutils.cpp b/plugins/bazaar/bazaarutils.cpp index 418d742a91..12bded763b 100644 --- a/plugins/bazaar/bazaarutils.cpp +++ b/plugins/bazaar/bazaarutils.cpp @@ -1,244 +1,244 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * 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 "bazaarutils.h" #include #include #include #include #include QDir BazaarUtils::toQDir(const QUrl& url) { return QDir(url.toLocalFile()); } QDir BazaarUtils::workingCopy(const QUrl& path) { QDir dir = BazaarUtils::toQDir(path); while (!dir.exists(QStringLiteral(".bzr")) && dir.cdUp()); return dir; } QString BazaarUtils::getRevisionSpec(const KDevelop::VcsRevision& revision) { if (revision.revisionType() == KDevelop::VcsRevision::Special) { if (revision.specialType() == KDevelop::VcsRevision::Head) return QStringLiteral("-rlast:1"); else if (revision.specialType() == KDevelop::VcsRevision::Base) return QString(); // Workaround strange KDevelop behaviour else if (revision.specialType() == KDevelop::VcsRevision::Working) return QString(); else if (revision.specialType() == KDevelop::VcsRevision::Start) return QStringLiteral("-r1"); else return QString(); // Don't know how to handle this situation } else if (revision.revisionType() == KDevelop::VcsRevision::GlobalNumber) return QStringLiteral("-r") + QString::number(revision.revisionValue().toLongLong()); else return QString(); // Don't know how to handle this situation } QString BazaarUtils::getRevisionSpecRange(const KDevelop::VcsRevision& end) { if (end.revisionType() == KDevelop::VcsRevision::Special) { if (end.specialType() == KDevelop::VcsRevision::Head) { return QStringLiteral("-r..last:1"); } else if (end.specialType() == KDevelop::VcsRevision::Base) { return QStringLiteral("-r..last:1"); // Workaround strange KDevelop behaviour } else if (end.specialType() == KDevelop::VcsRevision::Working) { return QString(); } else if (end.specialType() == KDevelop::VcsRevision::Start) { return QStringLiteral("-..r1"); } else { return QString(); // Don't know how to handle this situation } } else if (end.revisionType() == KDevelop::VcsRevision::GlobalNumber) { return QStringLiteral("-r") + QString::number(end.revisionValue().toLongLong()); } return QString(); // Don't know how to handle this situation } QString BazaarUtils::getRevisionSpecRange(const KDevelop::VcsRevision& begin, const KDevelop::VcsRevision& end) { if (begin.revisionType() == KDevelop::VcsRevision::Special) { if (begin.specialType() == KDevelop::VcsRevision::Previous) { if (end.revisionType() == KDevelop::VcsRevision::Special) { if (end.specialType() == KDevelop::VcsRevision::Base || end.specialType() == KDevelop::VcsRevision::Head) return QStringLiteral("-rlast:2..last:1"); else if (end.specialType() == KDevelop::VcsRevision::Working) return QString(); else if (end.specialType() == KDevelop::VcsRevision::Start) return QStringLiteral("-r0..1"); // That's wrong revision range } else if (end.revisionType() == KDevelop::VcsRevision::GlobalNumber) return QStringLiteral("-r") + QString::number(end.revisionValue().toLongLong() - 1) + ".." + QString::number(end.revisionValue().toLongLong()); else return QString(); // Don't know how to handle this situation } else if (begin.specialType() == KDevelop::VcsRevision::Base || begin.specialType() == KDevelop::VcsRevision::Head) { // Only one possibility: comparing working copy to last commit return QString(); } } else if (begin.revisionType() == KDevelop::VcsRevision::GlobalNumber) { if (end.revisionType() == KDevelop::VcsRevision::Special) { // Assuming working copy return QStringLiteral("-r") + QString::number(begin.revisionValue().toLongLong()); } else { return QStringLiteral("-r") + QString::number(begin.revisionValue().toLongLong()) + ".." + QString::number(end.revisionValue().toLongLong()); } } return QString(); // Don't know how to handle this situation } bool BazaarUtils::isValidDirectory(const QUrl& dirPath) { QDir dir = BazaarUtils::workingCopy(dirPath); return dir.cd(QStringLiteral(".bzr")) && dir.exists(QStringLiteral("branch")); } KDevelop::VcsStatusInfo BazaarUtils::parseVcsStatusInfoLine(const QString& line) { QStringList tokens = line.split(' ', QString::SkipEmptyParts); KDevelop::VcsStatusInfo result; if (tokens.size() < 2) // Don't know how to handle this situation (it is an error) return result; result.setUrl(QUrl::fromLocalFile(tokens.back())); if (tokens[0] == QLatin1String("M")) { result.setState(KDevelop::VcsStatusInfo::ItemModified); } else if (tokens[0] == QLatin1String("C")) { result.setState(KDevelop::VcsStatusInfo::ItemHasConflicts); } else if (tokens[0] == QLatin1String("+N")) { result.setState(KDevelop::VcsStatusInfo::ItemAdded); } else if (tokens[0] == QLatin1String("?")) { result.setState(KDevelop::VcsStatusInfo::ItemUnknown); } else if (tokens[0] == QLatin1String("D")) { result.setState(KDevelop::VcsStatusInfo::ItemDeleted); } else { result.setState(KDevelop::VcsStatusInfo::ItemUserState); qWarning() << "Unsupported status: " << tokens[0]; } return result; } QString BazaarUtils::concatenatePath(const QDir& workingCopy, const QUrl& pathInWorkingCopy) { return QFileInfo(workingCopy.absolutePath() + QDir::separator() + pathInWorkingCopy.toLocalFile()).absoluteFilePath(); } KDevelop::VcsEvent BazaarUtils::parseBzrLogPart(const QString& output) { const QStringList outputLines = output.split('\n'); KDevelop::VcsEvent commitInfo; bool atMessage = false; QString message; bool afterMessage = false; QHash fileToActionsMapping; KDevelop::VcsItemEvent::Action currentAction; for (const QString &line : outputLines) { if (!atMessage) { if (line.startsWith(QStringLiteral("revno"))) { QString revno = line.mid(QStringLiteral("revno: ").length()); revno = revno.left(revno.indexOf(' ')); KDevelop::VcsRevision revision; revision.setRevisionValue(revno.toLongLong(), KDevelop::VcsRevision::GlobalNumber); commitInfo.setRevision(revision); } else if (line.startsWith(QStringLiteral("committer: "))) { QString commiter = line.mid(QStringLiteral("committer: ").length()); commitInfo.setAuthor(commiter); // Author goes after commiter, but only if is different } else if (line.startsWith(QStringLiteral("author"))) { QString author = line.mid(QStringLiteral("author: ").length()); commitInfo.setAuthor(author); // It may override commiter (In fact commiter is not supported by VcsEvent) } else if (line.startsWith(QStringLiteral("timestamp"))) { const QString formatString = QStringLiteral("yyyy-MM-dd hh:mm:ss"); QString timestamp = line.mid(QStringLiteral("timestamp: ddd ").length(), formatString.length()); commitInfo.setDate(QDateTime::fromString(timestamp, formatString)); } else if (line.startsWith(QStringLiteral("message"))) { atMessage = true; } } else if (atMessage && !afterMessage) { if (!line.isEmpty() && line[0].isSpace()) { - message += line.trimmed() + "\n"; + message += line.trimmed() + QLatin1Char('\n'); } else if (!line.isEmpty()) { afterMessage = true; // leave atMessage = true currentAction = BazaarUtils::parseActionDescription(line); } // if line is empty - ignore and get next } else if (afterMessage) { if (!line.isEmpty() && !line[0].isSpace()) { currentAction = BazaarUtils::parseActionDescription(line); } else if (!line.isEmpty()) { fileToActionsMapping[line.trimmed()] |= currentAction; } // if line is empty - ignore and get next } } if (atMessage) commitInfo.setMessage(message.trimmed()); for (auto i = fileToActionsMapping.begin(); i != fileToActionsMapping.end(); ++i) { KDevelop::VcsItemEvent itemEvent; itemEvent.setRepositoryLocation(i.key()); itemEvent.setActions(i.value()); commitInfo.addItem(itemEvent); } return commitInfo; } KDevelop::VcsItemEvent::Action BazaarUtils::parseActionDescription(const QString& action) { if (action == QLatin1String("added:")) { return KDevelop::VcsItemEvent::Added; } else if (action == QLatin1String("modified:")) { return KDevelop::VcsItemEvent::Modified; } else if (action == QLatin1String("removed:")) { return KDevelop::VcsItemEvent::Deleted; } else if (action == QLatin1String("kind changed:")) { return KDevelop::VcsItemEvent::Replaced; // Best approximation } else if (action.startsWith(QStringLiteral("renamed"))) { return KDevelop::VcsItemEvent::Modified; // Best approximation } else { qCritical("Unsupported action: %s", action.toLocal8Bit().constData()); return KDevelop::VcsItemEvent::Action(); } } QList BazaarUtils::handleRecursion(const QList& listOfUrls, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (recursion == KDevelop::IBasicVersionControl::Recursive) { return listOfUrls; // Nothing to do } else { QList result; foreach (const auto& url, listOfUrls) { if (url.isLocalFile() && QFileInfo(url.toLocalFile()).isFile()) { result.push_back(url); } } return result; } } diff --git a/plugins/bazaar/bzrannotatejob.cpp b/plugins/bazaar/bzrannotatejob.cpp index b758ecda7d..682f38abe9 100644 --- a/plugins/bazaar/bzrannotatejob.cpp +++ b/plugins/bazaar/bzrannotatejob.cpp @@ -1,197 +1,197 @@ /*************************************************************************** * Copyright 2013-2014 Maciej Poleski * * * * 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 "bzrannotatejob.h" #include #include #include #include #include #include #include #include using namespace KDevelop; BzrAnnotateJob::BzrAnnotateJob(const QDir& workingDir, const QString& revisionSpec, const QUrl& localLocation, KDevelop::IPlugin* parent, KDevelop::OutputJob::OutputJobVerbosity verbosity) : VcsJob(parent, verbosity), m_workingDir(workingDir), m_revisionSpec(revisionSpec), m_localLocation(localLocation), m_vcsPlugin(parent), m_status(KDevelop::VcsJob::JobNotStarted) { setType(JobType::Annotate); setCapabilities(Killable); } bool BzrAnnotateJob::doKill() { m_status = KDevelop::VcsJob::JobCanceled; if (m_job) return m_job->kill(KJob::Quietly); else return true; } void BzrAnnotateJob::start() { if (m_status != KDevelop::VcsJob::JobNotStarted) return; DVcsJob* job = new KDevelop::DVcsJob(m_workingDir, m_vcsPlugin, KDevelop::OutputJob::Silent); *job << "bzr" << "annotate" << "--all" << m_revisionSpec << m_localLocation; connect(job, &DVcsJob::readyForParsing, this, &BzrAnnotateJob::parseBzrAnnotateOutput); m_status = VcsJob::JobRunning; m_job = job; job->start(); } void BzrAnnotateJob::parseBzrAnnotateOutput(KDevelop::DVcsJob* job) { m_outputLines = job->output().split('\n'); m_currentLine = 0; if (m_status == KDevelop::VcsJob::JobRunning) QTimer::singleShot(0, this, &BzrAnnotateJob::parseNextLine); } void BzrAnnotateJob::parseNextLine() { for(;;) { Q_ASSERT(m_currentLine<=m_outputLines.size()); if (m_currentLine == m_outputLines.size()) { m_status = KDevelop::VcsJob::JobSucceeded; emitResult(); emit resultsReady(this); break; } QString currentLine = m_outputLines[m_currentLine]; if (currentLine.isEmpty()) { ++m_currentLine; continue; } bool revOk; auto revision = currentLine.leftRef(currentLine.indexOf(' ')).toULong(&revOk); if (!revOk) { // Future compatibility - not a revision yet ++m_currentLine; continue; } auto i = m_commits.find(revision); if (i != m_commits.end()) { KDevelop::VcsAnnotationLine line; line.setAuthor(i.value().author()); line.setCommitMessage(i.value().message()); line.setDate(i.value().date()); line.setLineNumber(m_currentLine); line.setRevision(i.value().revision()); m_results.append(QVariant::fromValue(line)); ++m_currentLine; continue; } else { prepareCommitInfo(revision); break; //Will reenter this function when commit info will be ready } } } void BzrAnnotateJob::prepareCommitInfo(std::size_t revision) { if (m_status != KDevelop::VcsJob::JobRunning) return; KDevelop::DVcsJob* job = new KDevelop::DVcsJob(m_workingDir, m_vcsPlugin, KDevelop::OutputJob::Silent); job->setType(KDevelop::VcsJob::Log); *job << "bzr" << "log" << "--long" << "-r" << QString::number(revision); connect(job, &DVcsJob::readyForParsing, this, &BzrAnnotateJob::parseBzrLog); m_job = job; job->start(); } /* * This is slightly different from BazaarUtils::parseBzrLogPart(...). * This function parses only commit general info. It does not parse signle * actions. In fact output parsed by this function is slightly different * from output parsed by BazaarUtils. As a result parsing this output using * BazaarUtils would yield different results. * NOTE: This is all about parsing 'message'. */ void BzrAnnotateJob::parseBzrLog(KDevelop::DVcsJob* job) { QStringList outputLines = job->output().split('\n'); KDevelop::VcsEvent commitInfo; int revision=-1; bool atMessage = false; QString message; foreach (const QString &line, outputLines) { if (!atMessage) { if (line.startsWith(QStringLiteral("revno"))) { QString revno = line.mid(QStringLiteral("revno: ").length()); // In future there is possibility that "revno: " will change to // "revno??". If that's all, then we recover matching only // "revno" prefix and assuming placeholder of length 2 (": " or // "??"). // The same below with exception of "commiter" which possibly // can have also some suffix which changes meaning like // "commiter-some_property: "... revno = revno.left(revno.indexOf(' ')); revision = revno.toInt(); KDevelop::VcsRevision revision; revision.setRevisionValue(revno.toLongLong(), KDevelop::VcsRevision::GlobalNumber); commitInfo.setRevision(revision); } else if (line.startsWith(QStringLiteral("committer: "))) { QString commiter = line.mid(QStringLiteral("committer: ").length()); commitInfo.setAuthor(commiter); // Author goes after commiter, but only if is different } else if (line.startsWith(QStringLiteral("author"))) { QString author = line.mid(QStringLiteral("author: ").length()); commitInfo.setAuthor(author); // It may override commiter (In fact commiter is not supported by VcsEvent) } else if (line.startsWith(QStringLiteral("timestamp"))) { const QString formatString = QStringLiteral("yyyy-MM-dd hh:mm:ss"); QString timestamp = line.mid(QStringLiteral("timestamp: ddd ").length(), formatString.length()); commitInfo.setDate(QDateTime::fromString(timestamp, formatString)); } else if (line.startsWith(QStringLiteral("message"))) { atMessage = true; } } else { - message += line.trimmed() + "\n"; + message += line.trimmed() + QLatin1Char('\n'); } } if (atMessage) commitInfo.setMessage(message.trimmed()); Q_ASSERT(revision!=-1); m_commits[revision] = commitInfo; // Invoke from event loop to protect against stack overflow (it could happen // on very big files with very big history of changes if tail-recursion // optimization had failed here). QTimer::singleShot(0, this, &BzrAnnotateJob::parseNextLine); } QVariant BzrAnnotateJob::fetchResults() { return m_results; } KDevelop::VcsJob::JobStatus BzrAnnotateJob::status() const { return m_status; } KDevelop::IPlugin* BzrAnnotateJob::vcsPlugin() const { return m_vcsPlugin; } diff --git a/plugins/clang/clangsupport.cpp b/plugins/clang/clangsupport.cpp index 528f02eadb..3e17ec270f 100644 --- a/plugins/clang/clangsupport.cpp +++ b/plugins/clang/clangsupport.cpp @@ -1,438 +1,438 @@ /* This file is part of KDevelop Copyright 2013 Olivier de Gaalon Copyright 2013 Milian Wolff Copyright 2014 Kevin Funk This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "clangsupport.h" #include "clangparsejob.h" #include "util/clangdebug.h" #include "util/clangtypes.h" #include "util/clangutils.h" #include "codecompletion/model.h" #include "clanghighlighting.h" #include #include #include #include #include "codegen/clangrefactoring.h" #include "codegen/clangclasshelper.h" #include "codegen/adaptsignatureassistant.h" #include "duchain/documentfinderhelpers.h" #include "duchain/clangindex.h" #include "duchain/navigationwidget.h" #include "duchain/macrodefinition.h" #include "duchain/clangparsingenvironmentfile.h" #include "duchain/duchainutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "clangsettings/sessionsettings/sessionsettings.h" #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KDevClangSupportFactory, "kdevclangsupport.json", registerPlugin(); ) using namespace KDevelop; namespace { QPair lineInDocument(const QUrl &url, const KTextEditor::Cursor& position) { KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (!doc || !doc->textDocument() || !ICore::self()->documentController()->activeTextDocumentView()) { return {}; } const int lineNumber = position.line(); const int lineLength = doc->textDocument()->lineLength(lineNumber); KTextEditor::Range range(lineNumber, 0, lineNumber, lineLength); QString line = doc->textDocument()->text(range); return {line, range}; } QPair importedContextForPosition(const QUrl &url, const KTextEditor::Cursor& position) { auto pair = lineInDocument(url, position); const QString line = pair.first; if (line.isEmpty()) return {{}, KTextEditor::Range::invalid()}; KTextEditor::Range wordRange = ClangUtils::rangeForIncludePathSpec(line, pair.second); if (!wordRange.isValid()) { return {{}, KTextEditor::Range::invalid()}; } // Since this is called by the editor while editing, use a fast timeout so the editor stays responsive DUChainReadLocker lock(nullptr, 100); if (!lock.locked()) { clangDebug() << "Failed to lock the du-chain in time"; return {TopDUContextPointer(), KTextEditor::Range::invalid()}; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (line.isEmpty() || !topContext || !topContext->parsingEnvironmentFile()) { return {TopDUContextPointer(), KTextEditor::Range::invalid()}; } // It's an #include, find out which file was included at the given line foreach(const DUContext::Import &imported, topContext->importedParentContexts()) { auto context = imported.context(nullptr); if (context) { if(topContext->transformFromLocalRevision(topContext->importPosition(context)).line() == wordRange.start().line()) { if (auto importedTop = dynamic_cast(context)) return {TopDUContextPointer(importedTop), wordRange}; } } } // The last resort. Check if the file is already included (maybe recursively from another files). // This is needed as clang doesn't visit (clang_getInclusions) those inclusions. // TODO: Maybe create an assistant that'll report whether the file is already included? auto includeName = line.mid(wordRange.start().column(), wordRange.end().column() - wordRange.start().column()); if (!includeName.isEmpty()) { if (includeName.startsWith(QLatin1Char('.'))) { const Path dir = Path(url).parent(); includeName = Path(dir, includeName).toLocalFile(); } const auto recursiveImports = topContext->recursiveImportIndices(); auto iterator = recursiveImports.iterator(); while (iterator) { const auto str = (*iterator).url().str(); if (str == includeName || (str.endsWith(includeName) && str[str.size()-includeName.size()-1] == QLatin1Char('/'))) { return {TopDUContextPointer((*iterator).data()), wordRange}; } ++iterator; } } return {{}, KTextEditor::Range::invalid()}; } QPair macroExpansionForPosition(const QUrl &url, const KTextEditor::Cursor& position) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (topContext) { int useAt = topContext->findUseAt(topContext->transformToLocalRevision(position)); if (useAt >= 0) { Use use = topContext->uses()[useAt]; if (dynamic_cast(use.usedDeclaration(topContext))) { return {TopDUContextPointer(topContext), use}; } } } return {{}, Use()}; } } ClangSupport::ClangSupport(QObject* parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevclangsupport"), parent ) , ILanguageSupport() , m_highlighting(nullptr) , m_refactoring(nullptr) , m_index(nullptr) { { const auto builtinDir = ClangIntegration::DUChainUtils::clangBuiltinIncludePath(); const auto headerToCheck = QLatin1String("cpuid.h"); - if (!QFile::exists(builtinDir + QLatin1String("/") + headerToCheck)) { + if (!QFile::exists(builtinDir + QLatin1Char('/') + headerToCheck)) { setErrorDescription(i18n("The clang builtin include path \"%1\" is invalid (missing %2 header).\n" "Try setting the KDEV_CLANG_BUILTIN_DIR environment variable manually to fix this.\n" "See also: https://bugs.kde.org/show_bug.cgi?id=393779", builtinDir, headerToCheck)); return; } } setXMLFile( QStringLiteral("kdevclangsupport.rc") ); ClangIntegration::DUChainUtils::registerDUChainItems(); m_highlighting = new ClangHighlighting(this); m_refactoring = new ClangRefactoring(this); m_index.reset(new ClangIndex); auto model = new KDevelop::CodeCompletion( this, new ClangCodeCompletionModel(m_index.data(), this), name() ); // TODO: use direct signal/slot connect syntax for 5.1 connect(model, &CodeCompletion::registeredToView, this, &ClangSupport::disableKeywordCompletion); connect(model, &CodeCompletion::unregisteredFromView, this, &ClangSupport::enableKeywordCompletion); for(const auto& type : DocumentFinderHelpers::mimeTypesList()){ KDevelop::IBuddyDocumentFinder::addFinder(type, this); } auto assistantsManager = core()->languageController()->staticAssistantsManager(); assistantsManager->registerAssistant(StaticAssistant::Ptr(new RenameAssistant(this))); assistantsManager->registerAssistant(StaticAssistant::Ptr(new AdaptSignatureAssistant(this))); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ClangSupport::documentActivated); } ClangSupport::~ClangSupport() { parseLock()->lockForWrite(); // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state parseLock()->unlock(); for(const auto& type : DocumentFinderHelpers::mimeTypesList()) { KDevelop::IBuddyDocumentFinder::removeFinder(type); } ClangIntegration::DUChainUtils::unregisterDUChainItems(); } KDevelop::ConfigPage* ClangSupport::configPage(int number, QWidget* parent) { return number == 0 ? new SessionSettings(parent) : nullptr; } int ClangSupport::configPages() const { return 1; } ParseJob* ClangSupport::createParseJob(const IndexedString& url) { return new ClangParseJob(url, this); } QString ClangSupport::name() const { return QStringLiteral("clang"); } ICodeHighlighting* ClangSupport::codeHighlighting() const { return m_highlighting; } BasicRefactoring* ClangSupport::refactoring() const { return m_refactoring; } ICreateClassHelper* ClangSupport::createClassHelper() const { return new ClangClassHelper; } ClangIndex* ClangSupport::index() { return m_index.data(); } bool ClangSupport::areBuddies(const QUrl &url1, const QUrl& url2) { return DocumentFinderHelpers::areBuddies(url1, url2); } bool ClangSupport::buddyOrder(const QUrl &url1, const QUrl& url2) { return DocumentFinderHelpers::buddyOrder(url1, url2); } QVector ClangSupport::potentialBuddies(const QUrl& url) const { return DocumentFinderHelpers::potentialBuddies(url); } void ClangSupport::createActionsForMainWindow (Sublime::MainWindow* /*window*/, QString& _xmlFile, KActionCollection& actions) { _xmlFile = xmlFile(); QAction* renameDeclarationAction = actions.addAction(QStringLiteral("code_rename_declaration")); renameDeclarationAction->setText( i18n("Rename Declaration") ); renameDeclarationAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); actions.setDefaultShortcut(renameDeclarationAction, Qt::CTRL | Qt::SHIFT | Qt::Key_R); connect(renameDeclarationAction, &QAction::triggered, m_refactoring, &ClangRefactoring::executeRenameAction); QAction* moveIntoSourceAction = actions.addAction(QStringLiteral("code_move_definition")); moveIntoSourceAction->setText(i18n("Move into Source")); actions.setDefaultShortcut(moveIntoSourceAction, Qt::CTRL | Qt::ALT | Qt::Key_S); connect(moveIntoSourceAction, &QAction::triggered, m_refactoring, &ClangRefactoring::executeMoveIntoSourceAction); } KDevelop::ContextMenuExtension ClangSupport::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { ContextMenuExtension cm; EditorContext *ec = dynamic_cast(context); if (ec && ICore::self()->languageController()->languagesForUrl(ec->url()).contains(this)) { // It's a C++ file, let's add our context menu. m_refactoring->fillContextMenu(cm, context, parent); } return cm; } KTextEditor::Range ClangSupport::specialLanguageObjectRange(const QUrl &url, const KTextEditor::Cursor& position) { DUChainReadLocker lock; const QPair macroExpansion = macroExpansionForPosition(url, position); if (macroExpansion.first) { return macroExpansion.first->transformFromLocalRevision(macroExpansion.second.m_range); } const QPair import = importedContextForPosition(url, position); if(import.first) { return import.second; } return KTextEditor::Range::invalid(); } QPair ClangSupport::specialLanguageObjectJumpCursor(const QUrl &url, const KTextEditor::Cursor& position) { const QPair import = importedContextForPosition(url, position); DUChainReadLocker lock; if (import.first) { return qMakePair(import.first->url().toUrl(), KTextEditor::Cursor(0,0)); } return {{}, KTextEditor::Cursor::invalid()}; } QWidget* ClangSupport::specialLanguageObjectNavigationWidget(const QUrl &url, const KTextEditor::Cursor& position) { DUChainReadLocker lock; const QPair macroExpansion = macroExpansionForPosition(url, position); if (macroExpansion.first) { Declaration* declaration = macroExpansion.second.usedDeclaration(macroExpansion.first.data()); const MacroDefinition::Ptr macroDefinition(dynamic_cast(declaration)); Q_ASSERT(macroDefinition); auto rangeInRevision = macroExpansion.first->transformFromLocalRevision(macroExpansion.second.m_range.start); return new ClangNavigationWidget(macroDefinition, DocumentCursor(IndexedString(url), rangeInRevision)); } const QPair import = importedContextForPosition(url, position); if (import.first) { return import.first->createNavigationWidget(); } return nullptr; } TopDUContext* ClangSupport::standardContext(const QUrl &url, bool /*proxyContext*/) { ClangParsingEnvironment env; return DUChain::self()->chainForDocument(url, &env); } void ClangSupport::documentActivated(IDocument* doc) { TopDUContext::Features features; { DUChainReadLocker lock; auto ctx = DUChainUtils::standardContextForUrl(doc->url()); if (!ctx) { return; } auto file = ctx->parsingEnvironmentFile(); if (!file) { return; } if (file->type() != CppParsingEnvironment) { return; } if (file->needsUpdate()) { return; } features = ctx->features(); } const auto indexedUrl = IndexedString(doc->url()); auto sessionData = ClangIntegration::DUChainUtils::findParseSessionData(indexedUrl, index()->translationUnitForUrl(IndexedString(doc->url()))); if (sessionData) { return; } if ((features & TopDUContext::AllDeclarationsContextsAndUses) != TopDUContext::AllDeclarationsContextsAndUses) { // the file was parsed in simplified mode, we need to reparse it to get all data // now that its opened in the editor features = TopDUContext::AllDeclarationsContextsAndUses; } else { features = static_cast(ClangParseJob::AttachASTWithoutUpdating | features); if (ICore::self()->languageController()->backgroundParser()->isQueued(indexedUrl)) { // The document is already scheduled for parsing (happens when opening a project with an active document) // The background parser will optimize the previous request out, so we need to update highlighting features = static_cast(ClangParseJob::UpdateHighlighting | features); } } ICore::self()->languageController()->backgroundParser()->addDocument(indexedUrl, features); } static void setKeywordCompletion(KTextEditor::View* view, bool enabled) { if (auto config = qobject_cast(view)) { config->setConfigValue(QStringLiteral("keyword-completion"), enabled); } } int ClangSupport::suggestedReparseDelayForChange(KTextEditor::Document* /*doc*/, const KTextEditor::Range& /*changedRange*/, const QString& /*changedText*/, bool /*removal*/) const { return ILanguageSupport::DefaultDelay; } void ClangSupport::disableKeywordCompletion(KTextEditor::View* view) { setKeywordCompletion(view, false); } void ClangSupport::enableKeywordCompletion(KTextEditor::View* view) { setKeywordCompletion(view, true); } #include "clangsupport.moc" diff --git a/plugins/clang/codecompletion/context.cpp b/plugins/clang/codecompletion/context.cpp index 1147b022a1..1ffe2eb6d0 100644 --- a/plugins/clang/codecompletion/context.cpp +++ b/plugins/clang/codecompletion/context.cpp @@ -1,1304 +1,1305 @@ /* * This file is part of KDevelop * Copyright 2014 Milian Wolff * Copyright 2015 Sergey Kalinichev * * 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 "context.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../util/clangdebug.h" #include "../util/clangtypes.h" #include "../util/clangutils.h" #include "../duchain/clangdiagnosticevaluator.h" #include "../duchain/parsesession.h" #include "../duchain/duchainutils.h" #include "../duchain/navigationwidget.h" #include "../clangsettings/clangsettingsmanager.h" #include #include #include #include using namespace KDevelop; namespace { /// Maximum return-type string length in completion items const int MAX_RETURN_TYPE_STRING_LENGTH = 20; /// Priority of code-completion results. NOTE: Keep in sync with Clang code base. enum CodeCompletionPriority { /// Priority for the next initialization in a constructor initializer list. CCP_NextInitializer = 7, /// Priority for an enumeration constant inside a switch whose condition is of the enumeration type. CCP_EnumInCase = 7, CCP_LocalDeclarationMatch = 8, CCP_DeclarationMatch = 12, CCP_LocalDeclarationSimiliar = 17, /// Priority for a send-to-super completion. CCP_SuperCompletion = 20, CCP_DeclarationSimiliar = 25, /// Priority for a declaration that is in the local scope. CCP_LocalDeclaration = 34, /// Priority for a member declaration found from the current method or member function. CCP_MemberDeclaration = 35, /// Priority for a language keyword (that isn't any of the other categories). CCP_Keyword = 40, /// Priority for a code pattern. CCP_CodePattern = 40, /// Priority for a non-type declaration. CCP_Declaration = 50, /// Priority for a type. CCP_Type = CCP_Declaration, /// Priority for a constant value (e.g., enumerator). CCP_Constant = 65, /// Priority for a preprocessor macro. CCP_Macro = 70, /// Priority for a nested-name-specifier. CCP_NestedNameSpecifier = 75, /// Priority for a result that isn't likely to be what the user wants, but is included for completeness. CCP_Unlikely = 80 }; /** * Common base class for Clang code completion items. */ template class CompletionItem : public Base { public: CompletionItem(const QString& display, const QString& prefix) : Base() , m_display(display) , m_prefix(prefix) , m_unimportant(false) { } ~CompletionItem() override = default; QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* /*model*/) const override { if (role == Qt::DisplayRole) { if (index.column() == CodeCompletionModel::Prefix) { return m_prefix; } else if (index.column() == CodeCompletionModel::Name) { return m_display; } } return {}; } void markAsUnimportant() { m_unimportant = true; } protected: QString m_display; QString m_prefix; bool m_unimportant; }; class OverrideItem : public CompletionItem { public: OverrideItem(const QString& nameAndParams, const QString& returnType) : CompletionItem( nameAndParams, i18n("Override %1", returnType) ) , m_returnType(returnType) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole) { if (index.column() == KTextEditor::CodeCompletionModel::Icon) { static const QIcon icon = QIcon::fromTheme(QStringLiteral("CTparents")); return icon; } } return CompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_returnType + QLatin1Char(' ') + m_display.replace(QRegularExpression(QStringLiteral("\\s*=\\s*0")), QString()) + QLatin1String(" override;")); } private: QString m_returnType; }; /** * Specialized completion item class for items which are represented by a Declaration */ class DeclarationItem : public CompletionItem { public: DeclarationItem(Declaration* dec, const QString& display, const QString& prefix, const QString& replacement) : CompletionItem(display, prefix) , m_replacement(replacement) { m_declaration = dec; } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::MatchQuality && m_matchQuality) { return m_matchQuality; } auto ret = CompletionItem::data(index, role, model); if (ret.isValid()) { return ret; } return NormalDeclarationCompletionItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { QString repl = m_replacement; DUChainReadLocker lock; if(!m_declaration){ return; } if(m_declaration->isFunctionDeclaration()) { const auto functionType = m_declaration->type(); // protect against buggy code that created the m_declaration, // to mark it as a function but not assign a function type if (!functionType) return; auto doc = view->document(); // Function pointer? bool funcptr = false; const auto line = doc->line(word.start().line()); auto pos = word.end().column() - 1; while ( pos > 0 && (line.at(pos).isLetterOrNumber() || line.at(pos) == QLatin1Char(':')) ) { pos--; if ( line.at(pos) == QLatin1Char('&') ) { funcptr = true; break; } } auto restEmpty = doc->characterAt(word.end() + KTextEditor::Cursor{0, 1}) == QChar(); bool didAddParentheses = false; if ( !funcptr && doc->characterAt(word.end()) != QLatin1Char('(') ) { repl += QLatin1String("()"); didAddParentheses = true; } view->document()->replaceText(word, repl); if (functionType->indexedArgumentsSize() && didAddParentheses) { view->setCursorPosition(word.start() + KTextEditor::Cursor(0, repl.size() - 1)); } auto returnTypeIntegral = functionType->returnType().cast(); if ( restEmpty && !funcptr && returnTypeIntegral && returnTypeIntegral->dataType() == IntegralType::TypeVoid ) { // function returns void and rest of line is empty -- nothing can be done with the result if (functionType->indexedArgumentsSize() ) { // we placed the cursor inside the () view->document()->insertText(view->cursorPosition() + KTextEditor::Cursor(0, 1), QStringLiteral(";")); } else { // we placed the cursor after the () view->document()->insertText(view->cursorPosition(), QStringLiteral(";")); view->setCursorPosition(view->cursorPosition() + KTextEditor::Cursor{0, 1}); } } } else { view->document()->replaceText(word, repl); } } bool createsExpandingWidget() const override { return true; } QWidget* createExpandingWidget(const CodeCompletionModel* /*model*/) const override { return new ClangNavigationWidget(m_declaration, AbstractNavigationWidget::EmbeddableWidget); } int matchQuality() const { return m_matchQuality; } ///Sets match quality from 0 to 10. 10 is the best fit. void setMatchQuality(int value) { m_matchQuality = value; } void setInheritanceDepth(int depth) { m_inheritanceDepth = depth; } int argumentHintDepth() const override { return m_depth; } void setArgumentHintDepth(int depth) { m_depth = depth; } protected: int m_matchQuality = 0; int m_depth = 0; QString m_replacement; }; class ImplementsItem : public DeclarationItem { public: static QString replacement(const FuncImplementInfo& info) { QString replacement = info.templatePrefix; if (!info.isDestructor && !info.isConstructor) { replacement += info.returnType + QLatin1Char(' '); } replacement += info.prototype + QLatin1String("\n{\n}\n"); return replacement; } explicit ImplementsItem(const FuncImplementInfo& item) : DeclarationItem(item.declaration.data(), item.prototype, i18n("Implement %1", item.isConstructor ? QStringLiteral("") : item.isDestructor ? QStringLiteral("") : item.returnType), replacement(item) ) { } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (index.column() == CodeCompletionModel::Arguments) { // our display string already contains the arguments return {}; } return DeclarationItem::data(index, role, model); } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_replacement); } }; class ArgumentHintItem : public DeclarationItem { public: struct CurrentArgumentRange { int start; int end; }; ArgumentHintItem(Declaration* decl, const QString& prefix, const QString& name, const QString& arguments, const CurrentArgumentRange& range) : DeclarationItem(decl, name, prefix, {}) , m_range(range) , m_arguments(arguments) {} QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == CodeCompletionModel::CustomHighlight && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { - QList highlighting; - highlighting << QVariant(m_range.start); - highlighting << QVariant(m_range.end); QTextCharFormat boldFormat; boldFormat.setFontWeight(QFont::Bold); - highlighting << boldFormat; + const QList highlighting { + QVariant(m_range.start), + QVariant(m_range.end), + boldFormat, + }; return highlighting; } if (role == CodeCompletionModel::HighlightingMethod && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { return QVariant(CodeCompletionModel::CustomHighlighting); } if (index.column() == CodeCompletionModel::Arguments) { return m_arguments; } return DeclarationItem::data(index, role, model); } private: CurrentArgumentRange m_range; QString m_arguments; }; /** * A minimalistic completion item for macros and such */ class SimpleItem : public CompletionItem { public: SimpleItem(const QString& display, const QString& prefix, const QString& replacement, const QIcon& icon = QIcon()) : CompletionItem(display, prefix) , m_replacement(replacement) , m_icon(icon) { } void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { view->document()->replaceText(word, m_replacement); } QVariant data(const QModelIndex& index, int role, const CodeCompletionModel* model) const override { if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { return m_icon; } if (role == CodeCompletionModel::UnimportantItemRole) { return m_unimportant; } return CompletionItem::data(index, role, model); } private: QString m_replacement; QIcon m_icon; }; /** * Return true in case position @p position represents a cursor inside a comment */ bool isInsideComment(CXTranslationUnit unit, CXFile file, const KTextEditor::Cursor& position) { if (!position.isValid()) { return false; } // TODO: This may get very slow for a large TU, investigate if we can improve this function auto begin = clang_getLocation(unit, file, 1, 1); auto end = clang_getLocation(unit, file, position.line() + 1, position.column() + 1); CXSourceRange range = clang_getRange(begin, end); // tokenize the whole range from the start until 'position' // if we detect a comment token at this position, return true const ClangTokens tokens(unit, range); for (CXToken token : tokens) { CXTokenKind tokenKind = clang_getTokenKind(token); if (tokenKind != CXToken_Comment) { continue; } auto range = ClangRange(clang_getTokenExtent(unit, token)); if (range.toRange().contains(position)) { return true; } } return false; } QString& elideStringRight(QString& str, int length) { if (str.size() > length + 3) { return str.replace(length, str.size() - length, QStringLiteral("...")); } return str; } /** * @return Value suited for @ref CodeCompletionModel::MatchQuality in the range [0.0, 10.0] (the higher the better) * * See http://clang.llvm.org/doxygen/CodeCompleteConsumer_8h_source.html for list of priorities * They (currently) have a range from [-3, 80] (the lower, the better) */ int codeCompletionPriorityToMatchQuality(unsigned int completionPriority) { return 10u - qBound(0u, completionPriority, 80u) / 8; } int adjustPriorityForType(const AbstractType::Ptr& type, int completionPriority) { const auto modifier = 4; if (type) { const auto whichType = type->whichType(); if (whichType == AbstractType::TypePointer || whichType == AbstractType::TypeReference) { // Clang considers all pointers as similar, this is not what we want. completionPriority += modifier; } else if (whichType == AbstractType::TypeStructure) { // Clang considers all classes as similar too... completionPriority += modifier; } else if (whichType == AbstractType::TypeDelayed) { completionPriority += modifier; } else if (whichType == AbstractType::TypeAlias) { auto aliasedType = type.cast(); return adjustPriorityForType(aliasedType ? aliasedType->type() : AbstractType::Ptr(), completionPriority); } else if (whichType == AbstractType::TypeFunction) { auto functionType = type.cast(); return adjustPriorityForType(functionType ? functionType->returnType() : AbstractType::Ptr(), completionPriority); } } else { completionPriority += modifier; } return completionPriority; } /// Adjusts priority for the @p decl int adjustPriorityForDeclaration(Declaration* decl, unsigned int completionPriority) { if(completionPriority < CCP_LocalDeclarationSimiliar || completionPriority > CCP_SuperCompletion){ return completionPriority; } return adjustPriorityForType(decl->abstractType(), completionPriority); } /** * @return Whether the declaration represented by identifier @p identifier qualifies as completion result * * For example, we don't want to offer SomeClass::SomeClass as completion item to the user * (otherwise we'd end up generating code such as 's.SomeClass();') */ bool isValidCompletionIdentifier(const QualifiedIdentifier& identifier) { const int count = identifier.count(); if (identifier.count() < 2) { return true; } const Identifier scope = identifier.at(count-2); const Identifier id = identifier.last(); if (scope == id) { return false; // is constructor } const QString idString = id.toString(); if (idString.startsWith(QLatin1Char('~')) && scope.toString() == idString.midRef(1)) { return false; // is destructor } return true; } /** * @return Whether the declaration represented by identifier @p identifier qualifies as "special" completion result * * "Special" completion results are items that are likely not regularly used. * * Examples: * - 'SomeClass::operator=(const SomeClass&)' */ bool isValidSpecialCompletionIdentifier(const QualifiedIdentifier& identifier) { if (identifier.count() < 2) { return false; } const Identifier id = identifier.last(); const QString idString = id.toString(); if (idString.startsWith(QLatin1String("operator="))) { return true; // is assignment operator } return false; } Declaration* findDeclaration(const QualifiedIdentifier& qid, const DUContextPointer& ctx, const CursorInRevision& position, QSet& handled) { PersistentSymbolTable::Declarations decl = PersistentSymbolTable::self().getDeclarations(qid); const auto top = ctx->topContext(); const auto& importedContexts = top->importedParentContexts(); for (auto it = decl.iterator(); it; ++it) { // if the context is not included, then this match is not correct for our consideration // this fixes issues where we used to include matches from files that did not have // anything to do with the current TU, e.g. the main from a different file or stuff like that // it also reduces the chance of us picking up a function of the same name from somewhere else // also, this makes sure the context has the correct language and we don't get confused by stuff // from other language plugins if (std::none_of(importedContexts.begin(), importedContexts.end(), [it] (const DUContext::Import& import) { return import.topContextIndex() == it->indexedTopContext().index(); })) { continue; } auto declaration = it->declaration(); if (!declaration) { // Mitigate problems such as: Cannot load a top-context from file "/home/kfunk/.cache/kdevduchain/kdevelop-{foo}/topcontexts/6085" // - the required language-support for handling ID 55 is probably not loaded qCWarning(KDEV_CLANG) << "Detected an invalid declaration for" << qid; continue; } if (declaration->kind() == Declaration::Instance && !declaration->isFunctionDeclaration()) { break; } if (!handled.contains(declaration)) { handled.insert(declaration); return declaration; } } const auto foundDeclarations = ctx->findDeclarations(qid, position); for (auto dec : foundDeclarations) { if (!handled.contains(dec)) { handled.insert(dec); return dec; } } return nullptr; } /// If any parent of this context is a class, the closest class declaration is returned, nullptr otherwise Declaration* classDeclarationForContext(const DUContextPointer& context, const CursorInRevision& position) { auto parent = context; while (parent) { if (parent->type() == DUContext::Class) { break; } if (auto owner = parent->owner()) { // Work-around for out-of-line methods. They have Helper context instead of Class context if (owner->context() && owner->context()->type() == DUContext::Helper) { auto qid = owner->qualifiedIdentifier(); qid.pop(); QSet tmp; auto decl = findDeclaration(qid, context, position, tmp); if (decl && decl->internalContext() && decl->internalContext()->type() == DUContext::Class) { parent = decl->internalContext(); break; } } } parent = parent->parentContext(); } return parent ? parent->owner() : nullptr; } class LookAheadItemMatcher { public: explicit LookAheadItemMatcher(const TopDUContextPointer& ctx) : m_topContext(ctx) , m_enabled(ClangSettingsManager::self()->codeCompletionSettings().lookAhead) {} /// Adds all local declarations for @p declaration into possible look-ahead items. void addDeclarations(Declaration* declaration) { if (!m_enabled) { return; } if (declaration->kind() != Declaration::Instance) { return; } auto type = typeForDeclaration(declaration); auto identifiedType = dynamic_cast(type.data()); if (!identifiedType) { return; } addDeclarationsForType(identifiedType, declaration); } /// Add type for matching. This type'll be used for filtering look-ahead items /// Only items with @p type will be returned through @sa matchedItems void addMatchedType(const IndexedType& type) { matchedTypes.insert(type); } /// @return look-ahead items that math given types. @sa addMatchedType QList matchedItems() { QList lookAheadItems; for (const auto& pair: possibleLookAheadDeclarations) { auto decl = pair.first; if (matchedTypes.contains(decl->indexedType())) { auto parent = pair.second; const QString access = parent->abstractType()->whichType() == AbstractType::TypePointer ? QStringLiteral("->") : QStringLiteral("."); const QString text = parent->identifier().toString() + access + decl->identifier().toString(); auto item = new DeclarationItem(decl, text, {}, text); item->setMatchQuality(8); lookAheadItems.append(CompletionTreeItemPointer(item)); } } return lookAheadItems; } private: AbstractType::Ptr typeForDeclaration(const Declaration* decl) { return TypeUtils::targetType(decl->abstractType(), m_topContext.data()); } void addDeclarationsForType(const IdentifiedType* identifiedType, Declaration* declaration) { if (auto typeDecl = identifiedType->declaration(m_topContext.data())) { if (dynamic_cast(typeDecl->logicalDeclaration(m_topContext.data()))) { if (!typeDecl->internalContext()) { return; } for (auto localDecl : typeDecl->internalContext()->localDeclarations()) { if(localDecl->identifier().isEmpty()){ continue; } if(auto classMember = dynamic_cast(localDecl)){ // TODO: Also add protected/private members if completion is inside this class context. if(classMember->accessPolicy() != Declaration::Public){ continue; } } if(!declaration->abstractType()){ continue; } if (declaration->abstractType()->whichType() == AbstractType::TypeIntegral) { if (auto integralType = declaration->abstractType().cast()) { if (integralType->dataType() == IntegralType::TypeVoid) { continue; } } } possibleLookAheadDeclarations.insert({localDecl, declaration}); } } } } // Declaration and it's context typedef QPair DeclarationContext; /// Types of declarations that look-ahead completion items can have QSet matchedTypes; // List of declarations that can be added to the Look Ahead group // Second declaration represents context QSet possibleLookAheadDeclarations; TopDUContextPointer m_topContext; bool m_enabled; }; struct MemberAccessReplacer : public QObject { Q_OBJECT public: enum Type { None, DotToArrow, ArrowToDot }; public Q_SLOTS: void replaceCurrentAccess(MemberAccessReplacer::Type type) { if (auto document = ICore::self()->documentController()->activeDocument()) { if (auto textDocument = document->textDocument()) { auto activeView = document->activeTextView(); if (!activeView) { return; } auto cursor = activeView->cursorPosition(); QString oldAccess, newAccess; if (type == ArrowToDot) { oldAccess = QStringLiteral("->"); newAccess = QStringLiteral("."); } else { oldAccess = QStringLiteral("."); newAccess = QStringLiteral("->"); } auto oldRange = KTextEditor::Range(cursor - KTextEditor::Cursor(0, oldAccess.length()), cursor); // This code needed for testReplaceMemberAccess test // Maybe we should do a similar thing for '->' to '.' direction, but this is not so important while (textDocument->text(oldRange) == QLatin1String(" ") && oldRange.start().column() >= 0) { oldRange = KTextEditor::Range({oldRange.start().line(), oldRange.start().column() - 1}, {oldRange.end().line(), oldRange.end().column() - 1}); } if (oldRange.start().column() >= 0 && textDocument->text(oldRange) == oldAccess) { textDocument->replaceText(oldRange, newAccess); } } } } }; static MemberAccessReplacer s_memberAccessReplacer; } Q_DECLARE_METATYPE(MemberAccessReplacer::Type) ClangCodeCompletionContext::ClangCodeCompletionContext(const DUContextPointer& context, const ParseSessionData::Ptr& sessionData, const QUrl& url, const KTextEditor::Cursor& position, const QString& text, const QString& followingText ) : CodeCompletionContext(context, text + followingText, CursorInRevision::castFromSimpleCursor(position), 0) , m_results(nullptr, clang_disposeCodeCompleteResults) , m_parseSessionData(sessionData) { qRegisterMetaType(); const QByteArray file = url.toLocalFile().toUtf8(); ParseSession session(m_parseSessionData); QVector otherUnsavedFiles; { ForegroundLock lock; otherUnsavedFiles = ClangUtils::unsavedFiles(); } QVector allUnsaved; { const unsigned int completeOptions = clang_defaultCodeCompleteOptions(); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); - allUnsaved.reserve(otherUnsavedFiles.size()); + allUnsaved.reserve(otherUnsavedFiles.size() + 1); for ( const auto& f : otherUnsavedFiles ) { allUnsaved.append(f.toClangApi()); } allUnsaved.append(unsaved); m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1, allUnsaved.data(), allUnsaved.size(), completeOptions)); if (!m_results) { qCWarning(KDEV_CLANG) << "Something went wrong during 'clang_codeCompleteAt' for file" << file; return; } auto numDiagnostics = clang_codeCompleteGetNumDiagnostics(m_results.get()); for (uint i = 0; i < numDiagnostics; i++) { auto diagnostic = clang_codeCompleteGetDiagnostic(m_results.get(), i); auto diagnosticType = ClangDiagnosticEvaluator::diagnosticType(diagnostic); clang_disposeDiagnostic(diagnostic); if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithArrowProblem || diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { MemberAccessReplacer::Type replacementType; if (diagnosticType == ClangDiagnosticEvaluator::ReplaceWithDotProblem) { replacementType = MemberAccessReplacer::ArrowToDot; } else { replacementType = MemberAccessReplacer::DotToArrow; } QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, replacementType)); m_valid = false; return; } } auto addMacros = ClangSettingsManager::self()->codeCompletionSettings().macros; if (!addMacros) { m_filters |= NoMacros; } } if (!m_results->NumResults) { const auto trimmedText = text.trimmed(); if (trimmedText.endsWith(QLatin1Char('.'))) { // TODO: This shouldn't be needed if Clang provided diagnostic. // But it doesn't always do it, so let's try to manually determine whether '.' is used instead of '->' - m_text = trimmedText.left(trimmedText.size() - 1); - m_text += QStringLiteral("->"); + m_text = trimmedText.leftRef(trimmedText.size() - 1) + QStringLiteral("->"); CXUnsavedFile unsaved; unsaved.Filename = file.constData(); const QByteArray content = m_text.toUtf8(); unsaved.Contents = content.constData(); unsaved.Length = content.size(); allUnsaved[allUnsaved.size() - 1] = unsaved; m_results.reset(clang_codeCompleteAt(session.unit(), file.constData(), position.line() + 1, position.column() + 1 + 1, allUnsaved.data(), allUnsaved.size(), clang_defaultCodeCompleteOptions())); if (m_results && m_results->NumResults) { QMetaObject::invokeMethod(&s_memberAccessReplacer, "replaceCurrentAccess", Qt::QueuedConnection, Q_ARG(MemberAccessReplacer::Type, MemberAccessReplacer::DotToArrow)); } m_valid = false; return; } } // check 'isValidPosition' after parsing the new content auto clangFile = session.file(file); if (!isValidPosition(session.unit(), clangFile)) { m_valid = false; return; } m_completionHelper.computeCompletions(session, clangFile, position); } ClangCodeCompletionContext::~ClangCodeCompletionContext() { } bool ClangCodeCompletionContext::isValidPosition(CXTranslationUnit unit, CXFile file) const { if (isInsideComment(unit, file, m_position.castToSimpleCursor())) { clangDebug() << "Invalid completion context: Inside comment"; return false; } return true; } QList ClangCodeCompletionContext::completionItems(bool& abort, bool /*fullCompletion*/) { if (!m_valid || !m_duContext || !m_results) { return {}; } const auto ctx = DUContextPointer(m_duContext->findContextAt(m_position)); /// Normal completion items, such as 'void Foo::foo()' QList items; /// Stuff like 'Foo& Foo::operator=(const Foo&)', etc. Not regularly used by our users. QList specialItems; /// Macros from the current context QList macros; /// Builtins reported by Clang QList builtin; // two sets of handled declarations to prevent duplicates and make sure we show // all available overloads QSet handled; // this is only used for the CXCursor_OverloadCandidate completion items QSet overloadsHandled; LookAheadItemMatcher lookAheadMatcher(TopDUContextPointer(ctx->topContext())); // If ctx is/inside the Class context, this represents that context. const auto currentClassContext = classDeclarationForContext(ctx, m_position); clangDebug() << "Clang found" << m_results->NumResults << "completion results"; for (uint i = 0; i < m_results->NumResults; ++i) { if (abort) { return {}; } auto result = m_results->Results[i]; #if CINDEX_VERSION_MINOR >= 30 const bool isOverloadCandidate = result.CursorKind == CXCursor_OverloadCandidate; #else const bool isOverloadCandidate = false; #endif const auto availability = clang_getCompletionAvailability(result.CompletionString); if (availability == CXAvailability_NotAvailable) { continue; } const bool isMacroDefinition = result.CursorKind == CXCursor_MacroDefinition; if (isMacroDefinition && m_filters & NoMacros) { continue; } const bool isBuiltin = (result.CursorKind == CXCursor_NotImplemented); if (isBuiltin && m_filters & NoBuiltins) { continue; } const bool isDeclaration = !isMacroDefinition && !isBuiltin; if (isDeclaration && m_filters & NoDeclarations) { continue; } if (availability == CXAvailability_NotAccessible && (!isDeclaration || !currentClassContext)) { continue; } // the string that would be needed to type, usually the identifier of something. Also we use it as name for code completion declaration items. QString typed; // the return type of a function e.g. QString resultType; // the replacement text when an item gets executed QString replacement; QString arguments; ArgumentHintItem::CurrentArgumentRange argumentRange; //BEGIN function signature parsing // nesting depth of parentheses int parenDepth = 0; enum FunctionSignatureState { // not yet inside the function signature Before, // any token is part of the function signature now Inside, // finished parsing the function signature After }; // current state FunctionSignatureState signatureState = Before; //END function signature parsing std::function processChunks = [&] (CXCompletionString completionString) { const uint chunks = clang_getNumCompletionChunks(completionString); for (uint j = 0; j < chunks; ++j) { const auto kind = clang_getCompletionChunkKind(completionString, j); if (kind == CXCompletionChunk_Optional) { completionString = clang_getCompletionChunkCompletionString(completionString, j); if (completionString) { processChunks(completionString); } continue; } // We don't need function signature for declaration items, we can get it directly from the declaration. Also adding the function signature to the "display" would break the "Detailed completion" option. if (isDeclaration && !typed.isEmpty()) { // TODO: When parent context for CXCursor_OverloadCandidate is fixed remove this check if (!isOverloadCandidate) { break; } } const QString string = ClangString(clang_getCompletionChunkText(completionString, j)).toString(); switch (kind) { case CXCompletionChunk_TypedText: typed = string; replacement += string; break; case CXCompletionChunk_ResultType: resultType = string; continue; case CXCompletionChunk_Placeholder: if (signatureState == Inside) { arguments += string; } continue; case CXCompletionChunk_LeftParen: if (signatureState == Before && !parenDepth) { signatureState = Inside; } parenDepth++; break; case CXCompletionChunk_RightParen: --parenDepth; if (signatureState == Inside && !parenDepth) { arguments += QLatin1Char(')'); signatureState = After; } break; case CXCompletionChunk_Text: if (isOverloadCandidate) { typed += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } else if (result.CursorKind == CXCursor_EnumConstantDecl) { replacement += string; } break; case CXCompletionChunk_CurrentParameter: argumentRange.start = arguments.size(); argumentRange.end = string.size(); break; default: break; } if (signatureState == Inside) { arguments += string; } } }; processChunks(result.CompletionString); // TODO: No closing paren if default parameters present if (isOverloadCandidate && !arguments.endsWith(QLatin1Char(')'))) { arguments += QLatin1Char(')'); } // ellide text to the right for overly long result types (templates especially) elideStringRight(resultType, MAX_RETURN_TYPE_STRING_LENGTH); static const auto noIcon = QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevelop/pics/namespace.png"))); if (isDeclaration) { const Identifier id(typed); QualifiedIdentifier qid; ClangString parent(clang_getCompletionParent(result.CompletionString, nullptr)); if (parent.c_str() != nullptr) { qid = QualifiedIdentifier(parent.toString()); } qid.push(id); if (!isValidCompletionIdentifier(qid)) { continue; } if (isOverloadCandidate && resultType.isEmpty() && parent.isEmpty()) { // workaround: find constructor calls for non-namespaced classes // TODO: return the namespaced class as parent in libclang qid.push(id); } auto found = findDeclaration(qid, ctx, m_position, isOverloadCandidate ? overloadsHandled : handled); CompletionTreeItemPointer item; if (found) { // TODO: Bug in Clang: protected members from base classes not accessible in derived classes. if (availability == CXAvailability_NotAccessible) { if (auto cl = dynamic_cast(found)) { if (cl->accessPolicy() != Declaration::Protected) { continue; } auto declarationClassContext = classDeclarationForContext(DUContextPointer(found->context()), m_position); uint steps = 10; auto inheriters = DUChainUtils::getInheriters(declarationClassContext, steps); if(!inheriters.contains(currentClassContext)){ continue; } } else { continue; } } DeclarationItem* declarationItem = nullptr; if (isOverloadCandidate) { declarationItem = new ArgumentHintItem(found, resultType, typed, arguments, argumentRange); declarationItem->setArgumentHintDepth(1); } else { declarationItem = new DeclarationItem(found, typed, resultType, replacement); } const unsigned int completionPriority = adjustPriorityForDeclaration(found, clang_getCompletionPriority(result.CompletionString)); const bool bestMatch = completionPriority <= CCP_SuperCompletion; //don't set best match property for internal identifiers, also prefer declarations from current file const auto isInternal = found->indexedIdentifier().identifier().toString().startsWith(QLatin1String("__")); if (bestMatch && !isInternal ) { const int matchQuality = codeCompletionPriorityToMatchQuality(completionPriority); declarationItem->setMatchQuality(matchQuality); // TODO: LibClang missing API to determine expected code completion type. lookAheadMatcher.addMatchedType(found->indexedType()); } else { declarationItem->setInheritanceDepth(completionPriority); lookAheadMatcher.addDeclarations(found); } if ( isInternal ) { declarationItem->markAsUnimportant(); } item = declarationItem; } else { if (isOverloadCandidate) { // TODO: No parent context for CXCursor_OverloadCandidate items, hence qid is broken -> no declaration found auto ahi = new ArgumentHintItem({}, resultType, typed, arguments, argumentRange); ahi->setArgumentHintDepth(1); item = ahi; } else { // still, let's trust that Clang found something useful and put it into the completion result list clangDebug() << "Could not find declaration for" << qid; auto instance = new SimpleItem(typed + arguments, resultType, replacement, noIcon); instance->markAsUnimportant(); item = CompletionTreeItemPointer(instance); } } if (isValidSpecialCompletionIdentifier(qid)) { // If it's a special completion identifier e.g. "operator=(const&)" and we don't have a declaration for it, don't add it into completion list, as this item is completely useless and pollutes the test case. // This happens e.g. for "class A{}; a.|". At | we have "operator=(const A&)" as a special completion identifier without a declaration. if(item->declaration()){ specialItems.append(item); } } else { items.append(item); } continue; } if (result.CursorKind == CXCursor_MacroDefinition) { // TODO: grouping of macros and built-in stuff const auto text = QString(typed + arguments); auto instance = new SimpleItem(text, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); if ( text.startsWith(QLatin1Char('_')) ) { instance->markAsUnimportant(); } macros.append(item); } else if (result.CursorKind == CXCursor_NotImplemented) { auto instance = new SimpleItem(typed, resultType, replacement, noIcon); auto item = CompletionTreeItemPointer(instance); builtin.append(item); } } if (abort) { return {}; } addImplementationHelperItems(); addOverwritableItems(); eventuallyAddGroup(i18n("Special"), 700, specialItems); eventuallyAddGroup(i18n("Look-ahead Matches"), 800, lookAheadMatcher.matchedItems()); eventuallyAddGroup(i18n("Builtin"), 900, builtin); eventuallyAddGroup(i18n("Macros"), 1000, macros); return items; } void ClangCodeCompletionContext::eventuallyAddGroup(const QString& name, int priority, const QList& items) { if (items.isEmpty()) { return; } auto* node = new CompletionCustomGroupNode(name, priority); node->appendChildren(items); m_ungrouped << CompletionTreeElementPointer(node); } void ClangCodeCompletionContext::addOverwritableItems() { auto overrideList = m_completionHelper.overrides(); if (overrideList.isEmpty()) { return; } QList overrides; QList overridesAbstract; for (const auto& info : overrideList) { QStringList params; params.reserve(info.params.size()); for (const auto& param : info.params) { params << param.type + QLatin1Char(' ') + param.id; } QString nameAndParams = info.name + QLatin1Char('(') + params.join(QStringLiteral(", ")) + QLatin1Char(')'); if(info.isConst) nameAndParams = nameAndParams + QLatin1String(" const"); if(info.isPureVirtual) nameAndParams = nameAndParams + QLatin1String(" = 0"); auto item = CompletionTreeItemPointer(new OverrideItem(nameAndParams, info.returnType)); if (info.isPureVirtual) overridesAbstract << item; else overrides << item; } eventuallyAddGroup(i18n("Abstract Override"), 0, overridesAbstract); eventuallyAddGroup(i18n("Virtual Override"), 0, overrides); } void ClangCodeCompletionContext::addImplementationHelperItems() { auto implementsList = m_completionHelper.implements(); if (implementsList.isEmpty()) { return; } QList implements; + implements.reserve(implementsList.size()); foreach(const auto& info, implementsList) { implements << CompletionTreeItemPointer(new ImplementsItem(info)); } eventuallyAddGroup(i18n("Implement Function"), 0, implements); } QList ClangCodeCompletionContext::ungroupedElements() { return m_ungrouped; } ClangCodeCompletionContext::ContextFilters ClangCodeCompletionContext::filters() const { return m_filters; } void ClangCodeCompletionContext::setFilters(const ClangCodeCompletionContext::ContextFilters& filters) { m_filters = filters; } #include "context.moc" diff --git a/plugins/clang/codecompletion/includepathcompletioncontext.cpp b/plugins/clang/codecompletion/includepathcompletioncontext.cpp index c1f54a981c..b4f257d0ac 100644 --- a/plugins/clang/codecompletion/includepathcompletioncontext.cpp +++ b/plugins/clang/codecompletion/includepathcompletioncontext.cpp @@ -1,294 +1,295 @@ /* * This file is part of KDevelop * Copyright 2014 Sergey Kalinichev * Copyright 2015 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 "includepathcompletioncontext.h" #include "duchain/navigationwidget.h" #include "duchain/clanghelpers.h" #include #include #include #include using namespace KDevelop; /** * Parse the last line of @p text and extract information about any existing include path from it. */ IncludePathProperties IncludePathProperties::parseText(const QString& text, int rightBoundary) { IncludePathProperties properties; int idx = text.lastIndexOf(QLatin1Char('\n')); if (idx == -1) { idx = 0; } if (rightBoundary == -1) { rightBoundary = text.length(); } // what follows is a relatively simple parser for include lines that may contain comments, i.e.: // /*comment*/ #include /*comment*/ "path.h" /*comment*/ enum FindState { FindBang, FindInclude, FindType, FindTypeEnd }; FindState state = FindBang; QChar expectedEnd = QLatin1Char('>'); for (; idx < text.size(); ++idx) { const auto c = text.at(idx); if (c.isSpace()) { continue; } if (c == QLatin1Char('/') && state != FindTypeEnd) { // skip comments if (idx >= text.length() - 1 || text.at(idx + 1) != QLatin1Char('*')) { properties.valid = false; return properties; } idx += 2; while (idx < text.length() - 1 && (text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/'))) { ++idx; } if (idx >= text.length() - 1 || text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/')) { properties.valid = false; return properties; } ++idx; continue; } switch (state) { case FindBang: if (c != QLatin1Char('#')) { return properties; } state = FindInclude; break; case FindInclude: if (text.midRef(idx, 7) != QLatin1String("include")) { return properties; } idx += 6; state = FindType; properties.valid = true; break; case FindType: properties.inputFrom = idx + 1; if (c == QLatin1Char('"')) { expectedEnd = QLatin1Char('"'); properties.local = true; } else if (c != QLatin1Char('<')) { properties.valid = false; return properties; } state = FindTypeEnd; break; case FindTypeEnd: if (c == expectedEnd) { properties.inputTo = idx; // stop iteration idx = text.size(); } break; } } if (!properties.valid) { return properties; } // properly append to existing paths without overriding it // i.e.: #include should become #include // or: #include should again become #include // see unit tests for more examples if (properties.inputFrom != -1) { int end = properties.inputTo; if (end >= rightBoundary || end == -1) { end = text.lastIndexOf(QLatin1Char('/'), rightBoundary - 1) + 1; } if (end > 0) { properties.prefixPath = text.mid(properties.inputFrom, end - properties.inputFrom); properties.inputFrom += properties.prefixPath.length(); } } return properties; } namespace { QVector includeItemsForUrl(const QUrl& url, const IncludePathProperties& properties, const ClangParsingEnvironment::IncludePaths& includePaths) { QVector includeItems; Path::List paths; if (properties.local) { + paths.reserve(1 + includePaths.project.size() + includePaths.system.size()); paths.push_back(Path(url).parent()); paths += includePaths.project; paths += includePaths.system; } else { paths = includePaths.system + includePaths.project; } // ensure we don't add duplicate paths QSet handledPaths; // search paths QSet foundIncludePaths; // found items int pathNumber = 0; for (auto searchPath : paths) { if (handledPaths.contains(searchPath)) { continue; } handledPaths.insert(searchPath); if (!properties.prefixPath.isEmpty()) { searchPath.addPath(properties.prefixPath); } QDirIterator dirIterator(searchPath.toLocalFile()); while (dirIterator.hasNext()) { dirIterator.next(); KDevelop::IncludeItem item; item.name = dirIterator.fileName(); if (item.name.startsWith(QLatin1Char('.')) || item.name.endsWith(QLatin1Char('~'))) { //filter out ".", "..", hidden files, and backups continue; } const auto info = dirIterator.fileInfo(); item.isDirectory = info.isDir(); // filter files that are not a header // note: system headers sometimes don't have any extension, and we still want to show those if (!item.isDirectory && item.name.contains(QLatin1Char('.')) && !ClangHelpers::isHeader(item.name)) { continue; } const QString fullPath = info.canonicalFilePath(); if (foundIncludePaths.contains(fullPath)) { continue; } else { foundIncludePaths.insert(fullPath); } item.basePath = searchPath.toUrl(); item.pathNumber = pathNumber; includeItems << item; } ++pathNumber; } return includeItems; } } class IncludeFileCompletionItem : public AbstractIncludeFileCompletionItem { public: explicit IncludeFileCompletionItem(const IncludeItem& include) : AbstractIncludeFileCompletionItem(include) {} void execute(KTextEditor::View* view, const KTextEditor::Range& word) override { auto document = view->document(); auto range = word; const int lineNumber = word.end().line(); const QString line = document->line(lineNumber); const auto properties = IncludePathProperties::parseText(line, word.end().column()); if (!properties.valid) { return; } QString newText = includeItem.isDirectory ? (includeItem.name + QLatin1Char('/')) : includeItem.name; if (properties.inputFrom == -1) { newText.prepend(QLatin1Char('<')); } else { range.setStart({lineNumber, properties.inputFrom}); } if (properties.inputTo == -1) { // Add suffix if (properties.local) { newText += QLatin1Char('"'); } else { newText += QLatin1Char('>'); } // replace the whole line range.setEnd({lineNumber, line.size()}); } else { range.setEnd({lineNumber, properties.inputTo}); } document->replaceText(range, newText); if (includeItem.isDirectory) { // ensure we can continue to add files/paths when we just added a directory int offset = (properties.inputTo == -1) ? 1 : 0; view->setCursorPosition(range.start() + KTextEditor::Cursor(0, newText.length() - offset)); } else { // place cursor at end of line view->setCursorPosition({lineNumber, document->lineLength(lineNumber)}); } } }; IncludePathCompletionContext::IncludePathCompletionContext(const DUContextPointer& context, const ParseSessionData::Ptr& sessionData, const QUrl& url, const KTextEditor::Cursor& position, const QString& text) : CodeCompletionContext(context, text, CursorInRevision::castFromSimpleCursor(position), 0) { const IncludePathProperties properties = IncludePathProperties::parseText(text); if (!properties.valid) { return; } m_includeItems = includeItemsForUrl(url, properties, sessionData->environment().includes()); } QList< CompletionTreeItemPointer > IncludePathCompletionContext::completionItems(bool& abort, bool) { QList items; for (const auto& includeItem: m_includeItems) { if (abort) { return items; } items << CompletionTreeItemPointer(new IncludeFileCompletionItem(includeItem)); } return items; } diff --git a/plugins/clang/codegen/adaptsignatureassistant.cpp b/plugins/clang/codegen/adaptsignatureassistant.cpp index ff557f374d..3d363a8950 100644 --- a/plugins/clang/codegen/adaptsignatureassistant.cpp +++ b/plugins/clang/codegen/adaptsignatureassistant.cpp @@ -1,318 +1,323 @@ /* Copyright 2009 David Nolden Copyright 2014 Kevin Funk 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 "adaptsignatureassistant.h" #include #include #include #include #include #include #include #include #include "../util/clangdebug.h" using namespace KDevelop; namespace { Declaration *getDeclarationAtCursor(const KTextEditor::Cursor &cursor, const QUrl &documentUrl) { ENSURE_CHAIN_READ_LOCKED ReferencedTopDUContext top(DUChainUtils::standardContextForUrl(documentUrl)); if (!top) { clangDebug() << "no context found for document" << documentUrl; return nullptr; } const auto *context = top->findContextAt(top->transformToLocalRevision(cursor), true); return context->type() == DUContext::Function ? context->owner() : nullptr; } bool isConstructor(const Declaration *functionDecl) { auto classFun = dynamic_cast(DUChainUtils::declarationForDefinition(const_cast(functionDecl))); return classFun && classFun->isConstructor(); } Signature getDeclarationSignature(const Declaration *functionDecl, const DUContext *functionCtxt, bool includeDefaults) { ENSURE_CHAIN_READ_LOCKED int pos = 0; Signature signature; const AbstractFunctionDeclaration* abstractFunDecl = dynamic_cast(functionDecl); - foreach(Declaration * parameter, functionCtxt->localDeclarations()) { + const auto localDeclarations = functionCtxt->localDeclarations(); + const int localDeclarationsCount = localDeclarations.size(); + signature.defaultParams.reserve(localDeclarationsCount); + signature.parameters.reserve(localDeclarationsCount); + for (Declaration * parameter : localDeclarations) { signature.defaultParams << (includeDefaults ? abstractFunDecl->defaultParameterForArgument(pos).str() : QString()); signature.parameters << qMakePair(parameter->indexedType(), parameter->identifier().identifier().str()); ++pos; } signature.isConst = functionDecl->abstractType() && functionDecl->abstractType()->modifiers() & AbstractType::ConstModifier; if (!isConstructor(functionDecl)) { if (auto funType = functionDecl->type()) { signature.returnType = IndexedType(funType->returnType()); } } return signature; } } AdaptSignatureAssistant::AdaptSignatureAssistant(ILanguageSupport* supportedLanguage) : StaticAssistant(supportedLanguage) { } QString AdaptSignatureAssistant::title() const { return i18n("Adapt Signature"); } void AdaptSignatureAssistant::reset() { doHide(); clearActions(); m_editingDefinition = {}; m_declarationName = {}; m_otherSideId = DeclarationId(); m_otherSideTopContext = {}; m_otherSideContext = {}; m_oldSignature = {}; m_document = nullptr; m_view.clear(); } void AdaptSignatureAssistant::textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange, const QString& removedText) { reset(); m_document = doc; m_lastEditPosition = invocationRange.end(); KTextEditor::Range sigAssistRange = invocationRange; if (!removedText.isEmpty()) { sigAssistRange.setRange(sigAssistRange.start(), sigAssistRange.start()); } DUChainReadLocker lock(DUChain::lock(), 300); if (!lock.locked()) { clangDebug() << "failed to lock duchain in time"; return; } KTextEditor::Range simpleInvocationRange = KTextEditor::Range(sigAssistRange); Declaration* funDecl = getDeclarationAtCursor(simpleInvocationRange.start(), m_document->url()); if (!funDecl || !funDecl->type()) { clangDebug() << "No function at cursor"; return; } /* TODO: Port? if(QtFunctionDeclaration* classFun = dynamic_cast(funDecl)) { if (classFun->isSignal()) { // do not offer to change signature of a signal, as the implementation will be generated by moc return; } } */ Declaration* otherSide = nullptr; FunctionDefinition* definition = dynamic_cast(funDecl); if (definition) { m_editingDefinition = true; otherSide = definition->declaration(); } else if ((definition = FunctionDefinition::definition(funDecl))) { m_editingDefinition = false; otherSide = definition; } if (!otherSide) { clangDebug() << "no other side for signature found"; return; } m_otherSideContext = DUContextPointer(DUChainUtils::getFunctionContext(otherSide)); if (!m_otherSideContext) { clangDebug() << "no context for other side found"; return; } m_declarationName = funDecl->identifier(); m_otherSideId = otherSide->id(); m_otherSideTopContext = ReferencedTopDUContext(otherSide->topContext()); m_oldSignature = getDeclarationSignature(otherSide, m_otherSideContext.data(), true); //Schedule an update, to make sure the ranges match DUChain::self()->updateContextForUrl(m_otherSideTopContext->url(), TopDUContext::AllDeclarationsAndContexts); } bool AdaptSignatureAssistant::isUseful() const { return !m_declarationName.isEmpty() && m_otherSideId.isValid() && !actions().isEmpty(); } bool AdaptSignatureAssistant::getSignatureChanges(const Signature& newSignature, QList& oldPositions) const { bool changed = false; + oldPositions.reserve(oldPositions.size() + newSignature.parameters.size()); for (int i = 0; i < newSignature.parameters.size(); ++i) { oldPositions.append(-1); } for (int curNewParam = newSignature.parameters.size() - 1; curNewParam >= 0; --curNewParam) { int foundAt = -1; for (int curOldParam = m_oldSignature.parameters.size() - 1; curOldParam >= 0; --curOldParam) { if (newSignature.parameters[curNewParam].first != m_oldSignature.parameters[curOldParam].first) { continue; //Different type == different parameters } if (newSignature.parameters[curNewParam].second == m_oldSignature.parameters[curOldParam].second || curOldParam == curNewParam) { //given the same type and either the same position or the same name, it's (probably) the same argument foundAt = curOldParam; if (newSignature.parameters[curNewParam].second != m_oldSignature.parameters[curOldParam].second || curOldParam != curNewParam) { changed = true; //Either the name changed at this position, or position of this name has changed } if (newSignature.parameters[curNewParam].second == m_oldSignature.parameters[curOldParam].second) { break; //Found an argument with the same name and type, no need to look further } //else: position/type match, but name match will trump, allowing: (int i=0, int j=1) => (int j=1, int i=0) } } if (foundAt < 0) { changed = true; } oldPositions[curNewParam] = foundAt; } if (newSignature.parameters.size() != m_oldSignature.parameters.size()) { changed = true; } if (newSignature.isConst != m_oldSignature.isConst) { changed = true; } if (newSignature.returnType != m_oldSignature.returnType) { changed = true; } return changed; } void AdaptSignatureAssistant::setDefaultParams(Signature& newSignature, const QList& oldPositions) const { bool hadDefaultParam = false; for (int i = 0; i < newSignature.defaultParams.size(); ++i) { const auto oldPos = oldPositions[i]; if (oldPos == -1) { // default-initialize new argument if we encountered a previous default param if (hadDefaultParam) { newSignature.defaultParams[i] = QStringLiteral("{} /* TODO */"); } } else { newSignature.defaultParams[i] = m_oldSignature.defaultParams[oldPos]; hadDefaultParam = hadDefaultParam || !newSignature.defaultParams[i].isEmpty(); } } } QList AdaptSignatureAssistant::getRenameActions(const Signature &newSignature, const QList &oldPositions) const { ENSURE_CHAIN_READ_LOCKED QList renameActions; if (!m_otherSideContext) { return renameActions; } for (int i = newSignature.parameters.size() - 1; i >= 0; --i) { if (oldPositions[i] == -1) { continue; //new parameter } Declaration *renamedDecl = m_otherSideContext->localDeclarations()[oldPositions[i]]; if (newSignature.parameters[i].second != m_oldSignature.parameters[oldPositions[i]].second) { const auto uses = renamedDecl->uses(); if (!uses.isEmpty()) { renameActions << new RenameAction(renamedDecl->identifier(), newSignature.parameters[i].second, RevisionedFileRanges::convert(uses)); } } } return renameActions; } void AdaptSignatureAssistant::updateReady(const KDevelop::IndexedString& document, const KDevelop::ReferencedTopDUContext& top) { if (!top || !m_document || document.toUrl() != m_document->url() || top->url() != IndexedString(m_document->url())) { return; } clearActions(); DUChainReadLocker lock; Declaration *functionDecl = getDeclarationAtCursor(m_lastEditPosition, m_document->url()); if (!functionDecl || functionDecl->identifier() != m_declarationName) { clangDebug() << "No function found at" << m_document->url() << m_lastEditPosition; return; } DUContext *functionCtxt = DUChainUtils::getFunctionContext(functionDecl); if (!functionCtxt) { clangDebug() << "No function context found for" << functionDecl->toString(); return; } #if 0 // TODO: Port if (QtFunctionDeclaration * classFun = dynamic_cast(functionDecl)) { if (classFun->isSignal()) { // do not offer to change signature of a signal, as the implementation will be generated by moc return; } } #endif //ParseJob having finished, get the signature that was modified Signature newSignature = getDeclarationSignature(functionDecl, functionCtxt, false); //Check for changes between m_oldSignature and newSignature, use oldPositions to store old<->new param index mapping QList oldPositions; if (!getSignatureChanges(newSignature, oldPositions)) { reset(); clangDebug() << "no changes to signature"; return; //No changes to signature } QList renameActions; if (m_editingDefinition) { setDefaultParams(newSignature, oldPositions); //restore default parameters before updating the declarations } else { renameActions = getRenameActions(newSignature, oldPositions); //rename as needed when updating the definition } IAssistantAction::Ptr action(new AdaptSignatureAction(m_otherSideId, m_otherSideTopContext, m_oldSignature, newSignature, m_editingDefinition, renameActions)); connect(action.data(), &IAssistantAction::executed, this, &AdaptSignatureAssistant::reset); addAction(action); emit actionsChanged(); } KTextEditor::Range AdaptSignatureAssistant::displayRange() const { if (!m_document) { return {}; } auto s = m_lastEditPosition; KTextEditor::Range ran = {s.line(), 0, s.line(), m_document->lineLength(s.line())}; return ran; } #include "moc_adaptsignatureassistant.cpp" diff --git a/plugins/clang/codegen/codegenhelper.cpp b/plugins/clang/codegen/codegenhelper.cpp index 1f988ef72d..390f997538 100644 --- a/plugins/clang/codegen/codegenhelper.cpp +++ b/plugins/clang/codegen/codegenhelper.cpp @@ -1,469 +1,466 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "codegenhelper.h" #include "adaptsignatureaction.h" #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { IndexedTypeIdentifier stripPrefixIdentifiers(const IndexedTypeIdentifier& id, const QualifiedIdentifier& strip); Identifier stripPrefixIdentifiers(const Identifier& id, const QualifiedIdentifier& strip) { Identifier ret(id); ret.clearTemplateIdentifiers(); for (unsigned int a = 0; a < id.templateIdentifiersCount(); ++a) { ret.appendTemplateIdentifier(stripPrefixIdentifiers(id.templateIdentifier(a), strip)); } return ret; } IndexedTypeIdentifier stripPrefixIdentifiers(const IndexedTypeIdentifier& id, const QualifiedIdentifier& strip) { QualifiedIdentifier oldId(id.identifier().identifier()); QualifiedIdentifier qid; int commonPrefix = 0; for (; commonPrefix < oldId.count() - 1 && commonPrefix < strip.count(); ++commonPrefix) { if (strip.at(commonPrefix).toString() != oldId.at(commonPrefix).toString()) { break; } } for (int a = commonPrefix; a < oldId.count(); ++a) { qid.push(stripPrefixIdentifiers(oldId.at(a), strip)); } IndexedTypeIdentifier ret(id); ret.setIdentifier(qid); return ret; } int reservedIdentifierCount(const QString &name) { QStringList l = name.split(QStringLiteral("::")); int ret = 0; foreach(const QString &s, l) if (s.startsWith(QLatin1Char('_'))) { ++ret; } return ret; } uint buildIdentifierForType(const AbstractType::Ptr& type, IndexedTypeIdentifier& id, uint pointerLevel, TopDUContext* top) { if (!type) { return pointerLevel; } TypePtr refType = type.cast(); if (refType) { id.setIsReference(true); if (refType->modifiers() & AbstractType::ConstModifier) { id.setIsConstant(true); } return buildIdentifierForType(refType->baseType(), id, pointerLevel, top); } TypePtr pointerType = type.cast(); if (pointerType) { ++pointerLevel; uint maxPointerLevel = buildIdentifierForType(pointerType->baseType(), id, pointerLevel, top); if (type->modifiers() & AbstractType::ConstModifier) { id.setIsConstPointer(maxPointerLevel - pointerLevel, true); } if (static_cast(id.pointerDepth()) < pointerLevel) { id.setPointerDepth(pointerLevel); } return maxPointerLevel; } AbstractType::Ptr useTypeText = type; if (type->modifiers() & AbstractType::ConstModifier) { //Remove the 'const' modifier, as it will be added to the type-identifier below useTypeText = IndexedType(type).abstractType(); useTypeText->setModifiers(useTypeText->modifiers() & (~AbstractType::ConstModifier)); } id.setIdentifier(QualifiedIdentifier(useTypeText->toString(), true)); if (type->modifiers() & AbstractType::ConstModifier) { id.setIsConstant(true); } if (type->modifiers() & AbstractType::VolatileModifier) { id.setIsVolatile(true); } return pointerLevel; } IndexedTypeIdentifier identifierForType(const AbstractType::Ptr& type, TopDUContext* top) { IndexedTypeIdentifier ret; buildIdentifierForType(type, ret, 0, top); return ret; } IndexedTypeIdentifier removeTemplateParameters(const IndexedTypeIdentifier& identifier, int behindPosition); Identifier removeTemplateParameters(const Identifier& id, int behindPosition) { Identifier ret(id); ret.clearTemplateIdentifiers(); for (unsigned int a = 0; a < id.templateIdentifiersCount(); ++a) { IndexedTypeIdentifier replacement = removeTemplateParameters(id.templateIdentifier(a), behindPosition); if (( int ) a < behindPosition) { ret.appendTemplateIdentifier(replacement); } else { ret.appendTemplateIdentifier(IndexedTypeIdentifier(QualifiedIdentifier(QStringLiteral("...")))); break; } } return ret; } IndexedTypeIdentifier removeTemplateParameters(const IndexedTypeIdentifier& identifier, int behindPosition) { IndexedTypeIdentifier ret(identifier); QualifiedIdentifier oldId(identifier.identifier().identifier()); QualifiedIdentifier qid; for (int a = 0; a < oldId.count(); ++a) { qid.push(removeTemplateParameters(oldId.at(a), behindPosition)); } ret.setIdentifier(qid); return ret; } IndexedType removeConstModifier(const IndexedType& indexedType) { AbstractType::Ptr type = indexedType.abstractType(); type->setModifiers(type->modifiers() & (~AbstractType::ConstModifier)); return type->indexed(); } AbstractType::Ptr shortenTypeForViewing(const AbstractType::Ptr& type) { struct ShortenAliasExchanger : public TypeExchanger { AbstractType::Ptr exchange(const AbstractType::Ptr& type) override { if (!type) { return type; } AbstractType::Ptr newType(type->clone()); TypeAliasType::Ptr alias = type.cast(); if (alias) { //If the aliased type has less involved template arguments, prefer it AbstractType::Ptr shortenedTarget = exchange(alias->type()); if (shortenedTarget && shortenedTarget->toString().count(QLatin1Char('<')) < alias->toString().count(QLatin1Char('<')) && reservedIdentifierCount(shortenedTarget->toString()) <= reservedIdentifierCount(alias->toString())) { shortenedTarget->setModifiers(shortenedTarget->modifiers() | alias->modifiers()); return shortenedTarget; } } newType->exchangeTypes(this); return newType; } }; ShortenAliasExchanger exchanger; return exchanger.exchange(type); } ///Returns a type that has all template types replaced with DelayedType's that have their template default parameters stripped away, ///and all scope prefixes removed that are redundant within the given context ///The returned type should not actively be used in the type-system, but rather only for displaying. AbstractType::Ptr stripType(const AbstractType::Ptr& type, DUContext* ctx) { if (!type) { return AbstractType::Ptr(); } struct ShortenTemplateDefaultParameter : public TypeExchanger { DUContext* ctx; explicit ShortenTemplateDefaultParameter(DUContext* _ctx) : ctx(_ctx) { Q_ASSERT(ctx); } AbstractType::Ptr exchange(const AbstractType::Ptr& type) override { if (!type) { return type; } AbstractType::Ptr newType(type->clone()); if (const IdentifiedType * idType = dynamic_cast(type.data())) { Declaration* decl = idType->declaration(ctx->topContext()); if (!decl) { return type; } QualifiedIdentifier newTypeName; #if 0 // from oldcpp if (TemplateDeclaration * tempDecl = dynamic_cast(decl)) { if (decl->context()->type() == DUContext::Class && decl->context()->owner()) { //Strip template default-parameters from the parent class AbstractType::Ptr parentType = stripType(decl->context()->owner()->abstractType(), ctx); if (parentType) { newTypeName = QualifiedIdentifier(parentType->toString(), true); } } if (newTypeName.isEmpty()) { newTypeName = decl->context()->scopeIdentifier(true); } Identifier currentId; if (!idType->qualifiedIdentifier().isEmpty()) { currentId = idType->qualifiedIdentifier().last(); } currentId.clearTemplateIdentifiers(); InstantiationInformation instantiationInfo = tempDecl->instantiatedWith().information(); InstantiationInformation newInformation(instantiationInfo); newInformation.templateParametersList().clear(); for (uint neededParameters = 0; neededParameters < instantiationInfo.templateParametersSize(); ++neededParameters) { newInformation.templateParametersList().append(instantiationInfo.templateParameters()[neededParameters]); AbstractType::Ptr niceParam = stripType(instantiationInfo.templateParameters()[neededParameters].abstractType(), ctx); if (niceParam) { currentId.appendTemplateIdentifier(IndexedTypeIdentifier(niceParam->toString(), true)); // debug() << "testing param" << niceParam->toString(); } if (tempDecl->instantiate(newInformation, ctx->topContext()) == decl) { // debug() << "got full instantiation"; break; } } newTypeName.push(currentId); } else { newTypeName = decl->qualifiedIdentifier(); } #endif newTypeName = decl->qualifiedIdentifier(); //Strip unneded prefixes of the scope QualifiedIdentifier candidate = newTypeName; while (candidate.count() > 1) { candidate = candidate.mid(1); QList decls = ctx->findDeclarations(candidate); if (decls.isEmpty()) { continue; // type aliases might be available for nested sub scopes, hence we must not break early } if (decls[0]->kind() != Declaration::Type || removeConstModifier(decls[0]->indexedType()) != removeConstModifier(IndexedType(type))) { break; } newTypeName = candidate; } if (newTypeName == decl->qualifiedIdentifier()) { return type; } DelayedType::Ptr ret(new DelayedType); IndexedTypeIdentifier ti(newTypeName); ti.setIsConstant(type->modifiers() & AbstractType::ConstModifier); ret->setIdentifier(ti); return ret.cast(); } newType->exchangeTypes(this); return newType; } }; ShortenTemplateDefaultParameter exchanger(ctx); return exchanger.exchange(type); } } namespace CodegenHelper { AbstractType::Ptr typeForShortenedString(Declaration* decl) { AbstractType::Ptr type = decl->abstractType(); if (decl->isTypeAlias()) { if (type.cast()) { type = type.cast()->type(); } } if (decl->isFunctionDeclaration()) { FunctionType::Ptr funType = decl->type(); if (!funType) { return AbstractType::Ptr(); } type = funType->returnType(); } return type; } QString shortenedTypeString(Declaration* decl, DUContext* ctx, int desiredLength, const QualifiedIdentifier& stripPrefix) { return shortenedTypeString(typeForShortenedString(decl), ctx, desiredLength, stripPrefix); } QString simplifiedTypeString(const AbstractType::Ptr& type, DUContext* visibilityFrom) { return shortenedTypeString(type, visibilityFrom, 100000); } QString shortenedTypeString(const AbstractType::Ptr& type, DUContext* ctx, int desiredLength, const QualifiedIdentifier& stripPrefix) { return shortenedTypeIdentifier(type, ctx, desiredLength, stripPrefix).toString(); } IndexedTypeIdentifier shortenedTypeIdentifier(const AbstractType::Ptr& type_, DUContext* ctx, int desiredLength, const QualifiedIdentifier& stripPrefix) { bool isReference = false; bool isRValue = false; auto type = type_; if (const auto& refType = type.cast()) { isReference = true; type = refType->baseType(); isRValue = refType->isRValue(); } type = shortenTypeForViewing(type); if (ctx) { type = stripType(type, ctx); } if (!type) { return IndexedTypeIdentifier(); } IndexedTypeIdentifier identifier = identifierForType(type, ctx ? ctx->topContext() : nullptr); identifier = stripPrefixIdentifiers(identifier, stripPrefix); if (isReference) { identifier.setIsReference(true); } if (isRValue) { identifier.setIsRValue(true); } int removeTemplateParametersFrom = 10; while (identifier.toString().length() > desiredLength * 3 && removeTemplateParametersFrom >= 0) { --removeTemplateParametersFrom; identifier = removeTemplateParameters(identifier, removeTemplateParametersFrom); } return identifier; } QString makeSignatureString(const KDevelop::Declaration* functionDecl, const Signature& signature, const bool editingDefinition) { if (!functionDecl || !functionDecl->internalContext()) { return {}; } const auto visibilityFrom = functionDecl->internalContext()->parentContext(); if (!visibilityFrom) { return {}; } QString ret; if (!editingDefinition) { auto classMember = dynamic_cast(functionDecl); if (classMember && classMember->isStatic()) { ret += QLatin1String("static "); } } // constructors don't have a return type if (signature.returnType.isValid()) { ret += CodegenHelper::simplifiedTypeString(signature.returnType.abstractType(), - visibilityFrom); - ret += QLatin1Char(' '); + visibilityFrom) + + QLatin1Char(' '); } ret += editingDefinition ? functionDecl->qualifiedIdentifier().toString() : functionDecl->identifier().toString(); - - ret += QLatin1Char('('); int pos = 0; + QStringList parameters; + parameters.reserve(signature.parameters.size()); foreach(const ParameterItem &item, signature.parameters) { - if (pos != 0) { - ret += QLatin1String(", "); - } - + QString parameter; AbstractType::Ptr type = item.first.abstractType(); QString arrayAppendix; ArrayType::Ptr arrayType; while ((arrayType = type.cast())) { type = arrayType->elementType(); //note: we have to prepend since we iterate from outside, i.e. from right to left. if (arrayType->dimension()) { arrayAppendix.prepend(QStringLiteral("[%1]").arg(arrayType->dimension())); } else { // dimensionless arrayAppendix.prepend(QLatin1String("[]")); } } - ret += CodegenHelper::simplifiedTypeString(type, - visibilityFrom); + parameter += CodegenHelper::simplifiedTypeString(type, visibilityFrom); if (!item.second.isEmpty()) { - ret += QLatin1Char(' ') + item.second; + parameter += QLatin1Char(' ') + item.second; } - ret += arrayAppendix; + parameter += arrayAppendix; if (signature.defaultParams.size() > pos && !signature.defaultParams[pos].isEmpty()) { - ret += QLatin1String(" = ") + signature.defaultParams[pos]; + parameter += QLatin1String(" = ") + signature.defaultParams[pos]; } + parameters.append(parameter); ++pos; } - ret += QLatin1Char(')'); + ret += QLatin1Char('(') + parameters.join(QLatin1String(", ")) + QLatin1Char(')'); if (signature.isConst) { ret += QLatin1String(" const"); } return ret; } } diff --git a/plugins/clang/codegen/sourcemanipulation.cpp b/plugins/clang/codegen/sourcemanipulation.cpp index bc4c651327..1ecc7c41b7 100644 --- a/plugins/clang/codegen/sourcemanipulation.cpp +++ b/plugins/clang/codegen/sourcemanipulation.cpp @@ -1,288 +1,289 @@ /* * This file is part of KDevelop * * Copyright 2009 David Nolden * Copyright 2015 Sergey Kalinichev * * 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 "sourcemanipulation.h" #include #include #include #include #include #include #include #include #include #include #include #include "codegenhelper.h" #include "adaptsignatureaction.h" #include "util/clangdebug.h" using namespace KDevelop; namespace { QualifiedIdentifier stripPrefixes(const DUContextPointer& ctx, const QualifiedIdentifier& id) { if (!ctx) { return id; } auto imports = ctx->fullyApplyAliases({}, ctx->topContext()); if (imports.contains(id)) { return {}; /// The id is a namespace that is imported into the current context } auto basicDecls = ctx->findDeclarations(id, CursorInRevision::invalid(), {}, nullptr, (DUContext::SearchFlags)(DUContext::NoSelfLookUp | DUContext::NoFiltering)); if (basicDecls.isEmpty()) { return id; } auto newId = id.mid(1); auto result = id; while (!newId.isEmpty()) { auto foundDecls = ctx->findDeclarations(newId, CursorInRevision::invalid(), {}, nullptr, (DUContext::SearchFlags)(DUContext::NoSelfLookUp | DUContext::NoFiltering)); if (foundDecls == basicDecls) { result = newId; // must continue to find the shortest possible identifier // esp. for cases where nested namespaces are used (e.g. using namespace a::b::c;) newId = newId.mid(1); } } return result; } // Re-indents the code so the leftmost line starts at zero QString zeroIndentation(const QString& str, int fromLine = 0) { QStringList lines = str.split(QLatin1Char('\n')); QStringList ret; if (fromLine < lines.size()) { ret = lines.mid(0, fromLine); lines = lines.mid(fromLine); } QRegExp nonWhiteSpace(QStringLiteral("\\S")); int minLineStart = 10000; foreach (const auto& line, lines) { int lineStart = line.indexOf(nonWhiteSpace); if (lineStart < minLineStart) { minLineStart = lineStart; } } + ret.reserve(ret.size() + lines.size()); foreach (const auto& line, lines) { ret << line.mid(minLineStart); } return ret.join(QLatin1Char('\n')); } } DocumentChangeSet SourceCodeInsertion::changes() { return m_changeSet; } void SourceCodeInsertion::setSubScope(const QualifiedIdentifier& scope) { m_scope = scope; if (!m_context) { return; } QStringList needNamespace = m_scope.toStringList(); bool foundChild = true; while (!needNamespace.isEmpty() && foundChild) { foundChild = false; foreach (DUContext* child, m_context->childContexts()) { clangDebug() << "checking child" << child->localScopeIdentifier().toString() << "against" << needNamespace.first(); if (child->localScopeIdentifier().toString() == needNamespace.first() && child->type() == DUContext::Namespace) { clangDebug() << "taking"; m_context = child; foundChild = true; needNamespace.pop_front(); break; } } } m_scope = stripPrefixes(m_context, QualifiedIdentifier(needNamespace.join(QStringLiteral("::")))); } QString SourceCodeInsertion::applySubScope(const QString& decl) const { if (m_scope.isEmpty()) { return decl; } QString scopeType = QStringLiteral("namespace"); QString scopeClose; if (m_context && m_context->type() == DUContext::Class) { scopeType = QStringLiteral("struct"); scopeClose = QStringLiteral(";"); } QString ret; foreach (const QString& scope, m_scope.toStringList()) { - ret += scopeType + QStringLiteral(" ") + scope + QStringLiteral(" {\n"); + ret += scopeType + QLatin1Char(' ') + scope + QStringLiteral(" {\n"); } ret += decl; - ret += QStringLiteral("}") + scopeClose + QStringLiteral("\n").repeated(m_scope.count()); + ret += QLatin1Char('}') + scopeClose + QStringLiteral("\n").repeated(m_scope.count()); return ret; } SourceCodeInsertion::SourceCodeInsertion(TopDUContext* topContext) : m_context(topContext) , m_topContext(topContext) , m_codeRepresentation(createCodeRepresentation(m_topContext->url())) { } SourceCodeInsertion::~SourceCodeInsertion() { } KTextEditor::Cursor SourceCodeInsertion::end() const { auto ret = m_context->rangeInCurrentRevision().end(); if (m_codeRepresentation && m_codeRepresentation->lines() && dynamic_cast(m_context.data())) { ret.setLine(m_codeRepresentation->lines() - 1); ret.setColumn(m_codeRepresentation->line(ret.line()).size()); } return ret; } KTextEditor::Range SourceCodeInsertion::insertionRange(int line) { if (line == 0 || !m_codeRepresentation) { return KTextEditor::Range(line, 0, line, 0); } KTextEditor::Range range(line - 1, m_codeRepresentation->line(line - 1).size(), line - 1, m_codeRepresentation->line(line - 1).size()); // If the context finishes on that line, then this will need adjusting if (!m_context->rangeInCurrentRevision().contains(range)) { range.start() = m_context->rangeInCurrentRevision().end(); if (range.start().column() > 0) { range.start() = range.start() - KTextEditor::Cursor(0, 1); } range.end() = range.start(); } return range; } bool SourceCodeInsertion::insertFunctionDeclaration(KDevelop::Declaration* declaration, const Identifier& id, const QString& body) { if (!m_context) { return false; } Signature signature; const auto localDeclarations = declaration->internalContext()->localDeclarations(); signature.parameters.reserve(localDeclarations.count()); std::transform(localDeclarations.begin(), localDeclarations.end(), std::back_inserter(signature.parameters), [] (Declaration* argument) -> ParameterItem { return {IndexedType(argument->indexedType()), argument->identifier().toString()}; }); auto funcType = declaration->type(); auto returnType = funcType->returnType(); if (auto classFunDecl = dynamic_cast(declaration)) { if (classFunDecl->isConstructor() || classFunDecl->isDestructor()) { returnType = nullptr; } } signature.returnType = IndexedType(returnType); signature.isConst = funcType->modifiers() & AbstractType::ConstModifier; QString decl = CodegenHelper::makeSignatureString(declaration, signature, true); decl.replace(declaration->qualifiedIdentifier().toString(), id.toString()); if (body.isEmpty()) { - decl += QStringLiteral(";"); + decl += QLatin1Char(';'); } else { if (!body.startsWith(QLatin1Char(' ')) && !body.startsWith(QLatin1Char('\n'))) { - decl += QStringLiteral(" "); + decl += QLatin1Char(' '); } decl += zeroIndentation(body); } int line = findInsertionPoint(); decl = QStringLiteral("\n\n") + applySubScope(decl); const auto url = declaration->url().toUrl(); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); auto i = ICore::self()->sourceFormatterController()->formatterForUrl(url, mime); if (i) { decl = i->formatSource(decl, url, mime); } return m_changeSet.addChange(DocumentChange(m_context->url(), insertionRange(line), QString(), decl)); } int SourceCodeInsertion::findInsertionPoint() const { int line = end().line(); foreach (auto decl, m_context->localDeclarations()) { if (m_context->type() == DUContext::Class) { continue; } if (!dynamic_cast(decl)) { continue; } line = decl->range().end.line + 1; if (decl->internalContext()) { line = decl->internalContext()->range().end.line + 1; } } clangDebug() << line << m_context->scopeIdentifier(true) << m_context->rangeInCurrentRevision() << m_context->url().toUrl() << m_context->parentContext(); clangDebug() << "count of declarations:" << m_context->topContext()->localDeclarations().size(); return line; } diff --git a/plugins/clang/duchain/clangproblem.cpp b/plugins/clang/duchain/clangproblem.cpp index 7294243115..190b5f4c1c 100644 --- a/plugins/clang/duchain/clangproblem.cpp +++ b/plugins/clang/duchain/clangproblem.cpp @@ -1,269 +1,269 @@ /* * Copyright 2014 Kevin Funk * * 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 "clangproblem.h" #include #include #include "util/clangtypes.h" #include "util/clangdebug.h" #include #include #include using namespace KDevelop; namespace { IProblem::Severity diagnosticSeverityToSeverity(CXDiagnosticSeverity severity, const QString& optionName) { switch (severity) { case CXDiagnostic_Fatal: case CXDiagnostic_Error: return IProblem::Error; case CXDiagnostic_Warning: if (optionName.startsWith(QLatin1String("-Wunused-"))) { return IProblem::Hint; } return IProblem::Warning; break; default: return IProblem::Hint; } } /** * Clang diagnostic messages always start with a lowercase character * * @return Prettified version, starting with uppercase character */ inline QString prettyDiagnosticSpelling(const QString& str) { QString ret = str; if (ret.isEmpty()) { return {}; } ret[0] = ret[0].toUpper(); return ret; } ClangFixits fixitsForDiagnostic(CXDiagnostic diagnostic) { ClangFixits fixits; auto numFixits = clang_getDiagnosticNumFixIts(diagnostic); fixits.reserve(numFixits); for (uint i = 0; i < numFixits; ++i) { CXSourceRange range; const QString replacementText = ClangString(clang_getDiagnosticFixIt(diagnostic, i, &range)).toString(); const auto docRange = ClangRange(range).toDocumentRange(); auto doc = KDevelop::ICore::self()->documentController()->documentForUrl(docRange.document.toUrl()); const QString original = doc ? doc->text(docRange) : QString{}; fixits << ClangFixit{replacementText, docRange, QString(), original}; } return fixits; } } QDebug operator<<(QDebug debug, const ClangFixit& fixit) { debug.nospace() << "ClangFixit[" << "replacementText=" << fixit.replacementText << ", range=" << fixit.range << ", description=" << fixit.description << "]"; return debug; } ClangProblem::ClangProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) { const QString diagnosticOption = ClangString(clang_getDiagnosticOption(diagnostic, nullptr)).toString(); auto severity = diagnosticSeverityToSeverity(clang_getDiagnosticSeverity(diagnostic), diagnosticOption); setSeverity(severity); QString description = ClangString(clang_getDiagnosticSpelling(diagnostic)).toString(); if (!diagnosticOption.isEmpty()) { - description.append(QStringLiteral(" [%1]").arg(diagnosticOption)); + description.append(QLatin1String(" [") + diagnosticOption + QLatin1Char(']')); } setDescription(prettyDiagnosticSpelling(description)); ClangLocation location(clang_getDiagnosticLocation(diagnostic)); CXFile diagnosticFile; clang_getFileLocation(location, &diagnosticFile, nullptr, nullptr, nullptr); const ClangString fileName(clang_getFileName(diagnosticFile)); DocumentRange docRange(IndexedString(QUrl::fromLocalFile(fileName.toString()).adjusted(QUrl::NormalizePathSegments)), KTextEditor::Range(location, location)); const uint numRanges = clang_getDiagnosticNumRanges(diagnostic); for (uint i = 0; i < numRanges; ++i) { auto range = ClangRange(clang_getDiagnosticRange(diagnostic, i)).toRange(); if(!range.isValid()){ continue; } if (range.start() < docRange.start()) { docRange.setStart(range.start()); } if (range.end() > docRange.end()) { docRange.setEnd(range.end()); } } if (docRange.isEmpty()) { // try to find a bigger range for the given location by using the token at the given location CXFile file = nullptr; unsigned line = 0; unsigned column = 0; clang_getExpansionLocation(location, &file, &line, &column, nullptr); // just skip ahead some characters, hoping that it's sufficient to encompass // a token we can use for building the range auto nextLocation = clang_getLocation(unit, file, line, column + 100); auto rangeToTokenize = clang_getRange(location, nextLocation); const ClangTokens tokens(unit, rangeToTokenize); if (tokens.size()) { docRange.setRange(ClangRange(clang_getTokenExtent(unit, tokens.at(0))).toRange()); } } setFixits(fixitsForDiagnostic(diagnostic)); setFinalLocation(docRange); setSource(IProblem::SemanticAnalysis); QVector diagnostics; auto childDiagnostics = clang_getChildDiagnostics(diagnostic); auto numChildDiagnostics = clang_getNumDiagnosticsInSet(childDiagnostics); diagnostics.reserve(numChildDiagnostics); for (uint j = 0; j < numChildDiagnostics; ++j) { auto childDiagnostic = clang_getDiagnosticInSet(childDiagnostics, j); ClangProblem::Ptr problem(new ClangProblem(childDiagnostic, unit)); diagnostics << ProblemPointer(problem.data()); } setDiagnostics(diagnostics); } IAssistant::Ptr ClangProblem::solutionAssistant() const { if (allFixits().isEmpty()) { return {}; } return IAssistant::Ptr(new ClangFixitAssistant(allFixits())); } ClangFixits ClangProblem::fixits() const { return m_fixits; } void ClangProblem::setFixits(const ClangFixits& fixits) { m_fixits = fixits; } ClangFixits ClangProblem::allFixits() const { ClangFixits result; result << m_fixits; for (const IProblem::Ptr& diagnostic : diagnostics()) { const Ptr problem(dynamic_cast(diagnostic.data())); Q_ASSERT(problem); result << problem->allFixits(); } return result; } ClangFixitAssistant::ClangFixitAssistant(const ClangFixits& fixits) : m_title(i18n("Fix-it Hints")) , m_fixits(fixits) { } ClangFixitAssistant::ClangFixitAssistant(const QString& title, const ClangFixits& fixits) : m_title(title) , m_fixits(fixits) { } QString ClangFixitAssistant::title() const { return m_title; } void ClangFixitAssistant::createActions() { KDevelop::IAssistant::createActions(); for (const ClangFixit& fixit : m_fixits) { addAction(IAssistantAction::Ptr(new ClangFixitAction(fixit))); } } ClangFixits ClangFixitAssistant::fixits() const { return m_fixits; } ClangFixitAction::ClangFixitAction(const ClangFixit& fixit) : m_fixit(fixit) { } QString ClangFixitAction::description() const { if (!m_fixit.description.isEmpty()) return m_fixit.description; const auto range = m_fixit.range; if (range.start() == range.end()) { return i18n("Insert \"%1\" at line: %2, column: %3", m_fixit.replacementText, range.start().line()+1, range.start().column()+1); } else if (range.start().line() == range.end().line()) { if (m_fixit.currentText.isEmpty()) { return i18n("Replace text at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } else return i18n("Replace \"%1\" with: \"%2\"", m_fixit.currentText, m_fixit.replacementText); } else { return i18n("Replace multiple lines starting at line: %1, column: %2 with: \"%3\"", range.start().line()+1, range.start().column()+1, m_fixit.replacementText); } } void ClangFixitAction::execute() { DocumentChangeSet changes; { DUChainReadLocker lock; DocumentChange change(m_fixit.range.document, m_fixit.range, m_fixit.currentText, m_fixit.replacementText); change.m_ignoreOldText = !m_fixit.currentText.isEmpty(); changes.addChange(change); } changes.setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); changes.applyAllChanges(); emit executed(this); } diff --git a/plugins/clang/duchain/debugvisitor.cpp b/plugins/clang/duchain/debugvisitor.cpp index c2fca326ed..cb5bff39d9 100644 --- a/plugins/clang/duchain/debugvisitor.cpp +++ b/plugins/clang/duchain/debugvisitor.cpp @@ -1,123 +1,123 @@ /* This file is part of KDevelop Copyright 2013 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "debugvisitor.h" #include "util/clangtypes.h" #include "util/clangutils.h" namespace { struct ClientData { QTextStream* out; ParseSession* session; CXFile file; uint depth; }; CXChildVisitResult visitCursor(CXCursor cursor, CXCursor /*parent*/, CXClientData d) { auto data = static_cast(d); const auto kind = clang_getCursorKind(cursor); const auto location = clang_getCursorLocation(cursor); CXFile file; clang_getFileLocation(location, &file, nullptr, nullptr, nullptr); // don't skip MemberRefExpr with invalid location, see also: // http://lists.cs.uiuc.edu/pipermail/cfe-dev/2015-May/043114.html if (!ClangUtils::isFileEqual(file, data->file) && (file || kind != CXCursor_MemberRefExpr)) { return CXChildVisit_Continue; } (*data->out) << QByteArray(data->depth * 2, ' '); ClangString kindName(clang_getCursorKindSpelling(kind)); (*data->out) << kindName << " (" << kind << ") "; auto type = clang_getCursorType(cursor); if (type.kind != CXType_Invalid) { ClangString typeName(clang_getTypeSpelling(type)); - (*data->out) << "| type: \"" << typeName << "\"" << " (" << type.kind << ") "; + (*data->out) << "| type: \"" << typeName << '\"' << " (" << type.kind << ") "; } auto canonicalType = clang_getCanonicalType(type); if (canonicalType.kind != CXType_Invalid && !clang_equalTypes(type, canonicalType)) { ClangString typeName(clang_getTypeSpelling(canonicalType)); - (*data->out) << "| canonical type: \"" << typeName << "\"" << " (" << canonicalType.kind << ") "; + (*data->out) << "| canonical type: \"" << typeName << '\"' << " (" << canonicalType.kind << ") "; } auto typedefType = clang_getTypedefDeclUnderlyingType(cursor); if (typedefType.kind != CXType_Invalid && !clang_equalTypes(type, typedefType)) { ClangString typeName(clang_getTypeSpelling(typedefType)); - (*data->out) << "| typedef type: \"" << typeName << "\"" << " (" << typedefType.kind << ") "; + (*data->out) << "| typedef type: \"" << typeName << '\"' << " (" << typedefType.kind << ") "; } ClangString displayName(clang_getCursorDisplayName(cursor)); if (!displayName.isEmpty()) { (*data->out) << "| display: \"" << displayName << "\" "; } auto cursorExtent = ClangRange(clang_getCursorExtent(cursor)).toRange(); ClangString fileName(clang_getFileName(file)); (*data->out) << "| loc: " << fileName << '@' << '[' << '(' << cursorExtent.start().line()+1 << ',' << cursorExtent.start().column()+1 << ")," << '(' << cursorExtent.end().line()+1 << ',' << cursorExtent.end().column()+1 << ")] "; auto spellingNameRange = ClangRange(clang_Cursor_getSpellingNameRange(cursor, 0, 0)).toRange(); (*data->out) << "| sp-name-range: [" << '(' << spellingNameRange.start().line()+1 << ',' << spellingNameRange.start().column()+1 << ")," << '(' << spellingNameRange.end().line()+1 << ',' << spellingNameRange.end().column()+1 << ")] "; if (clang_isDeclaration(kind)) { (*data->out) << "| isDecl"; } else { auto referenced = clang_getCursorReferenced(cursor); if (kind != CXCursor_UnexposedExpr && !clang_equalCursors(clang_getNullCursor(), referenced)) { (*data->out) << "| isUse"; } } (*data->out) << endl; ClientData childData{data->out, data->session, data->file, data->depth + 1}; clang_visitChildren(cursor, &visitCursor, &childData); return CXChildVisit_Continue; } } DebugVisitor::DebugVisitor(ParseSession* session) : m_session(session) { } void DebugVisitor::visit(CXTranslationUnit unit, CXFile file) { auto cursor = clang_getTranslationUnitCursor(unit); QTextStream out(stdout); ClientData data {&out, m_session, file, 0}; clang_visitChildren(cursor, &visitCursor, &data); } diff --git a/plugins/clang/duchain/documentfinderhelpers.cpp b/plugins/clang/duchain/documentfinderhelpers.cpp index aeb036e402..10793421ec 100644 --- a/plugins/clang/duchain/documentfinderhelpers.cpp +++ b/plugins/clang/duchain/documentfinderhelpers.cpp @@ -1,281 +1,282 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "documentfinderhelpers.h" #include "duchain/clanghelpers.h" #include #include #include #include #include #include using namespace KDevelop; namespace { enum FileType { Unknown, ///< Doesn't belong to C++ Header, ///< Is a header file Source ///< Is a C(++) file }; class PotentialBuddyCollector : public DUChainUtils::DUChainItemFilter { public: enum BuddyMode { Header, Source }; explicit PotentialBuddyCollector(BuddyMode mode) : mode(mode) {} bool accept(Declaration* decl) override { if (decl->range().isEmpty()) return false; if (mode == Header && decl->isFunctionDeclaration()) { // Search for definitions of our declarations FunctionDefinition* def = FunctionDefinition::definition(decl); if (def) { vote(def->url().toUrl()); } return true; } else if (mode == Source && decl->isFunctionDeclaration()) { FunctionDefinition* fdef = dynamic_cast(decl); if (fdef) { Declaration* fdecl = fdef->declaration(); if (fdecl) { vote(fdecl->url().toUrl()); } } return true; } else { return false; } } bool accept(DUContext* ctx) override { if (ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper ) { return true; } else { return false; } } QUrl bestBuddy() const { QUrl ret; int bestCount = 0; for (auto it = m_buddyFiles.begin(); it != m_buddyFiles.end(); ++it) { if(it.value() > bestCount) { bestCount = it.value(); ret = it.key(); } } return ret; } private: BuddyMode mode; QHash m_buddyFiles; void vote(const QUrl& url) { m_buddyFiles[url]++; } }; /** * Tries to find a buddy file to the given file by looking at the DUChain. * * The project might keep source files separate from headers. To cover * this situation, we examine DUChain for the most probable buddy file. * This of course only works if we have parsed the buddy file, but it is * better than nothing. * * @param url url of the source/header file to find a buddy for * @param type type of the file @p url * * @returns QUrl of the most probable buddy file, or an empty url **/ QUrl duchainBuddyFile(const QUrl& url, FileType type) { DUChainReadLocker lock; auto ctx = DUChainUtils::standardContextForUrl(url); if (ctx) { PotentialBuddyCollector collector( type == Header ? PotentialBuddyCollector::Header : PotentialBuddyCollector::Source ); DUChainUtils::collectItems(ctx, collector); return collector.bestBuddy(); } return QUrl(); } /** * Generates the base path (without extension) and the file type * for the specified url. * * @returns pair of base path and file type which has been found for @p url. */ QPair basePathAndTypeForUrl(const QUrl &url) { QString path = url.toLocalFile(); int idxSlash = path.lastIndexOf(QLatin1Char('/')); int idxDot = path.lastIndexOf(QLatin1Char('.')); FileType fileType = Unknown; QString basePath; if (idxSlash >= 0 && idxDot >= 0 && idxDot > idxSlash) { basePath = path.left(idxDot); if (idxDot + 1 < path.length()) { QString extension = path.mid(idxDot + 1); if (ClangHelpers::isHeader(extension)) { fileType = Header; } else if (ClangHelpers::isSource(extension)) { fileType = Source; } } } else { basePath = path; } return qMakePair(basePath, fileType); } } namespace DocumentFinderHelpers { QStringList mimeTypesList() { static const QStringList mimeTypes = { QStringLiteral("text/x-chdr"), QStringLiteral("text/x-c++hdr"), QStringLiteral("text/x-csrc"), QStringLiteral("text/x-c++src"), QStringLiteral("text/x-objcsrc") }; return mimeTypes; } bool areBuddies(const QUrl &url1, const QUrl& url2) { auto type1 = basePathAndTypeForUrl(url1); auto type2 = basePathAndTypeForUrl(url2); QUrl headerPath; QUrl sourcePath; // Check that one file is a header, the other one is source if (type1.second == Header && type2.second == Source) { headerPath = url1; sourcePath = url2; } else if (type1.second == Source && type2.second == Header) { headerPath = url2; sourcePath = url1; } else { // Some other file constellation return false; } // The simplest directory layout is with header + source in one directory. // So check that first. if (type1.first == type2.first) { return true; } // Also check if the DUChain thinks this is likely if (duchainBuddyFile(sourcePath, Source) == headerPath) { return true; } return false; } bool buddyOrder(const QUrl &url1, const QUrl& url2) { auto type1 = basePathAndTypeForUrl(url1); auto type2 = basePathAndTypeForUrl(url2); // Precondition is that the two URLs are buddies, so don't check it return(type1.second == Header && type2.second == Source); } QVector potentialBuddies(const QUrl& url, bool checkDUChain) { auto type = basePathAndTypeForUrl(url); // Don't do anything for types we don't know if (type.second == Unknown) { return {}; } // Depending on the buddy's file type we either generate source extensions (for headers) // or header extensions (for sources) const auto& extensions = ( type.second == Header ? ClangHelpers::sourceExtensions() : ClangHelpers::headerExtensions() ); QVector< QUrl > buddies; + buddies.reserve(extensions.size()); for(const QString& extension : extensions) { if (!extension.contains(QLatin1Char('.'))) { buddies.append(QUrl::fromLocalFile(type.first + QLatin1Char('.') + extension)); } else { buddies.append(QUrl::fromLocalFile(type.first + extension)); } } if (checkDUChain) { // Also ask DUChain for a guess QUrl bestBuddy = duchainBuddyFile(url, type.second); if (!buddies.contains(bestBuddy)) { buddies.append(bestBuddy); } } return buddies; } QString sourceForHeader(const QString& headerPath) { if (!ClangHelpers::isHeader(headerPath)) { return {}; } QString targetUrl; auto buddies = DocumentFinderHelpers::potentialBuddies(QUrl::fromLocalFile(headerPath)); for (const auto& buddy : buddies) { const auto local = buddy.toLocalFile(); if (QFileInfo::exists(local)) { targetUrl = local; break; } } return targetUrl; } } diff --git a/plugins/clang/duchain/macronavigationcontext.cpp b/plugins/clang/duchain/macronavigationcontext.cpp index 7a1e37938b..3c065efdcd 100644 --- a/plugins/clang/duchain/macronavigationcontext.cpp +++ b/plugins/clang/duchain/macronavigationcontext.cpp @@ -1,94 +1,95 @@ /* Copyright 2007 David Nolden Copyright 2014 Kevin Funk 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 "macronavigationcontext.h" #include "util/clangdebug.h" #include "util/clangutils.h" #include #include using namespace KDevelop; MacroNavigationContext::MacroNavigationContext(const MacroDefinition::Ptr& macro, const KDevelop::DocumentCursor& /* expansionLocation */) : m_macro(macro) { } MacroNavigationContext::~MacroNavigationContext() { } QString MacroNavigationContext::name() const { return m_macro->identifier().toString(); } QString MacroNavigationContext::html(bool shorten) { clear(); modifyHtml() += QLatin1String("

    ") + fontSizePrefix(shorten); addExternalHtml(prefix()); QStringList parameterList; + parameterList.reserve(m_macro->parametersSize()); FOREACH_FUNCTION(const auto& parameter, m_macro->parameters) { parameterList << parameter.str(); } const QString parameters = (!parameterList.isEmpty() ? QStringLiteral("(%1)").arg(parameterList.join(QStringLiteral(", "))) : QString()); const QUrl url = m_macro->url().toUrl(); const QString path = url.toLocalFile(); KTextEditor::Cursor cursor(m_macro->rangeInCurrentRevision().start()); NavigationAction action(url, cursor); modifyHtml() += i18nc("%1: macro type, i.e.: 'Function macro' or just 'Macro'" "%2: the macro name and arguments", "%1: %2", (m_macro->isFunctionLike() ? i18n("Function macro") : i18n("Macro")), importantHighlight(name()) + parameters); modifyHtml() += QStringLiteral("
    "); modifyHtml() += i18nc("%1: the link to the definition", "Defined in: %1", createLink(QStringLiteral("%1 :%2").arg(url.fileName()).arg(cursor.line()+1), path, action)); modifyHtml() += QStringLiteral(" "); //The action name _must_ stay "show_uses", since that is also used from outside makeLink(i18n("Show uses"), QStringLiteral("show_uses"), NavigationAction(m_macro.dynamicCast(), NavigationAction::NavigateUses)); auto code = m_macro->definition().str(); modifyHtml() += QLatin1String("

    ") + i18n("Body: "); modifyHtml() += QLatin1String("") + code.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
    ")) + QLatin1String("
    "); modifyHtml() += QStringLiteral("

    "); modifyHtml() += fontSizeSuffix(shorten) + QLatin1String("

    "); return currentHtml(); } QString MacroNavigationContext::retrievePreprocessedBody(const DocumentCursor& /*expansionLocation*/) const { const TopDUContext* topContext = m_macro->topContext(); if (!topContext) { return QString(); } // TODO: Implement me. Still not exactly sure what do to here... return QString(); } diff --git a/plugins/clang/duchain/todoextractor.cpp b/plugins/clang/duchain/todoextractor.cpp index 5426db17d7..4de611b829 100644 --- a/plugins/clang/duchain/todoextractor.cpp +++ b/plugins/clang/duchain/todoextractor.cpp @@ -1,214 +1,216 @@ /* * Copyright 2014 Kevin Funk * * 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 "todoextractor.h" #include "../util/clangtypes.h" #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { inline int findEndOfLineOrEnd(const QString& str, int from = 0) { const int index = str.indexOf(QLatin1Char('\n'), from); return (index == -1 ? str.length() : index); } inline int findBeginningOfLineOrStart(const QString& str, int from = 0) { const int index = str.lastIndexOf(QLatin1Char('\n'), from); return (index == -1 ? 0 : index+1); } inline int findEndOfCommentOrEnd(const QString& str, int from = 0) { const int index = str.indexOf(QLatin1String("*/"), from); return (index == -1 ? str.length() : index); } /** * @brief Class for parsing to-do items out of a given comment string * * Make sure this class is very performant as it will be used for every comment string * throughout the code base. * * So: No unnecessary deep-copies of strings if possible! */ class CommentTodoParser { public: struct Result { /// Description of the problem QString description; /// Range within the comment KTextEditor::Range localRange; }; CommentTodoParser(const QString& str, const QStringList& markerWords) : m_str(str) , m_todoMarkerWords(markerWords) { skipUntilMarkerWord(); } QVector results() const { return m_results; } private: void skipUntilMarkerWord() { // in the most-cases, we won't find a to-do item // make sure this case is sufficiently fast // for each to-do marker, scan the comment text foreach (const QString& todoMarker, m_todoMarkerWords) { int offset = m_str.indexOf(todoMarker, m_offset); if (offset != -1) { m_offset = offset; parseTodoMarker(); } } } void skipUntilNewline() { m_offset = findEndOfLineOrEnd(m_str, m_offset); } void parseTodoMarker() { // okay, we've found something // m_offset points to the start of the to-do item const int lineStart = findBeginningOfLineOrStart(m_str, m_offset); const int lineEnd = findEndOfLineOrEnd(m_str, m_offset); Q_ASSERT(lineStart <= m_offset); Q_ASSERT(lineEnd > m_offset); QString text = m_str.mid(m_offset, lineEnd - m_offset); Q_ASSERT(!text.contains(QLatin1Char('\n'))); // there's nothing to be stripped on the left side, hence ignore that text.chop(text.length() - findEndOfCommentOrEnd(text)); text = text.trimmed(); // remove additional whitespace from the end // check at what line within the comment we are by just counting the newlines until now const int line = std::count(m_str.constBegin(), m_str.constBegin() + m_offset, QLatin1Char('\n')); KTextEditor::Cursor start = {line, m_offset - lineStart}; KTextEditor::Cursor end = {line, start.column() + text.length()}; m_results << Result{text, {start, end}}; skipUntilNewline(); skipUntilMarkerWord(); } inline bool atEnd() const { return m_offset < m_str.length(); } private: const QString m_str; const QStringList m_todoMarkerWords; int m_offset = 0; QVector m_results; }; } Q_DECLARE_TYPEINFO(CommentTodoParser::Result, Q_MOVABLE_TYPE); TodoExtractor::TodoExtractor(CXTranslationUnit unit, CXFile file) : m_unit(unit) , m_file(file) , m_todoMarkerWords(KDevelop::ICore::self()->languageController()->completionSettings()->todoMarkerWords()) { extractTodos(); } void TodoExtractor::extractTodos() { using uintLimits = std::numeric_limits; auto start = clang_getLocation(m_unit, m_file, 1, 1); auto end = clang_getLocation(m_unit, m_file, uintLimits::max(), uintLimits::max()); auto range = clang_getRange(start, end); IndexedString path(QDir(ClangString(clang_getFileName(m_file)).toString()).canonicalPath()); if(clang_Range_isNull(range)){ return; } const ClangTokens tokens(m_unit, range); for (CXToken token : tokens) { CXTokenKind tokenKind = clang_getTokenKind(token); if (tokenKind != CXToken_Comment) { continue; } CXString tokenSpelling = clang_getTokenSpelling(m_unit, token); auto tokenRange = ClangRange(clang_getTokenExtent(m_unit, token)).toRange(); const QString text = ClangString(tokenSpelling).toString(); CommentTodoParser parser(text, m_todoMarkerWords); - foreach (const CommentTodoParser::Result& result, parser.results()) { + const auto parserResults = parser.results(); + m_problems.reserve(m_problems.size() + parserResults.size()); + for (const CommentTodoParser::Result& result : parserResults) { ProblemPointer problem(new Problem); problem->setDescription(result.description); problem->setSeverity(IProblem::Hint); problem->setSource(IProblem::ToDo); // move the local range to the correct location // note: localRange is the range *within* the comment only auto localRange = result.localRange; KTextEditor::Range todoRange{ tokenRange.start().line() + localRange.start().line(), localRange.start().column(), tokenRange.start().line() + localRange.end().line(), localRange.end().column()}; problem->setFinalLocation({path, todoRange}); m_problems << problem; } } } QList< ProblemPointer > TodoExtractor::problems() const { return m_problems; } diff --git a/plugins/clang/duchain/types/classspecializationtype.cpp b/plugins/clang/duchain/types/classspecializationtype.cpp index c4ad0270ff..8ba42df90c 100644 --- a/plugins/clang/duchain/types/classspecializationtype.cpp +++ b/plugins/clang/duchain/types/classspecializationtype.cpp @@ -1,157 +1,156 @@ /* * This file is part of KDevelop * * Copyright 2015 Sergey Kalinichev * * 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 "classspecializationtype.h" using namespace KDevelop; #include #include // The type is registered in DUChainUtils::registerDUChainItems // REGISTER_TYPE(ClassSpecializationType); DEFINE_LIST_MEMBER_HASH(ClassSpecializationTypeData, parameters, IndexedType) ClassSpecializationTypeData::ClassSpecializationTypeData() { initializeAppendedLists(m_dynamic); } ClassSpecializationTypeData::ClassSpecializationTypeData(const ClassSpecializationTypeData& rhs) : KDevelop::StructureTypeData(rhs) { initializeAppendedLists(m_dynamic); copyListsFrom(rhs); } ClassSpecializationTypeData::~ClassSpecializationTypeData() { freeAppendedLists(); } ClassSpecializationTypeData& ClassSpecializationTypeData::operator=(const ClassSpecializationTypeData&) { return *this; } ClassSpecializationType::ClassSpecializationType(const ClassSpecializationType& rhs) : KDevelop::StructureType(copyData(*rhs.d_func())) {} ClassSpecializationType::ClassSpecializationType(ClassSpecializationTypeData& data) : KDevelop::StructureType(data) {} AbstractType* ClassSpecializationType::clone() const { return new ClassSpecializationType(*this); } ClassSpecializationType::ClassSpecializationType() : KDevelop::StructureType(createData()) {} uint ClassSpecializationType::hash() const { KDevHash kdevhash(StructureType::hash()); FOREACH_FUNCTION(const auto& param, d_func()->parameters) { kdevhash << param.hash(); } return kdevhash; } namespace { // we need to skip the template parameters of the last identifier, // so do the stringification manually here QString strippedQid(const QualifiedIdentifier& qid) { QString result; if (qid.explicitlyGlobal()) { result += QLatin1String("::"); } const auto parts = qid.count(); for (int i = 0; i < parts - 1; ++i) { - result += qid.at(i).toString(); - result += QLatin1String("::"); + result += qid.at(i).toString() + QLatin1String("::"); } const auto last = qid.at(parts - 1); result += last.identifier().str(); return result; } } QString ClassSpecializationType::toString() const { QualifiedIdentifier id = qualifiedIdentifier(); if (!id.isEmpty()) { QString result = AbstractType::toString() + strippedQid(id) + QLatin1String("< "); bool first = true; for (const auto& param : templateParameters()) { if (first) { first = false; } else { result += QLatin1String(", "); } result += param.abstractType()->toString(); } result += QLatin1String(" >"); return result; } return StructureType::toString(); } bool ClassSpecializationType::equals(const KDevelop::AbstractType* rhs) const { if (this == rhs) { return true; } auto tt = dynamic_cast(rhs); if (!tt || templateParameters() != tt->templateParameters()) { return false; } return StructureType::equals(rhs); } QVector ClassSpecializationType::templateParameters() const { const auto size = d_func()->parametersSize(); QVector parameters(size); std::copy_n(d_func()->parameters(), size, parameters.begin()); return parameters; } void ClassSpecializationType::addParameter(const KDevelop::IndexedType& param) { d_func_dynamic()->parametersList().append(param); } void ClassSpecializationType::clearParameters() { d_func_dynamic()->parametersList().clear(); } diff --git a/plugins/clang/duchain/unknowndeclarationproblem.cpp b/plugins/clang/duchain/unknowndeclarationproblem.cpp index 9f27d335e5..77afe64954 100644 --- a/plugins/clang/duchain/unknowndeclarationproblem.cpp +++ b/plugins/clang/duchain/unknowndeclarationproblem.cpp @@ -1,557 +1,559 @@ /* * Copyright 2014 Jørgen Kvalsvik * Copyright 2014 Kevin Funk * * 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 "unknowndeclarationproblem.h" #include "clanghelpers.h" #include "parsesession.h" #include "../util/clangdebug.h" #include "../util/clangutils.h" #include "../util/clangtypes.h" #include "../clangsettings/clangsettingsmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { /** Under some conditions, such as when looking up suggestions * for the undeclared namespace 'std' we will get an awful lot * of suggestions. This parameter limits how many suggestions * will pop up, as rarely more than a few will be relevant anyways * * Forward declaration suggestions are included in this number */ const int maxSuggestions = 5; /** * We don't want anything from the bits directory - * we'd rather prefer forwarding includes, such as */ bool isBlacklisted(const QString& path) { if (ClangHelpers::isSource(path)) return true; // Do not allow including directly from the bits directory. // Instead use one of the forwarding headers in other directories, when possible. if (path.contains( QLatin1String("bits") ) && path.contains(QLatin1String("/include/c++/"))) return true; return false; } QStringList scanIncludePaths( const QString& identifier, const QDir& dir, int maxDepth = 3 ) { if (!maxDepth) { return {}; } QStringList candidates; const auto path = dir.absolutePath(); if( isBlacklisted( path ) ) { return {}; } const QStringList nameFilters = {identifier, identifier + QLatin1String(".*")}; for (const auto& file : dir.entryList(nameFilters, QDir::Files)) { if (identifier.compare(file, Qt::CaseInsensitive) == 0 || ClangHelpers::isHeader(file)) { const QString filePath = path + QLatin1Char('/') + file; clangDebug() << "Found candidate file" << filePath; candidates.append( filePath ); } } maxDepth--; for( const auto& subdir : dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) ) candidates += scanIncludePaths( identifier, QDir{ path + QLatin1Char('/') + subdir }, maxDepth ); return candidates; } /** * Find files in dir that match the given identifier. Matches common C++ header file extensions only. */ QStringList scanIncludePaths( const QualifiedIdentifier& identifier, const KDevelop::Path::List& includes ) { const auto stripped_identifier = identifier.last().toString(); QStringList candidates; for( const auto& include : includes ) { candidates += scanIncludePaths( stripped_identifier, QDir{ include.toLocalFile() } ); } std::sort( candidates.begin(), candidates.end() ); candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() ); return candidates; } /** * Determine how much path is shared between two includes. * boost/tr1/unordered_map * boost/tr1/unordered_set * have a shared path of 2 where * boost/tr1/unordered_map * boost/vector * have a shared path of 1 */ int sharedPathLevel(const QString& a, const QString& b) { int shared = -1; for(auto x = a.begin(), y = b.begin(); *x == *y && x != a.end() && y != b.end() ; ++x, ++y ) { if( *x == QDir::separator() ) { ++shared; } } return shared; } /** * Try to find a proper include position from the DUChain: * * look at existing imports (i.e. #include's) and find a fitting * file with the same/similar path to the new include file and use that * * TODO: Implement a fallback scheme */ KDevelop::DocumentRange includeDirectivePosition(const KDevelop::Path& source, const QString& includeFile) { static const QRegularExpression mocFilenameExpression(QStringLiteral("(moc_[^\\/\\\\]+\\.cpp$|\\.moc$)") ); DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range"; return KDevelop::DocumentRange::invalid(); } int line = -1; // look at existing #include statements and re-use them int currentMatchQuality = -1; for( const auto& import : top->importedParentContexts() ) { const auto importFilename = import.context(top)->url().str(); const int matchQuality = sharedPathLevel( importFilename , includeFile ); if( matchQuality < currentMatchQuality ) { continue; } const auto match = mocFilenameExpression.match(importFilename); if (match.hasMatch()) { clangDebug() << "moc file detected in" << source.toUrl().toDisplayString() << ":" << importFilename << "-- not using as include insertion location"; continue; } line = import.position.line + 1; currentMatchQuality = matchQuality; } if( line == -1 ) { /* Insert at the top of the document */ return {IndexedString(source.pathOrUrl()), {0, 0, 0, 0}}; } return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}}; } KDevelop::DocumentRange forwardDeclarationPosition(const QualifiedIdentifier& identifier, const KDevelop::Path& source) { DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range"; return KDevelop::DocumentRange::invalid(); } if (!top->findDeclarations(identifier).isEmpty()) { // Already forward-declared return KDevelop::DocumentRange::invalid(); } int line = std::numeric_limits< int >::max(); for( const auto decl : top->localDeclarations() ) { line = std::min( line, decl->range().start.line ); } if( line == std::numeric_limits< int >::max() ) { return KDevelop::DocumentRange::invalid(); } // We want it one line above the first declaration line = std::max( line - 1, 0 ); return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}}; } /** * Iteratively build all levels of the current scope. A (missing) type anywhere * can be arbitrarily namespaced, so we create the permutations of possible * nestings of namespaces it can currently be in, * * TODO: add detection of namespace aliases, such as 'using namespace KDevelop;' * * namespace foo { * namespace bar { * function baz() { * type var; * } * } * } * * Would give: * foo::bar::baz::type * foo::bar::type * foo::type * type */ QVector findPossibleQualifiedIdentifiers( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::CursorInRevision& cursor ) { DUChainReadLocker lock; const TopDUContext* top = DUChainUtils::standardContextForUrl( file.toUrl() ); if( !top ) { clangDebug() << "unable to find standard context for" << file.toLocalFile() << "Not creating duchain candidates"; return {}; } const auto* context = top->findContextAt( cursor ); if( !context ) { clangDebug() << "No context found at" << cursor; return {}; } QVector declarations{ identifier }; - for( auto scopes = context->scopeIdentifier(); !scopes.isEmpty(); scopes.pop() ) { + auto scopes = context->scopeIdentifier(); + declarations.reserve(declarations.size() + scopes.count()); + for (; !scopes.isEmpty(); scopes.pop()) { declarations.append( scopes + identifier ); } clangDebug() << "Possible declarations:" << declarations; return declarations; } } QStringList UnknownDeclarationProblem::findMatchingIncludeFiles(const QVector& declarations) { DUChainReadLocker lock; QStringList candidates; for (const auto decl: declarations) { // skip declarations that don't belong to us const auto& file = decl->topContext()->parsingEnvironmentFile(); if (!file || file->language() != ParseSession::languageString()) { continue; } if( dynamic_cast( decl ) ) { continue; } if( decl->isForwardDeclaration() ) { continue; } const auto filepath = decl->url().toUrl().toLocalFile(); if( !isBlacklisted( filepath ) ) { candidates << filepath; clangDebug() << "Adding" << filepath << "determined from candidate" << decl->toString(); } for( const auto importer : file->importers() ) { if( importer->imports().count() != 1 && !isBlacklisted( filepath ) ) { continue; } if( importer->topContext()->localDeclarations().count() ) { continue; } const auto filePath = importer->url().toUrl().toLocalFile(); if( isBlacklisted( filePath ) ) { continue; } /* This file is a forwarder, such as * does not actually implement the functions, but include other headers that do * we prefer this to other headers */ candidates << filePath; clangDebug() << "Adding forwarder file" << filePath << "to the result set"; } } std::sort( candidates.begin(), candidates.end() ); candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() ); clangDebug() << "Candidates: " << candidates; return candidates; } namespace { /** * Takes a filepath and the include paths and determines what directive to use. */ ClangFixit directiveForFile( const QString& includefile, const KDevelop::Path::List& includepaths, const KDevelop::Path& source ) { const auto sourceFolder = source.parent(); const Path canonicalFile( QFileInfo( includefile ).canonicalFilePath() ); QString shortestDirective; bool isRelative = false; // we can include the file directly if (sourceFolder == canonicalFile.parent()) { shortestDirective = canonicalFile.lastPathSegment(); isRelative = true; } else { // find the include directive with the shortest length for( const auto& includePath : includepaths ) { QString relative = includePath.relativePath( canonicalFile ); if( relative.startsWith( QLatin1String("./") ) ) relative = relative.mid( 2 ); if( shortestDirective.isEmpty() || relative.length() < shortestDirective.length() ) { shortestDirective = relative; isRelative = includePath == sourceFolder; } } } if( shortestDirective.isEmpty() ) { // Item not found in include path return {}; } const auto range = DocumentRange(IndexedString(source.pathOrUrl()), includeDirectivePosition(source, canonicalFile.lastPathSegment())); if( !range.isValid() ) { clangDebug() << "unable to determine valid position for" << includefile << "in" << source.pathOrUrl(); return {}; } QString directive; if( isRelative ) { directive = QStringLiteral("#include \"%1\"").arg(shortestDirective); } else { directive = QStringLiteral("#include <%1>").arg(shortestDirective); } return ClangFixit{directive + QLatin1Char('\n'), range, i18n("Insert \'%1\'", directive)}; } KDevelop::Path::List includePaths( const KDevelop::Path& file ) { // Find project's custom include paths const auto source = file.toLocalFile(); const auto item = ICore::self()->projectController()->projectModel()->itemForPath( KDevelop::IndexedString( source ) ); return IDefinesAndIncludesManager::manager()->includes(item); } /** * Return a list of header files viable for inclusions. All elements will be unique */ QStringList includeFiles(const QualifiedIdentifier& identifier, const QVector& declarations, const KDevelop::Path& file) { const auto includes = includePaths( file ); if( includes.isEmpty() ) { clangDebug() << "Include path is empty"; return {}; } const auto candidates = UnknownDeclarationProblem::findMatchingIncludeFiles(declarations); if( !candidates.isEmpty() ) { // If we find a candidate from the duchain we don't bother scanning the include paths return candidates; } return scanIncludePaths(identifier, includes); } /** * Construct viable forward declarations for the type name. */ ClangFixits forwardDeclarations(const QVector& matchingDeclarations, const Path& source) { DUChainReadLocker lock; ClangFixits fixits; for (const auto decl : matchingDeclarations) { const auto qid = decl->qualifiedIdentifier(); if (qid.count() > 1) { // TODO: Currently we're not able to determine what is namespaces, class names etc // and makes a suitable forward declaration, so just suggest "vanilla" declarations. continue; } const auto range = forwardDeclarationPosition(qid, source); if (!range.isValid()) { continue; // do not know where to insert } if (const auto classDecl = dynamic_cast(decl)) { const auto name = qid.last().toString(); switch (classDecl->classType()) { case ClassDeclarationData::Class: fixits += { QLatin1String("class ") + name + QLatin1String(";\n"), range, i18n("Forward declare as 'class'") }; break; case ClassDeclarationData::Struct: fixits += { QLatin1String("struct ") + name + QLatin1String(";\n"), range, i18n("Forward declare as 'struct'") }; break; default: break; } } } return fixits; } /** * Search the persistent symbol table for matching declarations for identifiers @p identifiers */ QVector findMatchingDeclarations(const QVector& identifiers) { DUChainReadLocker lock; QVector matchingDeclarations; matchingDeclarations.reserve(identifiers.size()); for (const auto& declaration : identifiers) { clangDebug() << "Considering candidate declaration" << declaration; const IndexedDeclaration* declarations; uint declarationCount; PersistentSymbolTable::self().declarations( declaration , declarationCount, declarations ); for (uint i = 0; i < declarationCount; ++i) { // Skip if the declaration is invalid or if it is an alias declaration - // we want the actual declaration (and its file) if (auto decl = declarations[i].declaration()) { matchingDeclarations << decl; } } } return matchingDeclarations; } ClangFixits fixUnknownDeclaration( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::DocumentRange& docrange ) { ClangFixits fixits; const CursorInRevision cursor{docrange.start().line(), docrange.start().column()}; const auto possibleIdentifiers = findPossibleQualifiedIdentifiers(identifier, file, cursor); const auto matchingDeclarations = findMatchingDeclarations(possibleIdentifiers); if (ClangSettingsManager::self()->assistantsSettings().forwardDeclare) { for (const auto& fixit : forwardDeclarations(matchingDeclarations, file)) { fixits << fixit; if (fixits.size() == maxSuggestions) { return fixits; } } } const auto includefiles = includeFiles(identifier, matchingDeclarations, file); if (includefiles.isEmpty()) { // return early as the computation of the include paths is quite expensive return fixits; } const auto includepaths = includePaths( file ); clangDebug() << "found include paths for" << file << ":" << includepaths; /* create fixits for candidates */ for( const auto& includeFile : includefiles ) { const auto fixit = directiveForFile( includeFile, includepaths, file /* UP */ ); if (!fixit.range.isValid()) { clangDebug() << "unable to create directive for" << includeFile << "in" << file.toLocalFile(); continue; } fixits << fixit; if (fixits.size() == maxSuggestions) { return fixits; } } return fixits; } QString symbolFromDiagnosticSpelling(const QString& str) { /* in all error messages the symbol is in in the first pair of quotes */ const auto split = str.split( QLatin1Char('\'') ); auto symbol = split.value( 1 ); if( str.startsWith( QLatin1String("No member named") ) ) { symbol = split.value( 3 ) + QLatin1String("::") + split.value( 1 ); } return symbol; } } UnknownDeclarationProblem::UnknownDeclarationProblem(CXDiagnostic diagnostic, CXTranslationUnit unit) : ClangProblem(diagnostic, unit) { setSymbol(QualifiedIdentifier(symbolFromDiagnosticSpelling(description()))); } void UnknownDeclarationProblem::setSymbol(const QualifiedIdentifier& identifier) { m_identifier = identifier; } IAssistant::Ptr UnknownDeclarationProblem::solutionAssistant() const { const Path path(finalLocation().document.str()); const auto fixits = allFixits() + fixUnknownDeclaration(m_identifier, path, finalLocation()); return IAssistant::Ptr(new ClangFixitAssistant(fixits)); } diff --git a/plugins/clang/duchain/unsavedfile.cpp b/plugins/clang/duchain/unsavedfile.cpp index 2f3d813fbd..5ae49f20b9 100644 --- a/plugins/clang/duchain/unsavedfile.cpp +++ b/plugins/clang/duchain/unsavedfile.cpp @@ -1,56 +1,55 @@ /* This file is part of KDevelop Copyright 2015 Milian Wolff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "unsavedfile.h" #include #include UnsavedFile::UnsavedFile(const QString& fileName, const QStringList& contents) : m_fileName(fileName) , m_contents(contents) { } CXUnsavedFile UnsavedFile::toClangApi() const { if (m_fileNameUtf8.isEmpty()) { const_cast(this)->convertToUtf8(); } CXUnsavedFile file; file.Contents = m_contentsUtf8.data(); file.Length = m_contentsUtf8.size(); file.Filename = m_fileNameUtf8.data(); return file; } void UnsavedFile::convertToUtf8() { m_fileNameUtf8 = m_fileName.toUtf8(); m_contentsUtf8.clear(); foreach(const QString& line, m_contents) { - m_contentsUtf8 += line.toUtf8(); - m_contentsUtf8 += '\n'; + m_contentsUtf8 += line.toUtf8() + '\n'; } } diff --git a/plugins/clang/util/clangutils.cpp b/plugins/clang/util/clangutils.cpp index 04bb8fd555..fca2e47c5e 100644 --- a/plugins/clang/util/clangutils.cpp +++ b/plugins/clang/util/clangutils.cpp @@ -1,459 +1,460 @@ /* * Copyright 2014 Kevin Funk * * 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 "clangutils.h" #include "../util/clangdebug.h" #include "../util/clangtypes.h" #include "../duchain/cursorkindtraits.h" #include "../duchain/documentfinderhelpers.h" #include #include #include #include #include #include using namespace KDevelop; CXCursor ClangUtils::getCXCursor(int line, int column, const CXTranslationUnit& unit, const CXFile& file) { if (!file) { clangDebug() << "getCXCursor couldn't find file: " << clang_getFileName(file); return clang_getNullCursor(); } CXSourceLocation location = clang_getLocation(unit, file, line + 1, column + 1); if (clang_equalLocations(clang_getNullLocation(), location)) { clangDebug() << "getCXCursor given invalid position " << line << ", " << column << " for file " << clang_getFileName(file); return clang_getNullCursor(); } return clang_getCursor(unit, location); } QVector ClangUtils::unsavedFiles() { QVector ret; foreach(auto document, ICore::self()->documentController()->openDocuments()) { auto textDocument = document->textDocument(); // TODO: Introduce a cache so we don't have to re-read all the open documents // which were not changed since the last run if (!textDocument || !textDocument->url().isLocalFile() || !DocumentFinderHelpers::mimeTypesList().contains(textDocument->mimeType())) { continue; } if (!textDocument->isModified()) { continue; } ret << UnsavedFile(textDocument->url().toLocalFile(), textDocument->textLines(textDocument->documentRange())); } return ret; } KTextEditor::Range ClangUtils::rangeForIncludePathSpec(const QString& line, const KTextEditor::Range& originalRange) { static const QRegularExpression pattern(QStringLiteral("^\\s*(#\\s*include|#\\s*import)")); if (!line.contains(pattern)) { return KTextEditor::Range::invalid(); } KTextEditor::Range range = originalRange; int pos = 0; char term_char = 0; for (; pos < line.size(); ++pos) { if (line[pos] == QLatin1Char('"') || line[pos] == QLatin1Char('<')) { term_char = line[pos] == QLatin1Char('"') ? '"' : '>'; range.setStart({ range.start().line(), ++pos }); break; } } for (; pos < line.size(); ++pos) { if (line[pos] == QLatin1Char('\\')) { ++pos; continue; } else if(line[pos] == QLatin1Char(term_char)) { range.setEnd({ range.start().line(), pos }); break; } } return range; } namespace { struct FunctionInfo { KTextEditor::Range range; QString fileName; CXTranslationUnit unit; QStringList stringParts; }; CXChildVisitResult paramVisitor(CXCursor cursor, CXCursor /*parent*/, CXClientData data) { //Ignore the type of the parameter CXCursorKind kind = clang_getCursorKind(cursor); if (kind == CXCursor_TypeRef || kind == CXCursor_TemplateRef || kind == CXCursor_NamespaceRef) { return CXChildVisit_Continue; } FunctionInfo *info = static_cast(data); ClangRange range(clang_getCursorExtent(cursor)); CXFile file; clang_getFileLocation(clang_getCursorLocation(cursor),&file,nullptr,nullptr,nullptr); if (!file) { clangDebug() << "Couldn't find file associated with default parameter cursor!"; //We keep going, because getting an error because we accidentally duplicated //a default parameter is better than deleting a default parameter } QString fileName = ClangString(clang_getFileName(file)).toString(); //Clang doesn't make a distinction between the default arguments being in //the declaration or definition, and the default arguments don't have lexical //parents. So this range check is the only thing that really works. if ((info->fileName.isEmpty() || fileName == info->fileName) && info->range.contains(range.toRange())) { const ClangTokens tokens(info->unit, range.range()); + info->stringParts.reserve(info->stringParts.size() + tokens.size()); for (CXToken token : tokens) { info->stringParts.append(ClangString(clang_getTokenSpelling(info->unit, token)).toString()); } } return CXChildVisit_Continue; } } QVector ClangUtils::getDefaultArguments(CXCursor cursor, DefaultArgumentsMode mode) { if (!CursorKindTraits::isFunction(clang_getCursorKind(cursor))) { return QVector(); } int numArgs = clang_Cursor_getNumArguments(cursor); QVector arguments(mode == FixedSize ? numArgs : 0); QString fileName; CXFile file; clang_getFileLocation(clang_getCursorLocation(cursor),&file,nullptr,nullptr,nullptr); if (!file) { clangDebug() << "Couldn't find file associated with default parameter cursor!"; //The empty string serves as a wildcard string, because it's better to //duplicate a default parameter than delete one } else { fileName = ClangString(clang_getFileName(file)).toString(); } FunctionInfo info{ClangRange(clang_getCursorExtent(cursor)).toRange(), fileName, clang_Cursor_getTranslationUnit(cursor), QStringList()}; for (int i = 0; i < numArgs; i++) { CXCursor arg = clang_Cursor_getArgument(cursor, i); info.stringParts.clear(); clang_visitChildren(arg, paramVisitor, &info); //Clang includes the equal sign sometimes, but not other times. if (!info.stringParts.isEmpty() && info.stringParts.first() == QLatin1String("=")) { info.stringParts.removeFirst(); } //Clang seems to include the , or ) at the end of the param, so delete that if (!info.stringParts.isEmpty() && (info.stringParts.last() == QLatin1String(",") || info.stringParts.last() == QLatin1String(")"))) { info.stringParts.removeLast(); } const QString result = info.stringParts.join(QString()); if (mode == FixedSize) { arguments.replace(i, result); } else if (!result.isEmpty()) { arguments << result; } } return arguments; } bool ClangUtils::isScopeKind(CXCursorKind kind) { return kind == CXCursor_Namespace || kind == CXCursor_StructDecl || kind == CXCursor_UnionDecl || kind == CXCursor_ClassDecl || kind == CXCursor_ClassTemplate || kind == CXCursor_ClassTemplatePartialSpecialization; } QString ClangUtils::getScope(CXCursor cursor, CXCursor context) { QStringList scope; if (clang_Cursor_isNull(context)) { context = clang_getCursorLexicalParent(cursor); } context = clang_getCanonicalCursor(context); CXCursor search = clang_getCursorSemanticParent(cursor); while (isScopeKind(clang_getCursorKind(search)) && !clang_equalCursors(search, context)) { scope.prepend(ClangString(clang_getCursorDisplayName(search)).toString()); search = clang_getCursorSemanticParent(search); } return scope.join(QStringLiteral("::")); } QString ClangUtils::getCursorSignature(CXCursor cursor, const QString& scope, const QVector& defaultArgs) { CXCursorKind kind = clang_getCursorKind(cursor); //Get the return type QString ret; ret.reserve(128); QTextStream stream(&ret); if (kind != CXCursor_Constructor && kind != CXCursor_Destructor) { stream << ClangString(clang_getTypeSpelling(clang_getCursorResultType(cursor))).toString() << ' '; } //Build the function name, with scope and parameters if (!scope.isEmpty()) { stream << scope << "::"; } QString functionName = ClangString(clang_getCursorSpelling(cursor)).toString(); if (functionName.contains(QLatin1Char('<'))) { stream << functionName.left(functionName.indexOf(QLatin1Char('<'))); } else { stream << functionName; } //Add the parameters and such stream << '('; int numArgs ; QVector args; // SEE https://bugs.kde.org/show_bug.cgi?id=368544 // clang_Cursor_getNumArguments returns -1 for FunctionTemplate // clang checks if cursor's Decl is ObjCMethodDecl or FunctionDecl // CXCursor_FunctionTemplate is neither of them instead it has a FunctionTemplateDecl // HACK Get function template arguments by visiting children if (kind == CXCursor_FunctionTemplate) { clang_visitChildren(cursor, [] (CXCursor cursor, CXCursor /*parent*/, CXClientData data) { if (clang_getCursorKind(cursor) == CXCursor_ParmDecl) { (static_cast*>(data))->push_back(cursor); } return CXChildVisit_Continue; }, &args); numArgs = args.size(); } else { numArgs = clang_Cursor_getNumArguments(cursor); args.reserve(numArgs); for (int i = 0; i < numArgs; i++) { CXCursor arg = clang_Cursor_getArgument(cursor, i); args.push_back(arg); } } for (int i = 0; i < numArgs; i++) { CXCursor arg = args[i]; //Clang formats pointer types as "t *x" and reference types as "t &x", while //KDevelop formats them as "t* x" and "t& x". Make that adjustment. const QString type = ClangString(clang_getTypeSpelling(clang_getCursorType(arg))).toString(); if (type.endsWith(QLatin1String(" *")) || type.endsWith(QLatin1String(" &"))) { stream << type.left(type.length() - 2) << type.at(type.length() - 1); } else { stream << type; } const QString id = ClangString(clang_getCursorDisplayName(arg)).toString(); if (!id.isEmpty()) { stream << ' ' << id; } if (i < defaultArgs.count() && !defaultArgs.at(i).isEmpty()) { stream << " = " << defaultArgs.at(i); } if (i < numArgs - 1) { stream << ", "; } } if (clang_Cursor_isVariadic(cursor)) { if (numArgs > 0) { stream << ", "; } stream << "..."; } stream << ')'; if (clang_CXXMethod_isConst(cursor)) { stream << " const"; } return ret; } QStringList ClangUtils::templateArgumentTypes(CXCursor cursor) { CXType typeList = clang_getCursorType(cursor); int templateArgCount = clang_Type_getNumTemplateArguments(typeList); QStringList types; types.reserve(templateArgCount); for (int i = 0; i < templateArgCount; ++i) { ClangString clangString(clang_getTypeSpelling(clang_Type_getTemplateArgumentAsType(typeList, i))); types.append(clangString.toString()); } return types; } QByteArray ClangUtils::getRawContents(CXTranslationUnit unit, CXSourceRange range) { const auto rangeStart = clang_getRangeStart(range); const auto rangeEnd = clang_getRangeEnd(range); unsigned int start, end; clang_getFileLocation(rangeStart, nullptr, nullptr, nullptr, &start); clang_getFileLocation(rangeEnd, nullptr, nullptr, nullptr, &end); QByteArray result; const ClangTokens tokens(unit, range); for (CXToken token : tokens) { const auto location = ClangLocation(clang_getTokenLocation(unit, token)); unsigned int offset; clang_getFileLocation(location, nullptr, nullptr, nullptr, &offset); if (offset < start) // TODO: Sometimes hit, see bug 357585 return {}; const int fillCharacters = offset - start - result.size(); Q_ASSERT(fillCharacters >= 0); if (fillCharacters < 0) return {}; result.append(QByteArray(fillCharacters, ' ')); const auto spelling = clang_getTokenSpelling(unit, token); result.append(clang_getCString(spelling)); clang_disposeString(spelling); } // Clang always appends the full range of the last token, even if this exceeds the end of the requested range. // Fix this. result.chop(result.size() - (end - start)); return result; } bool ClangUtils::isExplicitlyDefaultedOrDeleted(CXCursor cursor) { if (clang_getCursorAvailability(cursor) == CXAvailability_NotAvailable) { return true; } #if CINDEX_VERSION_MINOR >= 34 if (clang_CXXMethod_isDefaulted(cursor)) { return true; } #else auto declCursor = clang_getCanonicalCursor(cursor); CXTranslationUnit tu = clang_Cursor_getTranslationUnit(declCursor); ClangTokens tokens(tu, clang_getCursorExtent(declCursor)); bool lastTokenWasDeleteOrDefault = false; for (auto it = tokens.rbegin(), end = tokens.rend(); it != end; ++it) { CXToken token = *it; auto kind = clang_getTokenKind(token); switch (kind) { case CXToken_Comment: break; case CXToken_Identifier: case CXToken_Literal: lastTokenWasDeleteOrDefault = false; break; case CXToken_Punctuation: { ClangString spelling(clang_getTokenSpelling(tu, token)); const char* spellingCStr = spelling.c_str(); if (strcmp(spellingCStr, ")") == 0) { // a closing parent means we have reached the end of the function parameter list // therefore this function can't be explicitly deleted/defaulted return false; } else if (strcmp(spellingCStr, "=") == 0) { if (lastTokenWasDeleteOrDefault) { return true; } #if CINDEX_VERSION_MINOR < 31 // HACK: on old clang versions, we don't get the default/delete // so there, assume the function is defaulted or deleted // when the last token is an equal sign if (it == tokens.rbegin()) { return true; } #endif } lastTokenWasDeleteOrDefault = false; break; } case CXToken_Keyword: { ClangString spelling(clang_getTokenSpelling(tu, token)); const char* spellingCStr = spelling.c_str(); if (strcmp(spellingCStr, "default") == 0 #if CINDEX_VERSION_MINOR < 31 || strcmp(spellingCStr, "delete") == 0 #endif ) { lastTokenWasDeleteOrDefault = true; } else { lastTokenWasDeleteOrDefault = false; } break; } } } #endif return false; } KDevelop::ClassFunctionFlags ClangUtils::specialAttributes(CXCursor cursor) { // check for our injected attributes to detect Qt signals and slots // see also the contents of wrappedQtHeaders/QtCore/qobjectdefs.h ClassFunctionFlags flags = {}; if (cursor.kind == CXCursor_CXXMethod) { clang_visitChildren(cursor, [] (CXCursor cursor, CXCursor /*parent*/, CXClientData data) -> CXChildVisitResult { auto& flags = *static_cast(data); switch (cursor.kind) { case CXCursor_AnnotateAttr: { ClangString attribute(clang_getCursorDisplayName(cursor)); if (attribute.c_str() == QByteArrayLiteral("qt_signal")) { flags |= FunctionSignalFlag; } else if (attribute.c_str() == QByteArrayLiteral("qt_slot")) { flags |= FunctionSlotFlag; } break; } case CXCursor_CXXFinalAttr: flags |= FinalFunctionFlag; break; default: break; } return CXChildVisit_Break; }, &flags); } return flags; } diff --git a/plugins/cmake/cmakecodecompletionmodel.cpp b/plugins/cmake/cmakecodecompletionmodel.cpp index d73d9e3737..33aa25e1ee 100644 --- a/plugins/cmake/cmakecodecompletionmodel.cpp +++ b/plugins/cmake/cmakecodecompletionmodel.cpp @@ -1,313 +1,315 @@ /* KDevelop CMake Support * * Copyright 2008 Aleix Pol * * 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 "cmakecodecompletionmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cmakeutils.h" #include "icmakedocumentation.h" using namespace KTextEditor; using namespace KDevelop; QVector CMakeCodeCompletionModel::s_commands; CMakeCodeCompletionModel::CMakeCodeCompletionModel(QObject* parent) : CodeCompletionModel(parent) , m_inside(false) {} bool isFunction(const Declaration* decl) { return decl->abstractType().cast(); } bool isPathChar(const QChar& c) { return c.isLetterOrNumber() || c=='/' || c=='.'; } QString escapePath(QString path) { // see https://cmake.org/Wiki/CMake/Language_Syntax#Escapes static const QString toBeEscaped = QStringLiteral("\"()#$^"); for(const QChar &ch : toBeEscaped) { - path.replace(ch, "\\" + ch); + path.replace(ch, QLatin1Char('\\') + ch); } return path; } void CMakeCodeCompletionModel::completionInvoked(View* view, const Range& range, InvocationType invocationType) { beginResetModel(); if(s_commands.isEmpty()) { ICMakeDocumentation* cmakedoc=CMake::cmakeDocumentation(); if(cmakedoc) s_commands=cmakedoc->names(ICMakeDocumentation::Command); } Q_UNUSED(invocationType); m_declarations.clear(); DUChainReadLocker lock(DUChain::lock()); KTextEditor::Document* d=view->document(); TopDUContext* ctx = DUChain::self()->chainForDocument( d->url() ); QString line=d->line(range.end().line()); // m_inside=line.lastIndexOf('(', range.end().column())>=0; m_inside=line.lastIndexOf('(', range.end().column()-line.size()-1)>=0; for(int l=range.end().line(); l>=0 && !m_inside; --l) { QString cline=d->line(l); QString line=cline.left(cline.indexOf('#')); int close=line.lastIndexOf(')'), open=line.indexOf('('); if(close>=0 && open>=0) { m_inside=open>close; break; } else if(open>=0) { m_inside=true; break; } else if(close>=0) { m_inside=false; break; } } int numRows = 0; if(m_inside) { Cursor start=range.start(); for(; isPathChar(d->characterAt(start)); start-=Cursor(0,1)) {} start+=Cursor(0,1); QString tocomplete=d->text(Range(start, range.end()-Cursor(0,1))); int lastdir=tocomplete.lastIndexOf('/'); QString path = KIO::upUrl(QUrl(d->url())).adjusted(QUrl::StripTrailingSlash).toLocalFile()+'/'; QString basePath; if(lastdir>=0) { basePath=tocomplete.mid(0, lastdir); path+=basePath; } QDir dir(path); QFileInfoList paths=dir.entryInfoList(QStringList() << tocomplete.mid(lastdir+1)+'*', QDir::AllEntries | QDir::NoDotAndDotDot); m_paths.clear(); + m_paths.reserve(paths.size()); foreach(const QFileInfo& f, paths) { QString currentPath = f.fileName(); if(f.isDir()) currentPath+='/'; m_paths += currentPath; } numRows += m_paths.count(); } else numRows += s_commands.count(); if(ctx) { const auto list = ctx->allDeclarations( ctx->transformToLocalRevision(KTextEditor::Cursor(range.start())), ctx ); for (const auto& pair : list) { bool func=isFunction(pair.first); if((func && !m_inside) || (!func && m_inside)) m_declarations.append(pair.first); } numRows+=m_declarations.count(); } setRowCount(numRows); endResetModel(); } CMakeCodeCompletionModel::Type CMakeCodeCompletionModel::indexType(int row) const { if(m_inside) { if(row < m_declarations.count()) { KDevelop::DUChainReadLocker lock; Declaration* dec = m_declarations.at(row).declaration(); if (dec && dec->type()) return Target; else return Variable; } else return Path; } else { if(rowidentifier().toString() : i18n("INVALID"); } } } else if(role==Qt::DisplayRole && index.column()==CodeCompletionModel::Prefix) { switch(type) { case Command: return i18n("Command"); case Variable: return i18n("Variable"); case Macro: return i18n("Macro"); case Path: return i18n("Path"); case Target: return i18n("Target"); } } else if(role==Qt::DecorationRole && index.column()==CodeCompletionModel::Icon) { switch(type) { case Command: return QIcon::fromTheme(QStringLiteral("code-block")); case Variable: return QIcon::fromTheme(QStringLiteral("code-variable")); case Macro: return QIcon::fromTheme(QStringLiteral("code-function")); case Target: return QIcon::fromTheme(QStringLiteral("system-run")); case Path: { QUrl url = QUrl::fromUserInput(m_paths[index.row()-m_declarations.size()]); QString iconName; if (url.isLocalFile()) { // don't read contents even if it is a local file iconName = QMimeDatabase().mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchExtension).iconName(); } else { // remote always only looks at the extension iconName = QMimeDatabase().mimeTypeForUrl(url).iconName(); } return QIcon::fromTheme(iconName); } } } else if(role==Qt::DisplayRole && index.column()==CodeCompletionModel::Arguments) { switch(type) { case Variable: case Command: case Path: case Target: break; case Macro: { DUChainReadLocker lock(DUChain::lock()); int pos=index.row(); FunctionType::Ptr func; if(m_declarations[pos].data()) func = m_declarations[pos].data()->abstractType().cast(); if(!func) return QVariant(); QStringList args; - foreach(const AbstractType::Ptr& t, func->arguments()) - { + const auto arguments = func->arguments(); + args.reserve(arguments.size()); + for (const AbstractType::Ptr& t : arguments) { DelayedType::Ptr delay = t.cast(); args.append(delay ? delay->identifier().toString() : i18n("wrong")); } return QString('('+args.join(QStringLiteral(", "))+')'); } } } return QVariant(); } void CMakeCodeCompletionModel::executeCompletionItem(View* view, const Range& word, const QModelIndex& idx) const { Document* document = view->document(); const int row = idx.row(); switch(indexType(row)) { case Path: { Range r=word; for(QChar c=document->characterAt(r.end()); c.isLetterOrNumber() || c=='.'; c=document->characterAt(r.end())) { r.setEnd(KTextEditor::Cursor(r.end().line(), r.end().column()+1)); } QString path = data(index(row, Name, QModelIndex())).toString(); document->replaceText(r, escapePath(path)); } break; case Macro: case Command: { QString code=data(index(row, Name, QModelIndex())).toString(); if(!document->line(word.start().line()).contains('(')) code.append('('); document->replaceText(word, code); } break; case Variable: { Range r=word, prefix(word.start()-Cursor(0,2), word.start()); QString bef=document->text(prefix); if(r.start().column()>=2 && bef==QLatin1String("${")) r.setStart(KTextEditor::Cursor(r.start().line(), r.start().column()-2)); QString code="${"+data(index(row, Name, QModelIndex())).toString(); if(document->characterAt(word.end())!='}') code+='}'; document->replaceText(r, code); } break; case Target: document->replaceText(word, data(index(row, Name, QModelIndex())).toString()); break; } } diff --git a/plugins/cmake/cmakeedit.cpp b/plugins/cmake/cmakeedit.cpp index 82ad875519..ccaa8762f5 100644 --- a/plugins/cmake/cmakeedit.cpp +++ b/plugins/cmake/cmakeedit.cpp @@ -1,333 +1,337 @@ /* KDevelop CMake Support * * Copyright 2007-2013 Aleix Pol * * 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 "cmakeedit.h" #include #include #include #include #include #include #include #include #include "cmakemodelitems.h" using namespace KDevelop; namespace CMakeEdit { void eatLeadingWhitespace(KTextEditor::Document* doc, KTextEditor::Range& eater, const KTextEditor::Range& bounds) { QString text = doc->text(KTextEditor::Range(bounds.start(), eater.start())); int newStartLine = eater.start().line(), pos = text.length() - 2; //pos = index before eater.start while (pos > 0) { if (text[pos] == '\n') --newStartLine; else if (!text[pos].isSpace()) { ++pos; break; } --pos; } int lastNewLinePos = text.lastIndexOf('\n', pos - 1); int newStartCol = lastNewLinePos == -1 ? eater.start().column() + pos : pos - lastNewLinePos - 1; eater.setStart(KTextEditor::Cursor(newStartLine, newStartCol)); } KTextEditor::Range rangeForText(KTextEditor::Document* doc, const KTextEditor::Range& r, const QString& name) { QString txt=doc->text(r); QRegExp match("([\\s]|^)(\\./)?"+QRegExp::escape(name)); int namepos = match.indexIn(txt); int length = match.cap(0).size(); if(namepos == -1) return KTextEditor::Range::invalid(); //QRegExp doesn't support lookbehind asserts, and \b isn't good enough //so either match "^" or match "\s" and then +1 here if (txt[namepos].isSpace()) { ++namepos; --length; } KTextEditor::Cursor c(r.start()); c.setLine(c.line() + txt.left(namepos).count('\n')); int lastNewLinePos = txt.lastIndexOf('\n', namepos); if (lastNewLinePos < 0) c.setColumn(r.start().column() + namepos); else c.setColumn(namepos - lastNewLinePos - 1); return KTextEditor::Range(c, KTextEditor::Cursor(c.line(), c.column()+length)); } bool followUses(KTextEditor::Document* doc, RangeInRevision r, const QString& name, const QUrl &lists, bool add, const QString& replace) { bool ret=false; KTextEditor::Range rx; if(!add) rx=rangeForText(doc, r.castToSimpleRange(), name); if(!add && rx.isValid()) { if(replace.isEmpty()) { eatLeadingWhitespace(doc, rx, r.castToSimpleRange()); doc->removeText(rx); } else doc->replaceText(rx, replace); ret=true; } else { const IndexedString idxLists(lists); KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); KDevelop::ReferencedTopDUContext topctx=DUChain::self()->chainForDocument(idxLists); QList decls; for(int i=0; iusesCount(); i++) { Use u = topctx->uses()[i]; if(!r.contains(u.m_range)) continue; //We just want the uses in the range, not the whole file Declaration* d=u.usedDeclaration(topctx); if(d && d->topContext()->url()==idxLists) decls += d; } if(add && decls.isEmpty()) { doc->insertText(r.castToSimpleRange().start(), ' '+name); ret=true; } else foreach(Declaration* d, decls) { r.start=d->range().end; for(int lineNum = r.start.line; lineNum <= r.end.line; lineNum++) { int endParenIndex = doc->line(lineNum).indexOf(')'); if(endParenIndex >= 0) { r.end = CursorInRevision(lineNum, endParenIndex); break; } } if(!r.isEmpty()) { ret = ret || followUses(doc, r, name, lists, add, replace); } } } return ret; } QString dotlessRelativeUrl(const QUrl &baseUrl, const QUrl& url) { QString dotlessRelative = QUrl::relativeUrl(baseUrl, url); if (dotlessRelative.startsWith(QLatin1String("./"))) dotlessRelative.remove(0, 2); return dotlessRelative; } QString relativeToLists(const QUrl &listsPath, const QUrl& url) { QUrl listsFolder(listsPath.upUrl()); listsFolder.adjustPath(QUrl::AddTrailingSlash); return dotlessRelativeUrl(listsFolder, url); } QUrl afterMoveUrl(const QUrl &origUrl, const QUrl& movedOrigUrl, const QUrl& movedNewUrl) { QString difference = dotlessRelativeUrl(movedOrigUrl, origUrl); return QUrl(movedNewUrl, difference); } QString itemListspath(const ProjectBaseItem* item) { const DescriptorAttatched *desc = 0; if (item->parent()->target()) desc = dynamic_cast(item->parent()); else if (item->type() == ProjectBaseItem::BuildFolder) desc = dynamic_cast(item); if (!desc) return QString(); return desc->descriptor().filePath; } bool itemAffected(const ProjectBaseItem *item, const QUrl &changeUrl) { QUrl listsPath = itemListspath(item); if (listsPath.isEmpty()) return false; QUrl listsFolder(listsPath); listsFolder = listsFolder.upUrl(); //Who thought it was a good idea to have QUrl::isParentOf return true if the urls are equal? return listsFolder.QUrl::isParentOf(changeUrl); } QList cmakeListedItemsAffectedByUrlChange(const IProject *proj, const QUrl &url, QUrl rootUrl) { if (rootUrl.isEmpty()) rootUrl = url; QList dirtyItems; QList sameUrlItems = proj->itemsForUrl(url); foreach(ProjectBaseItem *sameUrlItem, sameUrlItems) { if (itemAffected(sameUrlItem, rootUrl)) dirtyItems.append(sameUrlItem); - - foreach(ProjectBaseItem* childItem, sameUrlItem->children()) + + const auto childItems = sameUrlItem->children(); + dirtyItems.reserve(dirtyItems.size() + childItems.size()); + for (ProjectBaseItem* childItem : childItems) { dirtyItems.append(cmakeListedItemsAffectedByUrlChange(childItem->project(), childItem->url(), rootUrl)); + } } return dirtyItems; } QList cmakeListedItemsAffectedByItemsChanged(const QList &items) { QList dirtyItems; + dirtyItems.reserve(items.size()); foreach(ProjectBaseItem *item, items) dirtyItems.append(cmakeListedItemsAffectedByUrlChange(item->project(), item->url())); return dirtyItems; } bool changesWidgetRenameFolder(const CMakeFolderItem *folder, const QUrl &newUrl, ApplyChangesWidget *widget) { QString lists = folder->descriptor().filePath; widget->addDocuments(IndexedString(lists)); QString relative(relativeToLists(lists, newUrl)); KTextEditor::Range range = folder->descriptor().argRange().castToSimpleRange(); return widget->document()->replaceText(range, relative); } bool changesWidgetRemoveCMakeFolder(const CMakeFolderItem *folder, ApplyChangesWidget *widget) { widget->addDocuments(IndexedString(folder->descriptor().filePath)); KTextEditor::Range range = folder->descriptor().range().castToSimpleRange(); return widget->document()->removeText(range); } bool changesWidgetAddFolder(const QUrl &folderUrl, const CMakeFolderItem *toFolder, ApplyChangesWidget *widget) { QUrl lists(toFolder->url(), "CMakeLists.txt"); QString relative(relativeToLists(lists, folderUrl)); if (relative.endsWith('/')) relative.chop(1); QString insert = QString("add_subdirectory(%1)").arg(relative); widget->addDocuments(IndexedString(lists)); return widget->document()->insertLine(widget->document()->lines(), insert); } bool changesWidgetMoveTargetFile(const ProjectBaseItem *file, const QUrl &newUrl, ApplyChangesWidget *widget) { const DescriptorAttatched *desc = dynamic_cast(file->parent()); if (!desc || desc->descriptor().arguments.isEmpty()) { return false; } RangeInRevision targetRange(desc->descriptor().arguments.first().range().end, desc->descriptor().argRange().end); QString listsPath = desc->descriptor().filePath; QString newRelative = relativeToLists(listsPath, newUrl); QString oldRelative = relativeToLists(listsPath, file->url()); widget->addDocuments(IndexedString(listsPath)); return followUses(widget->document(), targetRange, oldRelative, listsPath, false, newRelative); } bool changesWidgetAddFileToTarget(const ProjectFileItem *item, const ProjectTargetItem *target, ApplyChangesWidget *widget) { const DescriptorAttatched *desc = dynamic_cast(target); if (!desc || desc->descriptor().arguments.isEmpty()) { return false; } RangeInRevision targetRange(desc->descriptor().arguments.first().range().end, desc->descriptor().range().end); QString lists = desc->descriptor().filePath; QString relative = relativeToLists(lists, item->url()); widget->addDocuments(IndexedString(lists)); return followUses(widget->document(), targetRange, relative, lists, true, QString()); } bool changesWidgetRemoveFileFromTarget(const ProjectBaseItem *item, ApplyChangesWidget *widget) { const DescriptorAttatched *desc = dynamic_cast(item->parent()); if (!desc || desc->descriptor().arguments.isEmpty()) { return false; } RangeInRevision targetRange(desc->descriptor().arguments.first().range().end, desc->descriptor().range().end); QString lists = desc->descriptor().filePath; QString relative = relativeToLists(lists, item->url()); widget->addDocuments(IndexedString(lists)); return followUses(widget->document(), targetRange, relative, lists, false, QString()); } bool changesWidgetRemoveItems(const QSet &items, ApplyChangesWidget *widget) { foreach(ProjectBaseItem *item, items) { CMakeFolderItem *folder = dynamic_cast(item); if (folder && !changesWidgetRemoveCMakeFolder(folder, widget)) return false; else if (item->parent()->target() && !changesWidgetRemoveFileFromTarget(item, widget)) return false; } return true; } bool changesWidgetRemoveFilesFromTargets(const QList &files, ApplyChangesWidget *widget) { foreach(ProjectBaseItem *file, files) { Q_ASSERT(file->parent()->target()); if (!changesWidgetRemoveFileFromTarget(file, widget)) return false; } return true; } bool changesWidgetAddFilesToTarget(const QList &files, const ProjectTargetItem* target, ApplyChangesWidget *widget) { foreach(ProjectFileItem *file, files) { if (!changesWidgetAddFileToTarget(file, target, widget)) return false; } return true; } CMakeFolderItem* nearestCMakeFolder(ProjectBaseItem* item) { while(!dynamic_cast(item) && item) item = item->parent(); return dynamic_cast(item); } } diff --git a/plugins/cmake/cmakeextraargumentshistory.cpp b/plugins/cmake/cmakeextraargumentshistory.cpp index fada39d4ec..14f5e87b15 100644 --- a/plugins/cmake/cmakeextraargumentshistory.cpp +++ b/plugins/cmake/cmakeextraargumentshistory.cpp @@ -1,70 +1,70 @@ /* KDevelop CMake Support * * Copyright 2016 René J.V. Bertin * * 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 #include #include "cmakeextraargumentshistory.h" namespace { const int maxExtraArgumentsInHistory = 15; } CMakeExtraArgumentsHistory::CMakeExtraArgumentsHistory(KComboBox* widget) : m_arguments(widget) { if (m_arguments) { KConfigGroup config = KSharedConfig::openConfig()->group("CMakeBuildDirChooser"); - QStringList lastExtraArguments = config.readEntry("LastExtraArguments", QStringList());; + QStringList lastExtraArguments = config.readEntry("LastExtraArguments", QStringList()); m_arguments->addItem(QString()); m_arguments->addItems(lastExtraArguments); m_arguments->setInsertPolicy(QComboBox::InsertAtTop); KCompletion *comp = m_arguments->completionObject(); KComboBox::connect(m_arguments, static_cast(&KComboBox::returnPressed), comp, static_cast(&KCompletion::addItem)); comp->insertItems(lastExtraArguments); } else { qFatal("CMakeExtraArgumentsHistory initialised with invalid widget"); } } CMakeExtraArgumentsHistory::~CMakeExtraArgumentsHistory() { KConfigGroup config = KSharedConfig::openConfig()->group("CMakeBuildDirChooser"); config.writeEntry("LastExtraArguments", list()); config.sync(); } QStringList CMakeExtraArgumentsHistory::list() const { QStringList list; if (!m_arguments->currentText().isEmpty()) { list << m_arguments->currentText(); } for (int i = 0; i < qMin(maxExtraArgumentsInHistory, m_arguments->count()); ++i) { if (!m_arguments->itemText(i).isEmpty() && (m_arguments->currentText() != m_arguments->itemText(i))) { list << m_arguments->itemText(i); } } return list; } diff --git a/plugins/cmake/cmakeserverimportjob.cpp b/plugins/cmake/cmakeserverimportjob.cpp index 1198634664..0337c2777b 100644 --- a/plugins/cmake/cmakeserverimportjob.cpp +++ b/plugins/cmake/cmakeserverimportjob.cpp @@ -1,217 +1,218 @@ /* KDevelop CMake Support * * Copyright 2017 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) 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 "cmakeserverimportjob.h" #include "cmakeutils.h" #include "cmakeserver.h" #include #include #include #include #include #include #include #include #include #include "debug.h" static QString unescape(const QStringRef& input) { QString output; output.reserve(input.length()); bool isEscaped = false; for (auto it = input.data(), end = it + input.length(); it != end; ++it) { QChar c = *it; if (!isEscaped && c == '\\') { isEscaped = true; } else { output.append(c); isEscaped = false; } } return output; } static QHash processDefines(const QString &compileFlags, const QJsonArray &defines) { QHash ret; const auto& defineRx = MakeFileResolver::defineRegularExpression(); auto it = defineRx.globalMatch(compileFlags); while (it.hasNext()) { const auto match = it.next(); QString value; if (match.lastCapturedIndex() > 1) { value = unescape(match.capturedRef(match.lastCapturedIndex())); } ret[match.captured(1)] = value; } for (const QJsonValue& defineValue: defines) { const QString define = defineValue.toString(); const int eqIdx = define.indexOf(QLatin1Char('=')); if (eqIdx<0) { ret[define] = QString(); } else { ret[define.left(eqIdx)] = define.mid(eqIdx+1); } } return ret; } CMakeTarget::Type typeToEnum(const QJsonObject& target) { static const QHash s_types = { {QStringLiteral("EXECUTABLE"), CMakeTarget::Executable}, {QStringLiteral("STATIC_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("MODULE_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("SHARED_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("OBJECT_LIBRARY"), CMakeTarget::Library}, {QStringLiteral("INTERFACE_LIBRARY"), CMakeTarget::Library} }; const auto value = target.value(QLatin1String("type")).toString(); return s_types.value(value, CMakeTarget::Custom); } void CMakeServerImportJob::processCodeModel(const QJsonObject &response, CMakeProjectData &data) { const auto configs = response.value(QStringLiteral("configurations")).toArray(); qCDebug(CMAKE) << "process response" << response; data.targets.clear(); data.compilationData.files.clear(); const auto rt = KDevelop::ICore::self()->runtimeController()->currentRuntime(); for (const auto &config: configs) { const auto projects = config.toObject().value(QStringLiteral("projects")).toArray(); for (const auto &project: projects) { const auto targets = project.toObject().value(QStringLiteral("targets")).toArray(); for (const auto &targetObject: targets) { const auto target = targetObject.toObject(); const KDevelop::Path targetDir = rt->pathInHost(KDevelop::Path(target.value(QStringLiteral("sourceDirectory")).toString())); KDevelop::Path::List targetSources; const auto fileGroups = target.value(QStringLiteral("fileGroups")).toArray(); for (const auto &fileGroupValue: fileGroups) { const auto fileGroup = fileGroupValue.toObject(); CMakeFile file; file.includes = kTransform(fileGroup.value(QStringLiteral("includePath")).toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toObject().value(QStringLiteral("path")).toString()); }); file.compileFlags = fileGroup.value(QStringLiteral("compileFlags")).toString(); file.defines = processDefines(file.compileFlags, fileGroup.value(QStringLiteral("defines")).toArray()); const auto sourcesArray = fileGroup.value(QStringLiteral("sources")).toArray(); const KDevelop::Path::List sources = kTransform(sourcesArray, [targetDir](const QJsonValue& val) { return KDevelop::Path(targetDir, val.toString()); }); + targetSources.reserve(targetSources.size() + sources.size()); for (const auto& source: sources) { // NOTE: we use the canonical file path to prevent issues with symlinks in the path // leading to lookup failures const auto localFile = rt->pathInHost(source); const auto canonicalFile = QFileInfo(source.toLocalFile()).canonicalFilePath(); const auto sourcePath = localFile.toLocalFile() == canonicalFile ? localFile : KDevelop::Path(canonicalFile); data.compilationData.files[sourcePath] = file; targetSources << sourcePath; } qCDebug(CMAKE) << "registering..." << sources << file; } CMakeTarget cmakeTarget{ typeToEnum(target), target.value(QStringLiteral("name")).toString(), kTransform(target[QLatin1String("artifacts")].toArray(), [](const QJsonValue& val) { return KDevelop::Path(val.toString()); }), targetSources, }; // ensure we don't add the same target multiple times, for different projects // cf.: https://bugs.kde.org/show_bug.cgi?id=387095 auto& dirTargets = data.targets[targetDir]; if (dirTargets.contains(cmakeTarget)) continue; dirTargets += cmakeTarget; qCDebug(CMAKE) << "adding target" << cmakeTarget.name << "with sources" << cmakeTarget.sources; } } } } CMakeServerImportJob::CMakeServerImportJob(KDevelop::IProject* project, CMakeServer* server, QObject* parent) : KJob(parent) , m_server(server) , m_project(project) { connect(m_server.data(), &CMakeServer::disconnected, this, [this]() { setError(UnexpectedDisconnect); emitResult(); }); } void CMakeServerImportJob::start() { if (m_server->isServerAvailable()) doStart(); else connect(m_server.data(), &CMakeServer::connected, this, &CMakeServerImportJob::doStart); } void CMakeServerImportJob::doStart() { connect(m_server.data(), &CMakeServer::response, this, &CMakeServerImportJob::processResponse); m_server->handshake(m_project->path(), CMake::currentBuildDir(m_project)); } void CMakeServerImportJob::processResponse(const QJsonObject& response) { const auto responseType = response.value(QStringLiteral("type")); if (responseType == QLatin1String("reply")) { const auto inReplyTo = response.value(QStringLiteral("inReplyTo")); qCDebug(CMAKE) << "replying..." << inReplyTo; if (inReplyTo == QLatin1String("handshake")) { m_server->configure({}); } else if (inReplyTo == QLatin1String("configure")) { m_server->compute(); } else if (inReplyTo == QLatin1String("compute")) { m_server->codemodel(); } else if(inReplyTo == QLatin1String("codemodel")) { processCodeModel(response, m_data); m_data.m_testSuites = CMake::importTestSuites(CMake::currentBuildDir(m_project)); m_data.m_server = m_server; emitResult(); } else { qCDebug(CMAKE) << "unhandled reply" << response; } } else if(responseType == QLatin1String("error")) { setError(ErrorResponse); setErrorText(response.value(QStringLiteral("errorMessage")).toString()); qCWarning(CMAKE) << "error!!" << response; emitResult(); } else if (responseType == QLatin1String("progress")) { int progress = response.value(QStringLiteral("progressCurrent")).toInt(); int total = response.value(QStringLiteral("progressMaximum")).toInt(); if (progress >= 0 && total > 0) { setPercent(100.0 * progress / total); } } else if (responseType == QLatin1String("message") || responseType == QLatin1String("hello")) { // Known, but not used for anything currently. } else { qCDebug(CMAKE) << "unhandled message" << response; } } diff --git a/plugins/cmake/cmakeutils.cpp b/plugins/cmake/cmakeutils.cpp index 4435c2a498..e6964e2063 100644 --- a/plugins/cmake/cmakeutils.cpp +++ b/plugins/cmake/cmakeutils.cpp @@ -1,731 +1,732 @@ /* KDevelop CMake Support * * Copyright 2009 Andreas Pakulat * * 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 "cmakeutils.h" #include "cmakeprojectdata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "icmakedocumentation.h" #include "cmakebuilddirchooser.h" #include "settings/cmakecachemodel.h" #include "debug.h" #include "cmakebuilderconfig.h" #include #include "parser/cmakelistsparser.h" using namespace KDevelop; namespace Config { namespace Old { static const QString currentBuildDirKey = QStringLiteral("CurrentBuildDir"); static const QString oldcmakeExecutableKey = QStringLiteral("CMake Binary"); // Todo: Remove at some point static const QString currentBuildTypeKey = QStringLiteral("CurrentBuildType"); static const QString currentInstallDirKey = QStringLiteral("CurrentInstallDir"); static const QString currentEnvironmentKey = QStringLiteral("CurrentEnvironment"); static const QString currentExtraArgumentsKey = QStringLiteral("Extra Arguments"); static const QString currentCMakeExecutableKey = QStringLiteral("Current CMake Binary"); static const QString projectRootRelativeKey = QStringLiteral("ProjectRootRelative"); static const QString projectBuildDirs = QStringLiteral("BuildDirs"); } static const QString buildDirIndexKey_ = QStringLiteral("Current Build Directory Index"); static const QString buildDirOverrideIndexKey = QStringLiteral("Temporary Build Directory Index"); static const QString buildDirCountKey = QStringLiteral("Build Directory Count"); //the used builddir will change for every runtime static QString buildDirIndexKey() { const QString currentRuntime = ICore::self()->runtimeController()->currentRuntime()->name(); return buildDirIndexKey_ + '-' + currentRuntime; } namespace Specific { static const QString buildDirPathKey = QStringLiteral("Build Directory Path"); // TODO: migrate to more generic & consistent key term "CMake Executable" // Support the old "CMake Binary" key too for backwards compatibility during // a reasonable transition period. Both keys are saved at least until 5.2.0 // is released. Import support for the old key will need to remain for a // considably longer period, ideally. static const QString cmakeBinaryKey = QStringLiteral("CMake Binary"); static const QString cmakeExecutableKey = QStringLiteral("CMake Executable"); static const QString cmakeBuildTypeKey = QStringLiteral("Build Type"); static const QString cmakeInstallDirKey = QStringLiteral("Install Directory"); static const QString cmakeEnvironmentKey = QStringLiteral("Environment Profile"); static const QString cmakeArgumentsKey = QStringLiteral("Extra Arguments"); static const QString buildDirRuntime = QStringLiteral("Runtime"); } static const QString groupNameBuildDir = QStringLiteral("CMake Build Directory %1"); static const QString groupName = QStringLiteral("CMake"); } // namespace Config namespace { KConfigGroup baseGroup( KDevelop::IProject* project ) { if (!project) return KConfigGroup(); return project->projectConfiguration()->group( Config::groupName ); } KConfigGroup buildDirGroup( KDevelop::IProject* project, int buildDirIndex ) { return baseGroup(project).group( Config::groupNameBuildDir.arg(buildDirIndex) ); } bool buildDirGroupExists( KDevelop::IProject* project, int buildDirIndex ) { return baseGroup(project).hasGroup( Config::groupNameBuildDir.arg(buildDirIndex) ); } QString readBuildDirParameter( KDevelop::IProject* project, const QString& key, const QString& aDefault, int buildDirectory ) { const int buildDirIndex = buildDirectory<0 ? CMake::currentBuildDirIndex(project) : buildDirectory; if (buildDirIndex >= 0) return buildDirGroup( project, buildDirIndex ).readEntry( key, aDefault ); else return aDefault; } void writeBuildDirParameter( KDevelop::IProject* project, const QString& key, const QString& value ) { int buildDirIndex = CMake::currentBuildDirIndex(project); if (buildDirIndex >= 0) { KConfigGroup buildDirGrp = buildDirGroup( project, buildDirIndex ); buildDirGrp.writeEntry( key, value ); } else { qCWarning(CMAKE) << "cannot write key" << key << "(" << value << ")" << "when no builddir is set!"; } } void writeProjectBaseParameter( KDevelop::IProject* project, const QString& key, const QString& value ) { KConfigGroup baseGrp = baseGroup(project); baseGrp.writeEntry( key, value ); } void setBuildDirRuntime( KDevelop::IProject* project, const QString& name) { writeBuildDirParameter(project, Config::Specific::buildDirRuntime, name); } QString buildDirRuntime( KDevelop::IProject* project, int builddir) { return readBuildDirParameter(project, Config::Specific::buildDirRuntime, QString(), builddir); } } // namespace namespace CMake { KDevelop::Path::List resolveSystemDirs(KDevelop::IProject* project, const QStringList& dirs) { const KDevelop::Path buildDir(CMake::currentBuildDir(project)); const KDevelop::Path installDir(CMake::currentInstallDir(project)); KDevelop::Path::List newList; newList.reserve(dirs.size()); foreach(const QString& s, dirs) { KDevelop::Path dir; if(s.startsWith(QLatin1String("#[bin_dir]"))) { dir = KDevelop::Path(buildDir, s); } else if(s.startsWith(QLatin1String("#[install_dir]"))) { dir = KDevelop::Path(installDir, s); } else { dir = KDevelop::Path(s); } // qCDebug(CMAKE) << "resolved" << s << "to" << d; if (!newList.contains(dir)) { newList.append(dir); } } return newList; } ///NOTE: when you change this, update @c defaultConfigure in cmakemanagertest.cpp bool checkForNeedingConfigure( KDevelop::IProject* project ) { const QString currentRuntime = ICore::self()->runtimeController()->currentRuntime()->name(); const KDevelop::Path builddir = currentBuildDir(project); const bool isValid = (buildDirRuntime(project, -1) == currentRuntime || buildDirRuntime(project, -1).isEmpty()) && builddir.isValid(); if( !isValid ) { CMakeBuildDirChooser bd; bd.setProject( project ); const auto builddirs = CMake::allBuildDirs(project); bd.setAlreadyUsed( builddirs ); bd.setShowAvailableBuildDirs(!builddirs.isEmpty()); bd.setCMakeExecutable(currentCMakeExecutable(project)); if( !bd.exec() ) { return false; } if (bd.reuseBuilddir()) { CMake::setCurrentBuildDirIndex( project, bd.alreadyUsedIndex() ); } else { QString newbuilddir = bd.buildFolder().toLocalFile(); int addedBuildDirIndex = buildDirCount( project ); // old count is the new index // Initialize the kconfig items with the values from the dialog, this ensures the settings // end up in the config file once the changes are saved qCDebug(CMAKE) << "adding to cmake config: new builddir index" << addedBuildDirIndex; qCDebug(CMAKE) << "adding to cmake config: builddir path " << bd.buildFolder(); qCDebug(CMAKE) << "adding to cmake config: installdir " << bd.installPrefix(); qCDebug(CMAKE) << "adding to cmake config: extra args" << bd.extraArguments(); qCDebug(CMAKE) << "adding to cmake config: build type " << bd.buildType(); qCDebug(CMAKE) << "adding to cmake config: cmake executable " << bd.cmakeExecutable(); qCDebug(CMAKE) << "adding to cmake config: environment "; CMake::setBuildDirCount( project, addedBuildDirIndex + 1 ); CMake::setCurrentBuildDirIndex( project, addedBuildDirIndex ); CMake::setCurrentBuildDir( project, bd.buildFolder() ); CMake::setCurrentInstallDir( project, bd.installPrefix() ); CMake::setCurrentExtraArguments( project, bd.extraArguments() ); CMake::setCurrentBuildType( project, bd.buildType() ); CMake::setCurrentCMakeExecutable(project, bd.cmakeExecutable()); CMake::setCurrentEnvironment( project, QString() ); } setBuildDirRuntime( project, currentRuntime ); return true; } else if( !QFile::exists( KDevelop::Path(builddir, QStringLiteral("CMakeCache.txt")).toLocalFile() ) || //TODO: maybe we could use the builder for that? !(QFile::exists( KDevelop::Path(builddir, QStringLiteral("Makefile")).toLocalFile() ) || QFile::exists( KDevelop::Path(builddir, QStringLiteral("build.ninja")).toLocalFile() ) ) ) { // User entered information already, but cmake hasn't actually been run yet. setBuildDirRuntime( project, currentRuntime ); return true; } setBuildDirRuntime( project, currentRuntime ); return false; } QHash enumerateTargets(const KDevelop::Path& targetsFilePath, const QString& sourceDir, const KDevelop::Path &buildDir) { const QString buildPath = buildDir.toLocalFile(); QHash targets; QFile targetsFile(targetsFilePath.toLocalFile()); if (!targetsFile.open(QIODevice::ReadOnly)) { qCDebug(CMAKE) << "Couldn't find the Targets file in" << targetsFile.fileName(); } QTextStream targetsFileStream(&targetsFile); const QRegularExpression rx(QStringLiteral("^(.*)/CMakeFiles/(.*).dir$")); while (!targetsFileStream.atEnd()) { const QString line = targetsFileStream.readLine(); auto match = rx.match(line); if (!match.isValid()) qCDebug(CMAKE) << "invalid match for" << line; const QString sourcePath = match.captured(1).replace(buildPath, sourceDir); targets[KDevelop::Path(sourcePath)].append(match.captured(2)); } return targets; } KDevelop::Path projectRoot(KDevelop::IProject* project) { if (!project) { return {}; } return project->path().cd(CMake::projectRootRelative(project)); } KDevelop::Path currentBuildDir( KDevelop::IProject* project, int builddir ) { return KDevelop::Path(readBuildDirParameter( project, Config::Specific::buildDirPathKey, QString(), builddir )); } KDevelop::Path commandsFile(KDevelop::IProject* project) { auto currentBuildDir = CMake::currentBuildDir(project); if (currentBuildDir.isEmpty()) { return {}; } return KDevelop::Path(currentBuildDir, QStringLiteral("compile_commands.json")); } KDevelop::Path targetDirectoriesFile(KDevelop::IProject* project) { auto currentBuildDir = CMake::currentBuildDir(project); if (currentBuildDir.isEmpty()) { return {}; } return KDevelop::Path(currentBuildDir, QStringLiteral("CMakeFiles/TargetDirectories.txt")); } QString currentBuildType( KDevelop::IProject* project, int builddir ) { return readBuildDirParameter( project, Config::Specific::cmakeBuildTypeKey, QStringLiteral("Release"), builddir ); } QString findExecutable() { auto cmake = QStandardPaths::findExecutable(QStringLiteral("cmake")); #ifdef Q_OS_WIN if (cmake.isEmpty()) cmake = QStandardPaths::findExecutable("cmake",{ "C:\\Program Files (x86)\\CMake\\bin", "C:\\Program Files\\CMake\\bin", "C:\\Program Files (x86)\\CMake 2.8\\bin", "C:\\Program Files\\CMake 2.8\\bin"}); #endif return cmake; } KDevelop::Path currentCMakeExecutable(KDevelop::IProject* project, int builddir) { auto defaultCMakeExecutable = CMakeBuilderSettings::self()->cmakeExecutable().toLocalFile(); if (!QFileInfo::exists(ICore::self()->runtimeController()->currentRuntime()->pathInHost(KDevelop::Path(defaultCMakeExecutable)).toLocalFile())) defaultCMakeExecutable = CMake::findExecutable(); if (project) { // check for "CMake Executable" but for now also "CMake Binary", falling back to the default. auto projectCMakeExecutable = readBuildDirParameter( project, Config::Specific::cmakeExecutableKey, readBuildDirParameter( project, Config::Specific::cmakeBinaryKey, defaultCMakeExecutable, builddir), builddir ); if (projectCMakeExecutable != defaultCMakeExecutable) { QFileInfo info(projectCMakeExecutable); if (!info.isExecutable()) { projectCMakeExecutable = defaultCMakeExecutable; } } return KDevelop::Path(projectCMakeExecutable); } return KDevelop::Path(defaultCMakeExecutable); } KDevelop::Path currentInstallDir( KDevelop::IProject* project, int builddir ) { const QString defaultInstallDir = #ifdef Q_OS_WIN QStringLiteral("C:\\Program Files"); #else QStringLiteral("/usr/local"); #endif return KDevelop::Path(readBuildDirParameter( project, Config::Specific::cmakeInstallDirKey, defaultInstallDir, builddir )); } QString projectRootRelative( KDevelop::IProject* project ) { return baseGroup(project).readEntry( Config::Old::projectRootRelativeKey, "." ); } bool hasProjectRootRelative(KDevelop::IProject* project) { return baseGroup(project).hasKey( Config::Old::projectRootRelativeKey ); } QString currentExtraArguments( KDevelop::IProject* project, int builddir ) { return readBuildDirParameter( project, Config::Specific::cmakeArgumentsKey, QString(), builddir ); } void setCurrentInstallDir( KDevelop::IProject* project, const KDevelop::Path& path ) { writeBuildDirParameter( project, Config::Specific::cmakeInstallDirKey, path.toLocalFile() ); } void setCurrentBuildType( KDevelop::IProject* project, const QString& type ) { writeBuildDirParameter( project, Config::Specific::cmakeBuildTypeKey, type ); } void setCurrentCMakeExecutable(KDevelop::IProject* project, const KDevelop::Path& path) { // maintain compatibility with older versions for now writeBuildDirParameter(project, Config::Specific::cmakeBinaryKey, path.toLocalFile()); writeBuildDirParameter(project, Config::Specific::cmakeExecutableKey, path.toLocalFile()); } void setCurrentBuildDir( KDevelop::IProject* project, const KDevelop::Path& path ) { writeBuildDirParameter( project, Config::Specific::buildDirPathKey, path.toLocalFile() ); } void setProjectRootRelative( KDevelop::IProject* project, const QString& relative) { writeProjectBaseParameter( project, Config::Old::projectRootRelativeKey, relative ); } void setCurrentExtraArguments( KDevelop::IProject* project, const QString& string) { writeBuildDirParameter( project, Config::Specific::cmakeArgumentsKey, string ); } QString currentEnvironment(KDevelop::IProject* project, int builddir) { return readBuildDirParameter( project, Config::Specific::cmakeEnvironmentKey, QString(), builddir ); } int currentBuildDirIndex( KDevelop::IProject* project ) { KConfigGroup baseGrp = baseGroup(project); if ( baseGrp.hasKey( Config::buildDirOverrideIndexKey ) ) return baseGrp.readEntry( Config::buildDirOverrideIndexKey, -1 ); else if (baseGrp.hasKey(Config::buildDirIndexKey())) return baseGrp.readEntry( Config::buildDirIndexKey(), -1 ); else return baseGrp.readEntry( Config::buildDirIndexKey_, -1 ); // backwards compatibility } void setCurrentBuildDirIndex( KDevelop::IProject* project, int buildDirIndex ) { writeProjectBaseParameter( project, Config::buildDirIndexKey(), QString::number (buildDirIndex) ); } void setCurrentEnvironment( KDevelop::IProject* project, const QString& environment ) { writeBuildDirParameter( project, Config::Specific::cmakeEnvironmentKey, environment ); } void initBuildDirConfig( KDevelop::IProject* project ) { int buildDirIndex = currentBuildDirIndex( project ); if (buildDirCount(project) <= buildDirIndex ) setBuildDirCount( project, buildDirIndex + 1 ); } int buildDirCount( KDevelop::IProject* project ) { return baseGroup(project).readEntry( Config::buildDirCountKey, 0 ); } void setBuildDirCount( KDevelop::IProject* project, int count ) { writeProjectBaseParameter( project, Config::buildDirCountKey, QString::number(count) ); } void removeBuildDirConfig( KDevelop::IProject* project ) { int buildDirIndex = currentBuildDirIndex( project ); if ( !buildDirGroupExists( project, buildDirIndex ) ) { qCWarning(CMAKE) << "build directory config" << buildDirIndex << "to be removed but does not exist"; return; } int bdCount = buildDirCount(project); setBuildDirCount( project, bdCount - 1 ); removeOverrideBuildDirIndex( project ); setCurrentBuildDirIndex( project, -1 ); // move (rename) the upper config groups to keep the numbering // if there's nothing to move, just delete the group physically if (buildDirIndex + 1 == bdCount) buildDirGroup( project, buildDirIndex ).deleteGroup(); else for (int i = buildDirIndex + 1; i < bdCount; ++i) { KConfigGroup src = buildDirGroup( project, i ); KConfigGroup dest = buildDirGroup( project, i - 1 ); dest.deleteGroup(); src.copyTo(&dest); src.deleteGroup(); } } QHash readCacheValues(const KDevelop::Path& cmakeCachePath, QSet variables) { QHash ret; QFile file(cmakeCachePath.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qCWarning(CMAKE) << "couldn't open CMakeCache.txt" << cmakeCachePath; return ret; } QTextStream in(&file); while (!in.atEnd() && !variables.isEmpty()) { QString line = in.readLine().trimmed(); if(!line.isEmpty() && line[0].isLetter()) { CacheLine c; c.readLine(line); if(!c.isCorrect()) continue; if (variables.remove(c.name())) { ret[c.name()] = c.value(); } } } return ret; } void updateConfig( KDevelop::IProject* project, int buildDirIndex) { if (buildDirIndex < 0) return; KConfigGroup buildDirGrp = buildDirGroup( project, buildDirIndex ); const KDevelop::Path builddir(buildDirGrp.readEntry( Config::Specific::buildDirPathKey, QString() )); const KDevelop::Path cacheFilePath( builddir, QStringLiteral("CMakeCache.txt")); const QMap keys = { { QStringLiteral("CMAKE_COMMAND"), Config::Specific::cmakeExecutableKey }, { QStringLiteral("CMAKE_INSTALL_PREFIX"), Config::Specific::cmakeInstallDirKey }, { QStringLiteral("CMAKE_BUILD_TYPE"), Config::Specific::cmakeBuildTypeKey } }; const QHash cacheValues = readCacheValues(cacheFilePath, keys.keys().toSet()); for(auto it = cacheValues.constBegin(), itEnd = cacheValues.constEnd(); it!=itEnd; ++it) { const QString key = keys.value(it.key()); Q_ASSERT(!key.isEmpty()); // Use cache only when the config value is not set. Without this check we will always // overwrite values provided by the user in config dialog. if (buildDirGrp.readEntry(key).isEmpty() && !it.value().isEmpty()) { buildDirGrp.writeEntry( key, it.value() ); } } } void attemptMigrate( KDevelop::IProject* project ) { if ( !baseGroup(project).hasKey( Config::Old::projectBuildDirs ) ) { qCDebug(CMAKE) << "CMake settings migration: already done, exiting"; return; } KConfigGroup baseGrp = baseGroup(project); KDevelop::Path buildDir( baseGrp.readEntry( Config::Old::currentBuildDirKey, QString() ) ); int buildDirIndex = -1; const QStringList existingBuildDirs = baseGrp.readEntry( Config::Old::projectBuildDirs, QStringList() ); { // also, find current build directory in this list (we need an index, not path) QString currentBuildDirCanonicalPath = QDir( buildDir.toLocalFile() ).canonicalPath(); for( int i = 0; i < existingBuildDirs.count(); ++i ) { const QString& nextBuildDir = existingBuildDirs.at(i); if( QDir(nextBuildDir).canonicalPath() == currentBuildDirCanonicalPath ) { buildDirIndex = i; } } } int buildDirsCount = existingBuildDirs.count(); qCDebug(CMAKE) << "CMake settings migration: existing build directories" << existingBuildDirs; qCDebug(CMAKE) << "CMake settings migration: build directory count" << buildDirsCount; qCDebug(CMAKE) << "CMake settings migration: current build directory" << buildDir << "(index" << buildDirIndex << ")"; baseGrp.writeEntry( Config::buildDirCountKey, buildDirsCount ); baseGrp.writeEntry( Config::buildDirIndexKey(), buildDirIndex ); for (int i = 0; i < buildDirsCount; ++i) { qCDebug(CMAKE) << "CMake settings migration: writing group" << i << ": path" << existingBuildDirs.at(i); KConfigGroup buildDirGrp = buildDirGroup( project, i ); buildDirGrp.writeEntry( Config::Specific::buildDirPathKey, existingBuildDirs.at(i) ); } baseGrp.deleteEntry( Config::Old::currentBuildDirKey ); baseGrp.deleteEntry( Config::Old::currentCMakeExecutableKey ); baseGrp.deleteEntry( Config::Old::currentBuildTypeKey ); baseGrp.deleteEntry( Config::Old::currentInstallDirKey ); baseGrp.deleteEntry( Config::Old::currentEnvironmentKey ); baseGrp.deleteEntry( Config::Old::currentExtraArgumentsKey ); baseGrp.deleteEntry( Config::Old::projectBuildDirs ); } void setOverrideBuildDirIndex( KDevelop::IProject* project, int overrideBuildDirIndex ) { writeProjectBaseParameter( project, Config::buildDirOverrideIndexKey, QString::number(overrideBuildDirIndex) ); } void removeOverrideBuildDirIndex( KDevelop::IProject* project, bool writeToMainIndex ) { KConfigGroup baseGrp = baseGroup(project); if( !baseGrp.hasKey(Config::buildDirOverrideIndexKey) ) return; if( writeToMainIndex ) baseGrp.writeEntry( Config::buildDirIndexKey(), baseGrp.readEntry(Config::buildDirOverrideIndexKey) ); baseGrp.deleteEntry(Config::buildDirOverrideIndexKey); } ICMakeDocumentation* cmakeDocumentation() { return KDevelop::ICore::self()->pluginController()->extensionForPlugin(QStringLiteral("org.kdevelop.ICMakeDocumentation")); } QStringList allBuildDirs(KDevelop::IProject* project) { QStringList result; int bdCount = buildDirCount(project); + result.reserve(bdCount); for (int i = 0; i < bdCount; ++i) result += buildDirGroup( project, i ).readEntry( Config::Specific::buildDirPathKey ); return result; } QString executeProcess(const QString& execName, const QStringList& args) { Q_ASSERT(!execName.isEmpty()); qCDebug(CMAKE) << "Executing:" << execName << "::" << args; QProcess p; QTemporaryDir tmp(QStringLiteral("kdevcmakemanager")); p.setWorkingDirectory( tmp.path() ); p.start(execName, args, QIODevice::ReadOnly); if(!p.waitForFinished()) { qCDebug(CMAKE) << "failed to execute:" << execName << args << p.exitStatus() << p.readAllStandardError(); } QByteArray b = p.readAllStandardOutput(); QString t; t.prepend(b.trimmed()); return t; } QStringList supportedGenerators() { QStringList generatorNames; bool hasNinja = ICore::self() && ICore::self()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IProjectBuilder"), QStringLiteral("KDevNinjaBuilder")); if (hasNinja) generatorNames << QStringLiteral("Ninja"); #ifdef Q_OS_WIN // Visual Studio solution is the standard generator under windows, but we don't want to use // the VS IDE, so we need nmake makefiles generatorNames << QStringLiteral("NMake Makefiles") << QStringLiteral("MinGW Makefiles"); #endif generatorNames << QStringLiteral("Unix Makefiles"); return generatorNames; } QString defaultGenerator() { const QStringList generatorNames = supportedGenerators(); QString defGen = generatorNames.value(CMakeBuilderSettings::self()->generator()); if (defGen.isEmpty()) { qCWarning(CMAKE) << "Couldn't find builder with index " << CMakeBuilderSettings::self()->generator() << ", defaulting to 0"; CMakeBuilderSettings::self()->setGenerator(0); defGen = generatorNames.at(0); } return defGen; } QVector importTestSuites(const Path &buildDir) { const auto contents = CMakeListsParser::readCMakeFile(buildDir.toLocalFile() + "/CTestTestfile.cmake"); QVector tests; for (const auto& entry: contents) { if (entry.name == QLatin1String("add_test")) { auto args = entry.arguments; Test test; test.name = args.takeFirst().value; test.executable = args.takeFirst().value; test.arguments = kTransform(args, [](const CMakeFunctionArgument& arg) { return arg.value; }); tests += test; } else if (entry.name == QLatin1String("subdirs")) { tests += importTestSuites(Path(buildDir, entry.arguments.first().value)); } else if (entry.name == QLatin1String("set_tests_properties")) { if(entry.arguments.count() < 4 || entry.arguments.count() % 2) { qCWarning(CMAKE) << "found set_tests_properties() with unexpected number of arguments:" << entry.arguments.count(); continue; } if (tests.isEmpty() || entry.arguments.first().value != tests.last().name) { qCWarning(CMAKE) << "found set_tests_properties(" << entry.arguments.first().value << " ...), but expected test " << tests.last().name; continue; } if (entry.arguments[1].value != QLatin1String("PROPERTIES")) { qCWarning(CMAKE) << "found set_tests_properties(" << entry.arguments.first().value << entry.arguments.at(1).value << "...), but expected PROPERTIES as second argument"; continue; } Test &test = tests.last(); for (int i = 2; i < entry.arguments.count(); i += 2) test.properties[entry.arguments[i].value] = entry.arguments[i + 1].value; } } return tests; } } diff --git a/plugins/cmake/parser/cmakelistsparser.cpp b/plugins/cmake/parser/cmakelistsparser.cpp index 20f14710c2..ae3f653b06 100644 --- a/plugins/cmake/parser/cmakelistsparser.cpp +++ b/plugins/cmake/parser/cmakelistsparser.cpp @@ -1,237 +1,239 @@ /* KDevelop CMake Support * * Copyright 2006 Matt Rogers * Copyright 2008 Aleix Pol * * 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 "cmakelistsparser.h" #include #include #include QMap whatToScape() { //Only add those where we're not scaping the next character QMap ret; ret['n']='\n'; ret['r']='\r'; ret['t']='\t'; return ret; } const QMap CMakeFunctionArgument::scapings=whatToScape(); static const QChar scapingChar='\\'; QString CMakeFunctionArgument::unescapeValue(const QString& value) { int firstScape=value.indexOf(scapingChar); if (firstScape<0) { return value; } QString newValue; int last=0; QMap::const_iterator itEnd = scapings.constEnd(); for(int i=firstScape; i=0; i=value.indexOf(scapingChar, i+2)) { newValue+=value.midRef(last, i-last); const QChar current=value[i+1]; QMap::const_iterator it = scapings.constFind(current); if(it!=itEnd) newValue += *it; else newValue += current; last=i+2; } newValue+=value.midRef(last, value.size()); // qCDebug(CMAKE) << "escapiiiiiiiiing" << value << newValue; return newValue; } void CMakeFunctionDesc::addArguments( const QStringList& args, bool addEvenIfEmpty ) { if(addEvenIfEmpty && args.isEmpty()) arguments += CMakeFunctionArgument(); - else foreach( const QString& arg, args ) - { - CMakeFunctionArgument cmakeArg( arg ); - arguments.append( cmakeArg ); + else { + arguments.reserve(arguments.size() + args.size()); + for (const auto& arg : args) { + CMakeFunctionArgument cmakeArg(arg); + arguments.append(cmakeArg); + } } } QString CMakeFunctionDesc::writeBack() const { - QString output=name+"( "; - foreach(const CMakeFunctionArgument& arg, arguments) - { - QString o = arg.value; - if(arg.quoted) - o='"'+o+'"'; - output += o+' '; + QStringList args; + args.reserve(arguments.size()); + for (const auto& arg : arguments) { + if (arg.quoted) { + args.append(QLatin1Char('"') + arg.value + QLatin1Char('"')); + } else { + args.append(arg.value); + } } - output += ')'; - return output; + return name + QLatin1String("( ") + args.join(QLatin1Char(' ')) + QLatin1String(" )"); } namespace CMakeListsParser { static bool readCMakeFunction( cmListFileLexer* lexer, CMakeFunctionDesc& func); CMakeFileContent readCMakeFile(const QString & _fileName) { cmListFileLexer* lexer = cmListFileLexer_New(); if ( !lexer ) return CMakeFileContent(); if ( !cmListFileLexer_SetFileName( lexer, qPrintable( _fileName ), nullptr ) ) { qCDebug(CMAKE) << "cmake read error. could not read " << _fileName; cmListFileLexer_Delete(lexer); return CMakeFileContent(); } CMakeFileContent ret; QString fileName = QDir::cleanPath(_fileName); bool readError = false, haveNewline = true; cmListFileLexer_Token* token; while(!readError && (token = cmListFileLexer_Scan(lexer))) { readError=false; if(token->type == cmListFileLexer_Token_Newline) { readError=false; haveNewline = true; } else if(token->type == cmListFileLexer_Token_Identifier) { if(haveNewline) { haveNewline = false; CMakeFunctionDesc function; function.name = QString::fromLocal8Bit(token->text, token->length).toLower(); function.filePath = fileName; function.line = token->line; function.column = token->column; readError = !readCMakeFunction( lexer, function); ret.append(function); if(readError) { qCDebug(CMAKE) << "Error while parsing:" << function.name << "at" << function.line; } } } } cmListFileLexer_Delete(lexer); return ret; } } bool CMakeListsParser::readCMakeFunction(cmListFileLexer *lexer, CMakeFunctionDesc &func) { // Command name has already been parsed. Read the left paren. cmListFileLexer_Token* token; if(!(token = cmListFileLexer_Scan(lexer))) { return false; } if(token->type != cmListFileLexer_Token_ParenLeft) { return false; } // Arguments. int parenthesis=1; while((token = cmListFileLexer_Scan(lexer))) { switch(token->type) { case cmListFileLexer_Token_ParenRight: parenthesis--; if(parenthesis==0) { func.endLine=token->line; func.endColumn=token->column; return true; } else if(parenthesis<0) return false; else func.arguments << CMakeFunctionArgument( QString::fromLocal8Bit(token->text, token->length), false, token->line, token->column ); break; case cmListFileLexer_Token_ParenLeft: parenthesis++; func.arguments << CMakeFunctionArgument( QString::fromLocal8Bit(token->text, token->length), false, token->line, token->column ); break; case cmListFileLexer_Token_Identifier: case cmListFileLexer_Token_ArgumentUnquoted: func.arguments << CMakeFunctionArgument( QString::fromLocal8Bit(token->text, token->length), false, token->line, token->column ); break; case cmListFileLexer_Token_ArgumentQuoted: func.arguments << CMakeFunctionArgument( QString::fromLocal8Bit(token->text, token->length), true, token->line, token->column+1 ); break; case cmListFileLexer_Token_Space: case cmListFileLexer_Token_Newline: break; default: return false; } } return false; } CMakeFunctionDesc::CMakeFunctionDesc(const QString& name, const QStringList& args) : name(name) { addArguments(args); } CMakeFunctionDesc::CMakeFunctionDesc() {} bool CMakeFunctionDesc::operator==(const CMakeFunctionDesc & other) const { if(other.arguments.count()!=arguments.count() || name!=other.name) return false; auto it=arguments.constBegin(); auto itOther=other.arguments.constBegin(); for(;it!=arguments.constEnd(); ++it, ++itOther) { if(*it!=*itOther) return false; } return true; } CMakeFunctionArgument::CMakeFunctionArgument(const QString& v, bool q, quint32 l, quint32 c) : value(unescapeValue(v)), quoted(q), line(l), column(c) { } CMakeFunctionArgument::CMakeFunctionArgument(const QString& v) : value(v) { } diff --git a/plugins/cmake/settings/cmakecachemodel.cpp b/plugins/cmake/settings/cmakecachemodel.cpp index 1031250258..c790c183f1 100644 --- a/plugins/cmake/settings/cmakecachemodel.cpp +++ b/plugins/cmake/settings/cmakecachemodel.cpp @@ -1,224 +1,226 @@ /* KDevelop CMake Support * * Copyright 2007 Aleix Pol * * 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 "cmakecachemodel.h" #include #include #include "cmakecachereader.h" #include //4 columns: name, type, value, comment //name:type=value - comment CMakeCacheModel::CMakeCacheModel(QObject *parent, const KDevelop::Path &path) : QStandardItemModel(parent), m_filePath(path) { read(); } void CMakeCacheModel::reset() { emit beginResetModel(); clear(); m_internal.clear(); m_modifiedRows.clear(); read(); emit endResetModel(); } void CMakeCacheModel::read() { // Set headers - QStringList labels; - labels.append(i18n("Name")); - labels.append(i18n("Type")); - labels.append(i18n("Value")); - labels.append(i18n("Comment")); - labels.append(i18n("Advanced")); - labels.append(i18n("Strings")); + const QStringList labels{ + i18n("Name"), + i18n("Type"), + i18n("Value"), + i18n("Comment"), + i18n("Advanced"), + i18n("Strings"), + }; setHorizontalHeaderLabels(labels); QFile file(m_filePath.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qCDebug(CMAKE) << "error. Could not find the file"; return; } int currentIdx=0; QStringList currentComment; QTextStream in(&file); QHash variablePos; while (!in.atEnd()) { QString line = in.readLine().trimmed(); if(line.startsWith(QLatin1String("//"))) currentComment += line.right(line.count()-2); else if(!line.isEmpty() && !line.startsWith('#')) //it is a variable { CacheLine c; c.readLine(line); if(c.isCorrect()) { QString name=c.name(), flag=c.flag(); QString type=c.type(); QString value=c.value(); - QList lineItems; - lineItems.append(new QStandardItem(name)); - lineItems.append(new QStandardItem(type)); - lineItems.append(new QStandardItem(value)); - lineItems.append(new QStandardItem(currentComment.join(QLatin1Char('\n')))); + const QList lineItems{ + new QStandardItem(name), + new QStandardItem(type), + new QStandardItem(value), + new QStandardItem(currentComment.join(QLatin1Char('\n'))), + }; if(flag==QLatin1String("INTERNAL")) { m_internal.insert(name); } else if(flag==QLatin1String("ADVANCED") || flag==QLatin1String("STRINGS")) { if(variablePos.contains(name)) { int pos=variablePos[name]; // if the flag is not ADVANCED, it's STRINGS. // The latter is stored in column 5 int column = flag==QLatin1String("ADVANCED") ? 4 : 5; QStandardItem *p = item(pos, column); if(!p) { p=new QStandardItem(value); setItem(pos, column, p); } else { p->setText(value); } } else { qCDebug(CMAKE) << "Flag for an unknown variable"; } } if(!flag.isEmpty()) { lineItems[0]->setText(lineItems[0]->text()+'-'+flag); } insertRow(currentIdx, lineItems); if (!variablePos.contains(name)) { variablePos[name]=currentIdx; } currentIdx++; currentComment.clear(); } } else if(line.startsWith('#') && line.contains(QLatin1String("INTERNAL"))) { m_internalBegin=currentIdx; // qCDebug(CMAKE) << "Comment: " << line << " -.- " << currentIdx; } else if(!line.startsWith('#') && !line.isEmpty()) { qCDebug(CMAKE) << "unrecognized cache line: " << line; } } } bool CMakeCacheModel::setData(const QModelIndex& index, const QVariant& value, int role) { bool ret = QStandardItemModel::setData(index, value, role); if (ret) { m_modifiedRows.insert(index.row()); } return ret; } QVariantMap CMakeCacheModel::changedValues() const { QVariantMap ret; for(int i=0; itext()+':'+type->text(), valu->text()); } return ret; } QString CMakeCacheModel::value(const QString & varName) const { for(int i=0; itext()==varName) { QStandardItem* valu = item(i, 2); return valu->text(); } } return QString(); } bool CMakeCacheModel::isAdvanced(int i) const { QStandardItem *p=item(i, 4); bool isAdv= (p!=nullptr) || i>m_internalBegin; if(!isAdv) { p=item(i, 1); isAdv = p->text()==QLatin1String("INTERNAL") || p->text()==QLatin1String("STATIC"); } return isAdv || m_internal.contains(item(i,0)->text()); } bool CMakeCacheModel::isInternal(int i) const { bool isInt= i>m_internalBegin; return isInt; } QList< QModelIndex > CMakeCacheModel::persistentIndices() const { QList< QModelIndex > ret; for(int i=0; itext()==QLatin1String("BOOL")) { QStandardItem* valu = item(i, 2); ret.append(valu->index()); } } return ret; } KDevelop::Path CMakeCacheModel::filePath() const { return m_filePath; } diff --git a/plugins/cmakebuilder/cmakebuilder.cpp b/plugins/cmakebuilder/cmakebuilder.cpp index 34de9cf7fb..73057af578 100644 --- a/plugins/cmakebuilder/cmakebuilder.cpp +++ b/plugins/cmakebuilder/cmakebuilder.cpp @@ -1,268 +1,268 @@ /* KDevelop CMake Support * * Copyright 2006-2007 Andreas Pakulat * * 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 "cmakebuilder.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "cmakejob.h" #include "prunejob.h" #include "cmakebuilderpreferences.h" #include "cmakeutils.h" #include K_PLUGIN_FACTORY_WITH_JSON(CMakeBuilderFactory, "kdevcmakebuilder.json", registerPlugin(); ) class ErrorJob : public KJob { public: ErrorJob(QObject* parent, const QString& error) : KJob(parent) , m_error(error) {} void start() override { setError(!m_error.isEmpty()); setErrorText(m_error); emitResult(); } private: QString m_error; }; CMakeBuilder::CMakeBuilder(QObject *parent, const QVariantList &) : KDevelop::IPlugin(QStringLiteral("kdevcmakebuilder"), parent) { - addBuilder(QStringLiteral("Makefile"), QStringList(QStringLiteral("Unix Makefiles")) - << QStringLiteral("NMake Makefiles") - << QStringLiteral("MinGW Makefiles"), + addBuilder(QStringLiteral("Makefile"), QStringList{QStringLiteral("Unix Makefiles"), + QStringLiteral("NMake Makefiles"), + QStringLiteral("MinGW Makefiles")}, core()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IMakeBuilder"))); addBuilder(QStringLiteral("build.ninja"), QStringList(QStringLiteral("Ninja")), core()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IProjectBuilder"), QStringLiteral("KDevNinjaBuilder"))); } CMakeBuilder::~CMakeBuilder() { } void CMakeBuilder::addBuilder(const QString& neededfile, const QStringList& generators, KDevelop::IPlugin* i) { if( i ) { IProjectBuilder* b = i->extension(); if( b ) { m_builders[neededfile] = b; foreach(const QString& gen, generators) { m_buildersForGenerator[gen] = b; } // can't use new signal/slot syntax here, IProjectBuilder is not a QObject connect(i, SIGNAL(built(KDevelop::ProjectBaseItem*)), this, SIGNAL(built(KDevelop::ProjectBaseItem*))); connect(i, SIGNAL(failed(KDevelop::ProjectBaseItem*)), this, SIGNAL(failed(KDevelop::ProjectBaseItem*))); connect(i, SIGNAL(cleaned(KDevelop::ProjectBaseItem*)), this, SIGNAL(cleaned(KDevelop::ProjectBaseItem*))); connect(i, SIGNAL(installed(KDevelop::ProjectBaseItem*)), this, SIGNAL(installed(KDevelop::ProjectBaseItem*))); qCDebug(KDEV_CMAKEBUILDER) << "Added builder " << i->metaObject()->className() << "for" << neededfile; } else qCWarning(KDEV_CMAKEBUILDER) << "Couldn't add" << i->metaObject()->className(); } } KJob* CMakeBuilder::build(KDevelop::ProjectBaseItem *dom) { KDevelop::IProject* p = dom->project(); IProjectBuilder* builder = builderForProject(p); if( builder ) { bool valid; KJob* configure = checkConfigureJob(dom->project(), valid); KJob* build = nullptr; if(dom->file()) { IMakeBuilder* makeBuilder = dynamic_cast(builder); if (!makeBuilder) { return new ErrorJob(this, i18n("Could not find the make builder. Check your installation")); } KDevelop::ProjectFileItem* file = dom->file(); int lastDot = file->text().lastIndexOf('.'); QString target = file->text().mid(0, lastDot)+".o"; build = makeBuilder->executeMakeTarget(dom->parent(), target); qCDebug(KDEV_CMAKEBUILDER) << "create build job for target" << build << dom << target; } qCDebug(KDEV_CMAKEBUILDER) << "Building with" << builder; if (!build) { build = builder->build(dom); } if( configure ) { qCDebug(KDEV_CMAKEBUILDER) << "creating composite job"; KDevelop::BuilderJob* builderJob = new KDevelop::BuilderJob; builderJob->addCustomJob( KDevelop::BuilderJob::Configure, configure, dom ); builderJob->addCustomJob( KDevelop::BuilderJob::Build, build, dom ); builderJob->updateJobName(); build = builderJob; } return build; } return new ErrorJob(this, i18n("Could not find a builder for %1", p->name())); } KJob* CMakeBuilder::clean(KDevelop::ProjectBaseItem *dom) { IProjectBuilder* builder = builderForProject(dom->project()); if( builder ) { bool valid; KJob* configure = checkConfigureJob(dom->project(), valid); KDevelop::ProjectBaseItem* item = dom; if(dom->file()) //It doesn't work to compile a file item=(KDevelop::ProjectBaseItem*) dom->parent(); qCDebug(KDEV_CMAKEBUILDER) << "Cleaning with" << builder; KJob* clean = builder->clean(item); if( configure ) { KDevelop::BuilderJob* builderJob = new KDevelop::BuilderJob; builderJob->addCustomJob( KDevelop::BuilderJob::Configure, configure, item ); builderJob->addCustomJob( KDevelop::BuilderJob::Clean, clean, item ); builderJob->updateJobName(); clean = builderJob; } return clean; } return new ErrorJob(this, i18n("Could not find a builder for %1", dom->project()->name())); } KJob* CMakeBuilder::install(KDevelop::ProjectBaseItem *dom, const QUrl &installPrefix) { IProjectBuilder* builder = builderForProject(dom->project()); if( builder ) { bool valid; KJob* configure = checkConfigureJob(dom->project(), valid); KDevelop::ProjectBaseItem* item = dom; if(dom->file()) item=(KDevelop::ProjectBaseItem*) dom->parent(); qCDebug(KDEV_CMAKEBUILDER) << "Installing with" << builder; KJob* install = builder->install(item, installPrefix); if( configure ) { KDevelop::BuilderJob* builderJob = new KDevelop::BuilderJob; builderJob->addCustomJob( KDevelop::BuilderJob::Configure, configure, item ); builderJob->addCustomJob( KDevelop::BuilderJob::Install, install, item ); builderJob->updateJobName(); install = builderJob; } return install; } return new ErrorJob(this, i18n("Could not find a builder for %1", dom->project()->name())); } KJob* CMakeBuilder::checkConfigureJob(KDevelop::IProject* project, bool& valid) { valid = false; KJob* configure = nullptr; if( CMake::checkForNeedingConfigure(project) ) { configure = this->configure(project); } else if( CMake::currentBuildDir(project).isEmpty() ) { return new ErrorJob(this, i18n("No Build Directory configured, cannot install")); } valid = true; return configure; } KJob* CMakeBuilder::configure( KDevelop::IProject* project ) { if( CMake::currentBuildDir( project ).isEmpty() ) { return new ErrorJob(this, i18n("No Build Directory configured, cannot configure")); } CMakeJob* job = new CMakeJob(this); job->setProject(project); connect(job, &KJob::result, this, [this, project] { emit configured(project); }); return job; } KJob* CMakeBuilder::prune( KDevelop::IProject* project ) { return new PruneJob(project); } KDevelop::IProjectBuilder* CMakeBuilder::builderForProject(KDevelop::IProject* p) const { QString builddir = CMake::currentBuildDir( p ).toLocalFile(); QMap::const_iterator it = m_builders.constBegin(), itEnd = m_builders.constEnd(); for(; it!=itEnd; ++it) { if(QFile::exists(builddir+'/'+it.key())) return it.value(); } //It means that it still has to be generated, so use the builder for //the generator we use return m_buildersForGenerator[CMake::defaultGenerator()]; } QList< KDevelop::IProjectBuilder* > CMakeBuilder::additionalBuilderPlugins( KDevelop::IProject* project ) const { IProjectBuilder* b = builderForProject( project ); QList< KDevelop::IProjectBuilder* > ret; if(b) ret << b; return ret; } int CMakeBuilder::configPages() const { return 1; } KDevelop::ConfigPage* CMakeBuilder::configPage(int number, QWidget* parent) { if (number == 0) { return new CMakeBuilderPreferences(this, parent); } return nullptr; } #include "cmakebuilder.moc" diff --git a/plugins/cmakebuilder/prunejob.cpp b/plugins/cmakebuilder/prunejob.cpp index 7891155e6f..748207f9ee 100644 --- a/plugins/cmakebuilder/prunejob.cpp +++ b/plugins/cmakebuilder/prunejob.cpp @@ -1,88 +1,89 @@ /* KDevelop CMake Support * * Copyright 2013 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) 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 "prunejob.h" #include #include #include #include #include #include using namespace KDevelop; PruneJob::PruneJob(KDevelop::IProject* project) : OutputJob(project, Verbose) , m_project(project) , m_job(nullptr) { setCapabilities( Killable ); setToolTitle( i18n("CMake") ); setStandardToolView( KDevelop::IOutputView::BuildView ); setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll ); } void PruneJob::start() { OutputModel* output = new OutputModel(this); setModel(output); startOutput(); Path builddir = CMake::currentBuildDir( m_project ); if( builddir.isEmpty() ) { output->appendLine(i18n("No Build Directory configured, cannot clear the build directory")); emitResult(); return; } else if (!builddir.isLocalFile() || QDir(builddir.toLocalFile()).exists(QStringLiteral("CMakeLists.txt"))) { output->appendLine(i18n("Wrong build directory, cannot clear the build directory")); emitResult(); return; } QDir d( builddir.toLocalFile() ); QList urls; - foreach( const QString& entry, d.entryList( QDir::NoDotAndDotDot | QDir::AllEntries ) ) - { + const auto entries = d.entryList( QDir::NoDotAndDotDot | QDir::AllEntries ); + urls.reserve(entries.size()); + for (const auto& entry : entries) { urls << Path(builddir, entry).toUrl(); } output->appendLine(i18n("%1> rm -rf %2", m_project->path().pathOrUrl(), builddir.toLocalFile())); m_job = KIO::del( urls ); m_job->start(); connect(m_job, &KJob::finished, this, &PruneJob::jobFinished); } bool PruneJob::doKill() { return m_job->kill(); } void PruneJob::jobFinished(KJob* job) { OutputModel* output = qobject_cast(model()); if(job->error()==0) output->appendLine(i18n("** Prune successful **")); else output->appendLine(i18n("** Prune failed: %1 **", job->errorString())); emitResult(); m_job = nullptr; } diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp index 8487fbbd82..fa005ad8f0 100644 --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -1,1495 +1,1497 @@ /* * 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 #include #include #include #include #include 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* contextAt(const QUrl& url, KTextEditor::Cursor cursor) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (!topContext) return nullptr; 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())).declaration); return DeclarationPointer(decl); } } class ContextBrowserViewFactory: public KDevelop::IToolViewFactory { public: explicit ContextBrowserViewFactory(ContextBrowserPlugin *plugin): m_plugin(plugin) {} QWidget* create(QWidget *parent = nullptr) 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 ) { m_browseManager = new BrowseManager(this); KXMLGUIClient* ret = KDevelop::IPlugin::createGUIForMainWindow(window); connect(ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); m_previousButton = new QToolButton(); m_previousButton->setToolTip(i18n("Go back in context history")); m_previousButton->setAutoRaise(true); 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); 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->setAutoRaise(true); 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); 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); connect(m_browseManager, &BrowseManager::invokeAction, this, &ContextBrowserPlugin::invokeAction); 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* sourceBrowseMode = actions.addAction(QStringLiteral("source_browse_mode")); sourceBrowseMode->setText( i18n("Source &Browse Mode") ); sourceBrowseMode->setIcon( QIcon::fromTheme(QStringLiteral("arrow-up")) ); sourceBrowseMode->setCheckable(true); connect(sourceBrowseMode, &QAction::triggered, m_browseManager, &BrowseManager::setBrowsing); 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) { qRegisterMetaType("KDevelop::IndexedDeclaration"); 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( ColorCache::self(), &ColorCache::colorsGotChanged, this, &ContextBrowserPlugin::colorSetupChanged ); 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, QWidget* parent) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension(context, parent); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker lock(DUChain::lock()); if(!codeContext->declaration().data()) return menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::NavigationGroup, 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()) { auto 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) { qCWarning(PLUGIN_CONTEXTBROWSER) << "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::invokeAction(int index) { if (!m_currentNavigationWidget) return; auto navigationWidget = qobject_cast(m_currentNavigationWidget); if (!navigationWidget) return; // TODO: Add API in AbstractNavigation{Widget,Context}? QMetaObject::invokeMethod(navigationWidget->context().data(), "executeAction", Q_ARG(int, index)); } void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if(!m_currentToolTip) { showToolTip(view, view->cursorPosition()); } } void ContextBrowserPlugin::hideToolTip() { if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; m_currentToolTipProblems.clear(); m_currentToolTipDeclaration = {}; } } static QVector findProblemsUnderCursor(TopDUContext* topContext, KTextEditor::Cursor position) { QVector problems; const auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); for (const auto& modelData : modelsData) { foreach (const auto& problem, modelData.model->problems(topContext->url())) { DocumentRange problemRange = problem->finalLocation(); if (problemRange.contains(position) || (problemRange.isEmpty() && problemRange.boundaryAtCursor(position))) problems += problem; } } return problems; } static QVector findProblemsCloseToCursor(TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::View* view) { QVector allProblems; const auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); for (const auto& modelData : modelsData) { - foreach (const auto& problem, modelData.model->problems(topContext->url())) { + const auto problems = modelData.model->problems(topContext->url()); + allProblems.reserve(allProblems.size() + problems.size()); + for (const auto& problem : problems) { allProblems += problem; } } if (allProblems.isEmpty()) return allProblems; std::sort(allProblems.begin(), allProblems.end(), [position](const KDevelop::IProblem::Ptr a, const KDevelop::IProblem::Ptr b) { const auto aRange = a->finalLocation(); const auto bRange = b->finalLocation(); const auto aLineDistance = qMin(qAbs(aRange.start().line() - position.line()), qAbs(aRange.end().line() - position.line())); const auto bLineDistance = qMin(qAbs(bRange.start().line() - position.line()), qAbs(bRange.end().line() - position.line())); if (aLineDistance != bLineDistance) { return aLineDistance < bLineDistance; } if (aRange.start().line() == bRange.start().line()) { return qAbs(aRange.start().column() - position.column()) < qAbs(bRange.start().column() - position.column()); } return qAbs(aRange.end().column() - position.column()) < qAbs(bRange.end().column() - position.column()); }); QVector closestProblems; // Show problems, located on the same line foreach (auto problem, allProblems) { auto r = problem->finalLocation(); if (r.onSingleLine() && r.start().line() == position.line()) closestProblems += problem; else break; } // If not, only show it in case there's only whitespace // between the current cursor position and the problem line if (closestProblems.isEmpty()) { foreach (auto problem, allProblems) { auto r = problem->finalLocation(); KTextEditor::Range dist; KTextEditor::Cursor bound(r.start().line(), 0); if (position < r.start()) dist = KTextEditor::Range(position, bound); else { bound.setLine(r.end().line() + 1); dist = KTextEditor::Range(bound, position); } if (view->document()->text(dist).trimmed().isEmpty()) closestProblems += problem; else break; } } return closestProblems; } QWidget* ContextBrowserPlugin::navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position) { QUrl viewUrl = view->document()->url(); auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); DUChainReadLocker lock(DUChain::lock()); foreach (const auto language, languages) { auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, KTextEditor::Cursor(position)); auto navigationWidget = qobject_cast(widget); if(navigationWidget) return navigationWidget; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (topContext) { // first pass: find problems under the cursor const auto problems = findProblemsUnderCursor(topContext, position); if (!problems.isEmpty()) { if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; auto context = new ProblemNavigationContext(problems); context->setTopContext(TopDUContextPointer(topContext)); widget->setContext(NavigationContextPointer(context)); return widget; } } auto declUnderCursor = DUChainUtils::itemUnderCursor(viewUrl, position).declaration; Declaration* decl = DUChainUtils::declarationForDefinition(declUnderCursor); 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 nullptr; m_currentToolTipDeclaration = IndexedDeclaration(decl); return decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); } if (topContext) { // second pass: find closest problem to the cursor const auto problems = findProblemsCloseToCursor(topContext, position, view); if (!problems.isEmpty()) { if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; // since the problem is not under cursor: show location widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problems, ProblemNavigationContext::ShowLocation))); return widget; } } return nullptr; } 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 auto navigationWidget = navigationWidgetForPosition(view, position); 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 = nullptr; m_currentNavigationWidget = nullptr; } KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, view->mapToGlobal(view->cursorToCoordinate(position)) + QPoint(20, 40), navigationWidget); KTextEditor::Range itemRange; { DUChainReadLocker lock; auto viewUrl = view->document()->url(); itemRange = DUChainUtils::itemUnderCursor(viewUrl, position).range; } tooltip->setHandleRect(KTextEditorHelpers::getItemBoundingRect(view, itemRange)); tooltip->resize( navigationWidget->sizeHint() + QSize(10, 10) ); QObject::connect( view, &KTextEditor::View::verticalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip ); QObject::connect( view, &KTextEditor::View::horizontalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip ); 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 ContextBrowserPlugin::highlightedUseAttribute(KTextEditor::View* view) const { if( !m_highlightAttribute ) { m_highlightAttribute = Attribute::Ptr( new Attribute() ); m_highlightAttribute->setDefaultStyle(KTextEditor::dsNormal); m_highlightAttribute->setForeground(m_highlightAttribute->selectedForeground()); m_highlightAttribute->setBackgroundFillWhitespace(true); auto iface = qobject_cast(view); auto background = iface->configValue(QStringLiteral("search-highlight-color")).value(); m_highlightAttribute->setBackground(background); } return m_highlightAttribute; } void ContextBrowserPlugin::colorSetupChanged() { m_highlightAttribute = Attribute::Ptr(); } Attribute::Ptr ContextBrowserPlugin::highlightedSpecialObjectAttribute(KTextEditor::View* view) const { return highlightedUseAttribute(view); } 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(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); // Highlight uses { const auto currentRevisionUses = decl->usesCurrentRevision(); for (auto fileIt = currentRevisionUses.constBegin(); fileIt != currentRevisionUses.constEnd(); ++fileIt) { for (auto useIt = (*fileIt).constBegin(); useIt != (*fileIt).constEnd(); ++useIt) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(*useIt, fileIt.key())); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } } if( FunctionDefinition* def = FunctionDefinition::definition(decl) ) { highlights.highlights << def->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } Declaration* ContextBrowserPlugin::findDeclaration(View* view, const KTextEditor::Cursor& position, bool mouseHighlight) { Q_UNUSED(mouseHighlight); Declaration* foundDeclaration = nullptr; 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).declaration ); 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 nullptr; } 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) : nullptr; if(specialRange.isValid()) { // Highlight a special language object if(allowHighlight) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(specialRange, IndexedString(url))); highlights.highlights.back()->setAttribute(highlightedSpecialObjectAttribute(view)); 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 = nullptr; 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(&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 = nullptr; //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).declaration; if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { Declaration* target = nullptr; 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) { QVector 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 QVector 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(); QVector 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 = contextAt(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 = contextAt(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; indices.reserve(m_history.size()-m_nextHistoryIndex); for(int a = m_nextHistoryIndex; a < m_history.size(); ++a) { indices << a; } fillHistoryPopup(m_nextMenu, indices); } void ContextBrowserPlugin::previousMenuAboutToShow() { QList indices; indices.reserve(m_nextHistoryIndex-1); 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) { qCWarning(PLUGIN_CONTEXTBROWSER) << "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/custom-buildsystem/custombuildsystemconfigwidget.cpp b/plugins/custom-buildsystem/custombuildsystemconfigwidget.cpp index fbcac33baa..1db08391ac 100644 --- a/plugins/custom-buildsystem/custombuildsystemconfigwidget.cpp +++ b/plugins/custom-buildsystem/custombuildsystemconfigwidget.cpp @@ -1,214 +1,217 @@ /************************************************************************ * KDevelop4 Custom Buildsystem Support * * * * Copyright 2010 Andreas Pakulat * * * * 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 or version 3 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, see . * ************************************************************************/ #include "custombuildsystemconfigwidget.h" #include #include #include "ui_custombuildsystemconfigwidget.h" #include "configconstants.h" namespace { QString generateToolGroupName( CustomBuildSystemTool::ActionType type ) { static const int toolTypeCount = CustomBuildSystemTool::Undefined + 1; // Static Qt-based objects are discouraged (MSVC-incompatible), so use raw strings static const char* const toolTypes[] = { "Build", "Configure", "Install", "Clean", "Prune", "Undefined" }; Q_ASSERT( type >= 0 && type < toolTypeCount ); Q_UNUSED( toolTypeCount ); return ConfigConstants::toolGroupPrefix() + toolTypes[type]; } } CustomBuildSystemConfigWidget::CustomBuildSystemConfigWidget( QWidget* parent ) : QWidget( parent ), ui( new Ui::CustomBuildSystemConfigWidget ) { ui->setupUi( this ); // hack taken from kurlrequester, make the buttons a bit less in height so they better match the url-requester ui->addConfig->setFixedHeight( ui->currentConfig->sizeHint().height() ); ui->removeConfig->setFixedHeight( ui->currentConfig->sizeHint().height() ); connect( ui->currentConfig, static_cast(&QComboBox::activated), this, &CustomBuildSystemConfigWidget::changeCurrentConfig); connect( ui->configWidget, &ConfigWidget::changed, this, &CustomBuildSystemConfigWidget::configChanged ); connect( ui->addConfig, &QPushButton::clicked, this, &CustomBuildSystemConfigWidget::addConfig); connect( ui->removeConfig, &QPushButton::clicked, this, &CustomBuildSystemConfigWidget::removeConfig); connect( ui->currentConfig, &QComboBox::editTextChanged, this, &CustomBuildSystemConfigWidget::renameCurrentConfig ); connect( this, &CustomBuildSystemConfigWidget::changed, this, &CustomBuildSystemConfigWidget::verify ); } void CustomBuildSystemConfigWidget::loadDefaults() { } void CustomBuildSystemConfigWidget::loadFrom( KConfig* cfg ) { ui->currentConfig->clear(); configs.clear(); QStringList groupNameList; KConfigGroup grp = cfg->group(ConfigConstants::customBuildSystemGroup()); - foreach( const QString& grpName, grp.groupList() ) { + const auto groupList = grp.groupList(); + groupNameList.reserve(groupList.size()); + configs.reserve(groupList.size()); + for (const auto& grpName : groupList) { KConfigGroup subgrp = grp.group( grpName ); CustomBuildSystemConfig config; config.title = subgrp.readEntry(ConfigConstants::configTitleKey(), QString()); config.buildDir = subgrp.readEntry(ConfigConstants::buildDirKey(), QUrl()); foreach( const QString& subgrpName, subgrp.groupList() ) { if (subgrpName.startsWith(ConfigConstants::toolGroupPrefix())) { KConfigGroup toolgrp = subgrp.group( subgrpName ); CustomBuildSystemTool tool; tool.arguments = toolgrp.readEntry(ConfigConstants::toolArguments(), QString()); tool.executable = toolgrp.readEntry(ConfigConstants::toolExecutable(), QUrl()); tool.envGrp = toolgrp.readEntry(ConfigConstants::toolEnvironment(), QString()); tool.enabled = toolgrp.readEntry(ConfigConstants::toolEnabled(), false); tool.type = CustomBuildSystemTool::ActionType(toolgrp.readEntry(ConfigConstants::toolType(), 0)); config.tools[tool.type] = tool; } } configs << config; ui->currentConfig->addItem( config.title ); groupNameList << grpName; } int idx = groupNameList.indexOf(grp.readEntry(ConfigConstants::currentConfigKey(), QString())); if( !groupNameList.isEmpty() && idx < 0 ) idx = 0; ui->currentConfig->setCurrentIndex( idx ); changeCurrentConfig( idx ); } void CustomBuildSystemConfigWidget::saveConfig( KConfigGroup& grp, CustomBuildSystemConfig& c, int index ) { // Generate group name, access and clear it KConfigGroup subgrp = grp.group(ConfigConstants::buildConfigPrefix() + QString::number(index)); subgrp.deleteGroup(); // Write current configuration key, if our group is current if( ui->currentConfig->currentIndex() == index ) grp.writeEntry(ConfigConstants::currentConfigKey(), subgrp.name()); subgrp.writeEntry(ConfigConstants::configTitleKey(), c.title); subgrp.writeEntry(ConfigConstants::buildDirKey(), c.buildDir); foreach( const CustomBuildSystemTool& tool, c.tools ) { KConfigGroup toolgrp = subgrp.group( generateToolGroupName( tool.type ) ); toolgrp.writeEntry(ConfigConstants::toolType(), int(tool.type)); toolgrp.writeEntry(ConfigConstants::toolEnvironment(), tool.envGrp); toolgrp.writeEntry(ConfigConstants::toolEnabled(), tool.enabled); toolgrp.writeEntry(ConfigConstants::toolExecutable(), tool.executable); toolgrp.writeEntry(ConfigConstants::toolArguments(), tool.arguments); } } void CustomBuildSystemConfigWidget::saveTo( KConfig* cfg, KDevelop::IProject* /*project*/ ) { KConfigGroup subgrp = cfg->group(ConfigConstants::customBuildSystemGroup()); subgrp.deleteGroup(); for( int i = 0; i < ui->currentConfig->count(); i++ ) { configs[i].title = ui->currentConfig->itemText(i); saveConfig( subgrp, configs[i], i ); } cfg->sync(); } void CustomBuildSystemConfigWidget::configChanged() { int idx = ui->currentConfig->currentIndex(); if( idx >= 0 && idx < configs.count() ) { configs[idx] = ui->configWidget->config(); emit changed(); } } void CustomBuildSystemConfigWidget::changeCurrentConfig( int idx ) { if( idx < 0 || idx >= configs.size() ) { ui->configWidget->clear(); emit changed(); return; } CustomBuildSystemConfig cfg = configs.at( idx ); ui->configWidget->loadConfig( cfg ); emit changed(); } void CustomBuildSystemConfigWidget::addConfig() { CustomBuildSystemConfig c; configs.append( c ); ui->currentConfig->addItem( c.title ); ui->currentConfig->setCurrentIndex( ui->currentConfig->count() - 1 ); changeCurrentConfig( ui->currentConfig->currentIndex() ); } void CustomBuildSystemConfigWidget::removeConfig() { int curidx = ui->currentConfig->currentIndex(); Q_ASSERT( curidx < configs.length() && curidx >= 0 ); configs.removeAt( curidx ); ui->currentConfig->removeItem( curidx ); // Make sure the new index is valid if possible, removing the first index // would otherwise set the index to -1 and that will cause havoc in changeCurrentConfig // since it clears the config if a negative index comes in. I'm not sure that is actually // correct, but well easier to fix and understand here right now and the combobox should // be dropped ultimately anyway I guess - or at least not be editable anymore. int newidx = curidx > 0 ? curidx - 1 : 0; ui->currentConfig->setCurrentIndex( newidx ); changeCurrentConfig( ui->currentConfig->currentIndex() ); } void CustomBuildSystemConfigWidget::verify() { Q_ASSERT( ui->currentConfig->count() == configs.count() ); const bool hasAnyConfigurations = (configs.count() != 0); Q_ASSERT( !hasAnyConfigurations || (ui->currentConfig->currentIndex() >= 0) ); ui->configWidget->setEnabled( hasAnyConfigurations ); ui->removeConfig->setEnabled( hasAnyConfigurations ); ui->currentConfig->setEditable( hasAnyConfigurations ); } void CustomBuildSystemConfigWidget::renameCurrentConfig(const QString& name) { int idx = ui->currentConfig->currentIndex(); if( idx >= 0 && idx < configs.count() ) { ui->currentConfig->setItemText( idx, name ); emit changed(); } } diff --git a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp index 30e7bb2936..dcc8fef11c 100644 --- a/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/compilerprovider.cpp @@ -1,280 +1,282 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "compilerprovider.h" #include "debug.h" #include "qtcompat_p.h" #include "compilerfactories.h" #include "settingsmanager.h" #include #include #include #include #include #include #include using namespace KDevelop; namespace { class NoCompiler : public ICompiler { public: NoCompiler(): ICompiler(i18n("None"), QString(), QString(), false) {} QHash< QString, QString > defines(Utils::LanguageType, const QString&) const override { return {}; } Path::List includes(Utils::LanguageType, const QString&) const override { return {}; } }; static CompilerPointer createDummyCompiler() { static CompilerPointer compiler(new NoCompiler()); return compiler; } ConfigEntry configForItem(KDevelop::ProjectBaseItem* item) { if(!item){ return ConfigEntry(); } const Path itemPath = item->path(); const Path rootDirectory = item->project()->path(); auto paths = SettingsManager::globalInstance()->readPaths(item->project()->projectConfiguration().data()); ConfigEntry config; Path closestPath; // find config entry closest to the requested item for (const auto& entry : paths) { auto configEntry = entry; Path targetDirectory = rootDirectory; targetDirectory.addPath(entry.path); if (targetDirectory == itemPath) { return configEntry; } if (targetDirectory.isParentOf(itemPath)) { if (config.path.isEmpty() || targetDirectory.segments().size() > closestPath.segments().size()) { config = configEntry; closestPath = targetDirectory; } } } return config; } } CompilerProvider::CompilerProvider( SettingsManager* settings, QObject* parent ) : QObject( parent ) , m_settings(settings) { - m_factories.append(CompilerFactoryPointer(new GccFactory())); - m_factories.append(CompilerFactoryPointer(new ClangFactory())); + m_factories = { + CompilerFactoryPointer(new GccFactory()), + CompilerFactoryPointer(new ClangFactory()), #ifdef _WIN32 - m_factories.append(CompilerFactoryPointer(new MsvcFactory())); + CompilerFactoryPointer(new MsvcFactory()), #endif + }; if (!QStandardPaths::findExecutable( QStringLiteral("clang") ).isEmpty()) { m_factories[1]->registerDefaultCompilers(this); } if (!QStandardPaths::findExecutable( QStringLiteral("gcc") ).isEmpty()) { m_factories[0]->registerDefaultCompilers(this); } #ifdef _WIN32 if (!QStandardPaths::findExecutable("cl.exe").isEmpty()) { m_factories[2]->registerDefaultCompilers(this); } #endif registerCompiler(createDummyCompiler()); retrieveUserDefinedCompilers(); connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, [this]() { m_defaultProvider.clear(); }); } CompilerProvider::~CompilerProvider() = default; QHash CompilerProvider::defines( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } QHash CompilerProvider::defines( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of defines. if (languageType == Utils::Other) { return {}; } return config.compiler->defines(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( const QString& path ) const { auto config = configForItem(nullptr); auto languageType = Utils::languageType(path, config.parserArguments.parseAmbiguousAsCPP); // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::includes( ProjectBaseItem* item ) const { auto config = configForItem(item); auto languageType = Utils::Cpp; if (item) { languageType = Utils::languageType(item->path().path(), config.parserArguments.parseAmbiguousAsCPP); } // If called on files that we can't compile, return an empty set of includes. if (languageType == Utils::Other) { return {}; } return config.compiler->includes(languageType, config.parserArguments[languageType]); } Path::List CompilerProvider::frameworkDirectories( const QString& /* path */ ) const { return {}; } Path::List CompilerProvider::frameworkDirectories( ProjectBaseItem* /* item */ ) const { return {}; } IDefinesAndIncludesManager::Type CompilerProvider::type() const { return IDefinesAndIncludesManager::CompilerSpecific; } CompilerPointer CompilerProvider::defaultCompiler() const { if (m_defaultProvider) return m_defaultProvider; auto rt = ICore::self()->runtimeController()->currentRuntime(); const auto path = QFile::decodeName(rt->getenv("PATH")).split(QtCompat::listSeparator()); for ( const CompilerPointer& compiler : m_compilers ) { const bool absolutePath = QDir::isAbsolutePath(compiler->path()); if ((absolutePath && QFileInfo::exists(rt->pathInHost(Path(compiler->path())).toLocalFile())) || QStandardPaths::findExecutable( compiler->path(), path).isEmpty() ) { continue; } m_defaultProvider = compiler; break; } if (!m_defaultProvider) m_defaultProvider = createDummyCompiler(); qCDebug(DEFINESANDINCLUDES) << "new default compiler" << rt->name() << m_defaultProvider->name() << m_defaultProvider->path(); return m_defaultProvider; } QVector< CompilerPointer > CompilerProvider::compilers() const { return m_compilers; } CompilerPointer CompilerProvider::compilerForItem( KDevelop::ProjectBaseItem* item ) const { auto compiler = configForItem(item).compiler; Q_ASSERT(compiler); return compiler; } bool CompilerProvider::registerCompiler(const CompilerPointer& compiler) { if (!compiler) { return false; } for(auto c: m_compilers){ if (c->name() == compiler->name()) { return false; } } m_compilers.append(compiler); return true; } void CompilerProvider::unregisterCompiler(const CompilerPointer& compiler) { if (!compiler->editable()) { return; } for (int i = 0; i < m_compilers.count(); i++) { if (m_compilers[i]->name() == compiler->name()) { m_compilers.remove(i); break; } } } QVector< CompilerFactoryPointer > CompilerProvider::compilerFactories() const { return m_factories; } void CompilerProvider::retrieveUserDefinedCompilers() { auto compilers = m_settings->userDefinedCompilers(); for (auto c : compilers) { registerCompiler(c); } } diff --git a/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp b/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp index 860d7d3de4..bd6eee943e 100644 --- a/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/gcclikecompiler.cpp @@ -1,219 +1,224 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * 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 "gcclikecompiler.h" #include #include #include #include #include #include #include using namespace KDevelop; namespace { QString languageOption(Utils::LanguageType type) { switch (type) { case Utils::C: return QStringLiteral("-xc"); case Utils::Cpp: return QStringLiteral("-xc++"); case Utils::OpenCl: return QStringLiteral("-xcl"); case Utils::Cuda: return QStringLiteral("-xcuda"); case Utils::ObjC: return QStringLiteral("-xobjective-c"); default: Q_UNREACHABLE(); } } QString languageStandard(const QString& arguments) { // TODO: handle -ansi flag: In C mode, this is equivalent to -std=c90. In C++ mode, it is equivalent to -std=c++98. const QRegularExpression regexp(QStringLiteral("-std=(\\S+)")); auto result = regexp.match(arguments); if (result.hasMatch()) return result.captured(0); // no -std= flag passed -> assume c++11 return QStringLiteral("-std=c++11"); } } Defines GccLikeCompiler::defines(Utils::LanguageType type, const QString& arguments) const { auto& data = m_definesIncludes[arguments]; if (!data.definedMacros.isEmpty() ) { return data.definedMacros; } // #define a 1 // #define a QRegExp defineExpression( "#define\\s+(\\S+)(?:\\s+(.*)\\s*)?"); const auto rt = ICore::self()->runtimeController()->currentRuntime(); QProcess proc; proc.setProcessChannelMode( QProcess::MergedChannels ); // TODO: what about -mXXX or -target= flags, some of these change search paths/defines - QStringList compilerArguments{languageOption(type), languageStandard(arguments)}; - compilerArguments.append(QStringLiteral("-dM")); - compilerArguments.append(QStringLiteral("-E")); - compilerArguments.append(QStringLiteral("-")); - + const QStringList compilerArguments{ + languageOption(type), + languageStandard(arguments), + QStringLiteral("-dM"), + QStringLiteral("-E"), + QStringLiteral("-"), + }; proc.setStandardInputFile(QProcess::nullDevice()); proc.setProgram(path()); proc.setArguments(compilerArguments); rt->startProcess(&proc); if ( !proc.waitForStarted( 2000 ) || !proc.waitForFinished( 2000 ) ) { qCDebug(DEFINESANDINCLUDES) << "Unable to read standard macro definitions from "<< path(); return {}; } if (proc.exitCode() != 0) { qCWarning(DEFINESANDINCLUDES) << "error while fetching defines for the compiler:" << path() << proc.readAll(); return {}; } while ( proc.canReadLine() ) { auto line = proc.readLine(); if ( defineExpression.indexIn( line ) != -1 ) { data.definedMacros[defineExpression.cap( 1 )] = defineExpression.cap( 2 ).trimmed(); } } return data.definedMacros; } Path::List GccLikeCompiler::includes(Utils::LanguageType type, const QString& arguments) const { auto& data = m_definesIncludes[arguments]; if ( !data.includePaths.isEmpty() ) { return data.includePaths; } const auto rt = ICore::self()->runtimeController()->currentRuntime(); QProcess proc; proc.setProcessChannelMode( QProcess::MergedChannels ); // The following command will spit out a bunch of information we don't care // about before spitting out the include paths. The parts we care about // look like this: // #include "..." search starts here: // #include <...> search starts here: // /usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2 // /usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/i486-linux-gnu // /usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/backward // /usr/local/include // /usr/lib/gcc/i486-linux-gnu/4.1.2/include // /usr/include // End of search list. - QStringList compilerArguments{languageOption(type), languageStandard(arguments)}; - compilerArguments.append(QStringLiteral("-E")); - compilerArguments.append(QStringLiteral("-v")); - compilerArguments.append(QStringLiteral("-")); + const QStringList compilerArguments{ + languageOption(type), + languageStandard(arguments), + QStringLiteral("-E"), + QStringLiteral("-v"), + QStringLiteral("-"), + }; proc.setStandardInputFile(QProcess::nullDevice()); proc.setProgram(path()); proc.setArguments(compilerArguments); rt->startProcess(&proc); if ( !proc.waitForStarted( 2000 ) || !proc.waitForFinished( 2000 ) ) { qCDebug(DEFINESANDINCLUDES) << "Unable to read standard include paths from " << path(); return {}; } if (proc.exitCode() != 0) { qCWarning(DEFINESANDINCLUDES) << "error while fetching includes for the compiler:" << path() << proc.readAll(); return {}; } // We'll use the following constants to know what we're currently parsing. enum Status { Initial, FirstSearch, Includes, Finished }; Status mode = Initial; const auto output = QString::fromLocal8Bit( proc.readAllStandardOutput() ); foreach( const auto &line, output.splitRef( '\n' ) ) { switch ( mode ) { case Initial: if ( line.indexOf( QLatin1String("#include \"...\"") ) != -1 ) { mode = FirstSearch; } break; case FirstSearch: if ( line.indexOf( QLatin1String("#include <...>") ) != -1 ) { mode = Includes; } break; case Includes: //Detect the include-paths by the first space that is prepended. Reason: The list may contain relative paths like "." if ( !line.startsWith( ' ' ) ) { // We've reached the end of the list. mode = Finished; } else { // This is an include path, add it to the list. auto hostPath = rt->pathInHost(Path(line.trimmed().toString())); // but skip folders with compiler builtins, we cannot parse these with clang if (!QFile::exists(hostPath.toLocalFile() + QLatin1String("/cpuid.h"))) { data.includePaths << Path(QFileInfo(hostPath.toLocalFile()).canonicalFilePath()); } } break; default: break; } if ( mode == Finished ) { break; } } return data.includePaths; } void GccLikeCompiler::invalidateCache() { m_definesIncludes.clear(); } GccLikeCompiler::GccLikeCompiler(const QString& name, const QString& path, bool editable, const QString& factoryName): ICompiler(name, path, factoryName, editable) { connect(ICore::self()->runtimeController(), &IRuntimeController::currentRuntimeChanged, this, &GccLikeCompiler::invalidateCache); } diff --git a/plugins/custom-definesandincludes/compilerprovider/msvccompiler.cpp b/plugins/custom-definesandincludes/compilerprovider/msvccompiler.cpp index 145213162e..9ef6d5d9fc 100644 --- a/plugins/custom-definesandincludes/compilerprovider/msvccompiler.cpp +++ b/plugins/custom-definesandincludes/compilerprovider/msvccompiler.cpp @@ -1,130 +1,131 @@ /* * This file is part of KDevelop * * Copyright 2010 Patrick Spendrin * Copyright 2013 Kevin Funk * Copyright 2014 Sergey Kalinichev * * 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 "msvccompiler.h" #include #include #include #include using namespace KDevelop; Defines MsvcCompiler::defines(Utils::LanguageType, const QString&) const { Defines ret; //Get standard macros from kdevmsvcdefinehelpers KProcess proc; proc.setOutputChannelMode( KProcess::MergedChannels ); proc.setTextModeEnabled( true ); // we want to use kdevmsvcdefinehelper as a pseudo compiler backend which // returns the defines used in msvc. there is no such thing as -dM with cl.exe proc << path() << QStringLiteral("/nologo") << QStringLiteral("/Bxkdevmsvcdefinehelper") << QStringLiteral("empty.cpp"); // this will fail, so check on that as well if ( proc.execute( 5000 ) == 2 ) { QString line; proc.readLine(); // read the filename while ( proc.canReadLine() ) { QByteArray buff = proc.readLine(); qCDebug(DEFINESANDINCLUDES) << "msvcstandardmacros:" << buff; if ( !buff.isEmpty() ) { line = buff; if ( line.startsWith( QLatin1String("#define ") ) ) { line = line.right( line.length() - 8 ).trimmed(); int pos = line.indexOf( ' ' ); if ( pos != -1 ) { ret[line.left( pos )] = line.right( line.length() - pos - 1 ).toUtf8(); } else { ret[line] = QLatin1String(""); } } } } } else { qCDebug(DEFINESANDINCLUDES) << "Unable to read standard c++ macro definitions from " + path(); while ( proc.canReadLine() ){ qCDebug(DEFINESANDINCLUDES) << proc.readLine(); } qCDebug(DEFINESANDINCLUDES) << proc.exitCode(); } // MSVC builtin attributes { ret[QStringLiteral("__cdecl")] = QLatin1String(""); ret[QStringLiteral("__fastcall")] = QLatin1String(""); ret[QStringLiteral("__stdcall")] = QLatin1String(""); ret[QStringLiteral("__thiscall")] = QLatin1String(""); } // MSVC builtin types // see http://msdn.microsoft.com/en-us/library/cc953fe1.aspx { ret[QStringLiteral("__int8")] = QStringLiteral("char"); ret[QStringLiteral("__int16")] = QStringLiteral("short"); ret[QStringLiteral("__int32")] = QStringLiteral("int"); ret[QStringLiteral("__int64")] = QStringLiteral("long long"); ret[QStringLiteral("__int16")] = QStringLiteral("short"); ret[QStringLiteral("__ptr32")] = QLatin1String(""); ret[QStringLiteral("__ptr64")] = QLatin1String(""); } // MSVC specific modifiers // see http://msdn.microsoft.com/en-us/library/vstudio/s04b5w00.aspx { ret[QStringLiteral("__sptr")] = QLatin1String(""); ret[QStringLiteral("__uptr")] = QLatin1String(""); ret[QStringLiteral("__unaligned")] = QLatin1String(""); ret[QStringLiteral("__w64")] = QLatin1String(""); } // MSVC function specifiers // see http://msdn.microsoft.com/de-de/library/z8y1yy88.aspx { ret[QStringLiteral("__inline")] = QLatin1String(""); ret[QStringLiteral("__forceinline")] = QLatin1String(""); } return ret; } Path::List MsvcCompiler::includes(Utils::LanguageType, const QString&) const { QStringList _includePaths = QProcessEnvironment::systemEnvironment().value(QStringLiteral("INCLUDE")).split(QLatin1Char(';'), QString::SkipEmptyParts); Path::List includePaths; + includePaths.reserve(_includePaths.size()); foreach( const QString &include, _includePaths ) { includePaths.append( Path( QDir::fromNativeSeparators( include ) ) ); } return includePaths; } MsvcCompiler::MsvcCompiler(const QString& name, const QString& path, bool editable, const QString& factoryName): ICompiler(name, path, factoryName, editable) {} diff --git a/plugins/custom-definesandincludes/kcm_widget/includesmodel.cpp b/plugins/custom-definesandincludes/kcm_widget/includesmodel.cpp index cb8e3e333c..901edaa840 100644 --- a/plugins/custom-definesandincludes/kcm_widget/includesmodel.cpp +++ b/plugins/custom-definesandincludes/kcm_widget/includesmodel.cpp @@ -1,122 +1,120 @@ /************************************************************************ * * * Copyright 2010 Andreas Pakulat * * * * 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 or version 3 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, see . * ************************************************************************/ #include "includesmodel.h" #include IncludesModel::IncludesModel( QObject* parent ) : QAbstractListModel( parent ) { } QVariant IncludesModel::data( const QModelIndex& index, int role ) const { if( !index.isValid() || ( role != Qt::DisplayRole && role != Qt::EditRole ) ) { return QVariant(); } if( index.row() < 0 || index.row() >= rowCount() || index.column() != 0 ) { return QVariant(); } return m_includes.at( index.row() ); } int IncludesModel::rowCount( const QModelIndex& parent ) const { return parent.isValid() ? 0 : m_includes.count(); } bool IncludesModel::setData( const QModelIndex& index, const QVariant& value, int role ) { if( !index.isValid() || role != Qt::EditRole ) { return false; } if( index.row() < 0 || index.row() >= rowCount() || index.column() != 0 ) { return false; } m_includes[index.row()] = value.toString().trimmed(); emit dataChanged( index, index ); return true; } Qt::ItemFlags IncludesModel::flags( const QModelIndex& index ) const { if( !index.isValid() ) { return Qt::NoItemFlags; } return Qt::ItemFlags( Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled ); } QStringList IncludesModel::includes() const { return m_includes; } void IncludesModel::setIncludes(const QStringList& includes ) { beginResetModel(); m_includes.clear(); foreach( const QString includePath, includes ) { addIncludeInternal( includePath.trimmed() ); } endResetModel(); } bool IncludesModel::removeRows( int row, int count, const QModelIndex& parent ) { if( row >= 0 && count > 0 && row < m_includes.count() ) { beginRemoveRows( parent, row, row + count - 1 ); for( int i = 0; i < count; ++i ) { m_includes.removeAt( row ); } endRemoveRows(); return true; } return false; } void IncludesModel::addInclude( const QString& includePath ) { if( !includePath.isEmpty() ) { beginInsertRows( QModelIndex(), rowCount(), rowCount() ); addIncludeInternal( includePath ); endInsertRows(); } } void IncludesModel::addIncludeInternal( const QString& includePath ) { if ( includePath.isEmpty() ) { return; } // Do not allow duplicates - foreach( const QString &include, m_includes ) { - if( include == includePath ) { - return; - } + if (m_includes.contains(includePath)) { + return; } m_includes << includePath; } diff --git a/plugins/custom-definesandincludes/kcm_widget/projectpathswidget.cpp b/plugins/custom-definesandincludes/kcm_widget/projectpathswidget.cpp index e943a7d368..d6ca62934b 100644 --- a/plugins/custom-definesandincludes/kcm_widget/projectpathswidget.cpp +++ b/plugins/custom-definesandincludes/kcm_widget/projectpathswidget.cpp @@ -1,306 +1,306 @@ /************************************************************************ * * * Copyright 2010 Andreas Pakulat * * Copyright 2014 Sergey Kalinichev * * * * 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, see . * ************************************************************************/ #include "projectpathswidget.h" #include #include #include #include #include #include #include "../compilerprovider/compilerprovider.h" #include "../compilerprovider/settingsmanager.h" #include "ui_projectpathswidget.h" #include "ui_batchedit.h" #include "projectpathsmodel.h" #include using namespace KDevelop; namespace { enum PageType { IncludesPage, DefinesPage, ParserArgumentsPage }; } ProjectPathsWidget::ProjectPathsWidget( QWidget* parent ) : QWidget(parent), ui(new Ui::ProjectPathsWidget), pathsModel(new ProjectPathsModel(this)) { ui->setupUi( this ); // hack taken from kurlrequester, make the buttons a bit less in height so they better match the url-requester ui->addPath->setFixedHeight( ui->projectPaths->sizeHint().height() ); ui->removePath->setFixedHeight( ui->projectPaths->sizeHint().height() ); connect( ui->addPath, &QPushButton::clicked, this, &ProjectPathsWidget::addProjectPath ); connect( ui->removePath, &QPushButton::clicked, this, &ProjectPathsWidget::deleteProjectPath ); connect( ui->batchEdit, &QPushButton::clicked, this, &ProjectPathsWidget::batchEdit ); ui->projectPaths->setModel( pathsModel ); connect( ui->projectPaths, static_cast(&KComboBox::currentIndexChanged), this, &ProjectPathsWidget::projectPathSelected ); connect( pathsModel, &ProjectPathsModel::dataChanged, this, &ProjectPathsWidget::changed ); connect( pathsModel, &ProjectPathsModel::rowsInserted, this, &ProjectPathsWidget::changed ); connect( pathsModel, &ProjectPathsModel::rowsRemoved, this, &ProjectPathsWidget::changed ); connect( ui->compiler, static_cast(&QComboBox::activated), this, &ProjectPathsWidget::changed ); connect( ui->compiler, static_cast(&QComboBox::activated), this, &ProjectPathsWidget::changeCompilerForPath ); connect( ui->includesWidget, static_cast(&IncludesWidget::includesChanged), this, &ProjectPathsWidget::includesChanged ); connect( ui->definesWidget, static_cast(&DefinesWidget::definesChanged), this, &ProjectPathsWidget::definesChanged ); connect(ui->languageParameters, &QTabWidget::currentChanged, this, &ProjectPathsWidget::tabChanged); connect(ui->parserWidget, &ParserWidget::changed, this, &ProjectPathsWidget::parserArgumentsChanged); tabChanged(IncludesPage); } QVector ProjectPathsWidget::paths() const { return pathsModel->paths(); } void ProjectPathsWidget::setPaths( const QVector& paths ) { bool b = blockSignals( true ); clear(); pathsModel->setPaths( paths ); blockSignals( b ); ui->projectPaths->setCurrentIndex(0); // at least a project root item is present ui->languageParameters->setCurrentIndex(0); // Set compilers ui->compiler->clear(); auto settings = SettingsManager::globalInstance(); auto compilers = settings->provider()->compilers(); for (int i = 0 ; i < compilers.count(); ++i) { Q_ASSERT(compilers[i]); if (!compilers[i]) { continue; } ui->compiler->addItem(compilers[i]->name()); QVariant val; val.setValue(compilers[i]); ui->compiler->setItemData(i, val); } projectPathSelected(0); updateEnablements(); } void ProjectPathsWidget::definesChanged( const Defines& defines ) { qCDebug(DEFINESANDINCLUDES) << "defines changed"; updatePathsModel( QVariant::fromValue(defines), ProjectPathsModel::DefinesDataRole ); } void ProjectPathsWidget::includesChanged( const QStringList& includes ) { qCDebug(DEFINESANDINCLUDES) << "includes changed"; updatePathsModel( includes, ProjectPathsModel::IncludesDataRole ); } void ProjectPathsWidget::parserArgumentsChanged() { updatePathsModel(QVariant::fromValue(ui->parserWidget->parserArguments()), ProjectPathsModel::ParserArgumentsRole); } void ProjectPathsWidget::updatePathsModel(const QVariant& newData, int role) { QModelIndex idx = pathsModel->index( ui->projectPaths->currentIndex(), 0, QModelIndex() ); if( idx.isValid() ) { bool b = pathsModel->setData( idx, newData, role ); if( b ) { emit changed(); } } } void ProjectPathsWidget::projectPathSelected( int index ) { if( index < 0 && pathsModel->rowCount() > 0 ) { index = 0; } Q_ASSERT(index >= 0); const QModelIndex midx = pathsModel->index( index, 0 ); ui->includesWidget->setIncludes( pathsModel->data( midx, ProjectPathsModel::IncludesDataRole ).toStringList() ); ui->definesWidget->setDefines( pathsModel->data( midx, ProjectPathsModel::DefinesDataRole ).value() ); Q_ASSERT(pathsModel->data(midx, ProjectPathsModel::CompilerDataRole).value()); ui->compiler->setCurrentText(pathsModel->data(midx, ProjectPathsModel::CompilerDataRole).value()->name()); ui->parserWidget->setParserArguments(pathsModel->data(midx, ProjectPathsModel::ParserArgumentsRole).value()); updateEnablements(); } void ProjectPathsWidget::clear() { bool sigDisabled = ui->projectPaths->blockSignals( true ); pathsModel->setPaths({}); ui->includesWidget->clear(); ui->definesWidget->clear(); updateEnablements(); ui->projectPaths->blockSignals( sigDisabled ); } void ProjectPathsWidget::addProjectPath() { const QUrl directory = pathsModel->data(pathsModel->index(0, 0), ProjectPathsModel::FullUrlDataRole).toUrl(); QPointer dlg = new QFileDialog(this, i18n("Select Project Path"), directory.toLocalFile()); dlg->setFileMode(QFileDialog::Directory); dlg->setOption(QFileDialog::ShowDirsOnly); if (dlg->exec()) { pathsModel->addPath(dlg->selectedUrls().value(0)); ui->projectPaths->setCurrentIndex(pathsModel->rowCount() - 1); updateEnablements(); } delete dlg; } void ProjectPathsWidget::deleteProjectPath() { const QModelIndex idx = pathsModel->index( ui->projectPaths->currentIndex(), 0 ); if( KMessageBox::questionYesNo( this, i18n("Are you sure you want to remove the configuration for the path '%1'?", pathsModel->data( idx, Qt::DisplayRole ).toString() ), QStringLiteral("Remove Path Configuration") ) == KMessageBox::Yes ) { pathsModel->removeRows( ui->projectPaths->currentIndex(), 1 ); } updateEnablements(); } void ProjectPathsWidget::setProject(KDevelop::IProject* w_project) { pathsModel->setProject( w_project ); ui->includesWidget->setProject( w_project ); } void ProjectPathsWidget::updateEnablements() { // Disable removal of the project root entry which is always first in the list ui->removePath->setEnabled( ui->projectPaths->currentIndex() > 0 ); } void ProjectPathsWidget::batchEdit() { Ui::BatchEdit be; QPointer dialog = new QDialog(this); be.setupUi(dialog); const int index = qMax(ui->projectPaths->currentIndex(), 0); const QModelIndex midx = pathsModel->index(index, 0); if (!midx.isValid()) { return; } bool includesTab = ui->languageParameters->currentIndex() == 0; if (includesTab) { auto includes = pathsModel->data(midx, ProjectPathsModel::IncludesDataRole).toStringList(); be.textEdit->setPlainText(includes.join(QLatin1Char('\n'))); dialog->setWindowTitle(i18n("Edit include directories/files")); } else { auto defines = pathsModel->data(midx, ProjectPathsModel::DefinesDataRole).value(); for (auto it = defines.constBegin(); it != defines.constEnd(); it++) { - be.textEdit->appendPlainText(it.key() + "=" + it.value()); + be.textEdit->appendPlainText(it.key() + QLatin1Char('=') + it.value()); } dialog->setWindowTitle(i18n("Edit defined macros")); } if (dialog->exec() != QDialog::Accepted) { delete dialog; return; } if (includesTab) { auto includes = be.textEdit->toPlainText().split('\n', QString::SkipEmptyParts); for (auto& s : includes) { s = s.trimmed(); } pathsModel->setData(midx, includes, ProjectPathsModel::IncludesDataRole); } else { auto list = be.textEdit->toPlainText().split('\n', QString::SkipEmptyParts); Defines defines; for (auto& d : list) { //This matches: a=b, a=, a QRegExp r("^([^=]+)(=(.*))?$"); if (!r.exactMatch(d)) { continue; } defines[r.cap(1).trimmed()] = r.cap(3).trimmed(); } pathsModel->setData(midx, QVariant::fromValue(defines), ProjectPathsModel::DefinesDataRole); } projectPathSelected(index); delete dialog; } void ProjectPathsWidget::setCurrentCompiler(const QString& name) { for (int i = 0 ; i < ui->compiler->count(); ++i) { if(ui->compiler->itemText(i) == name) { ui->compiler->setCurrentIndex(i); } } } CompilerPointer ProjectPathsWidget::currentCompiler() const { return ui->compiler->itemData(ui->compiler->currentIndex()).value(); } void ProjectPathsWidget::tabChanged(int idx) { if (idx == ParserArgumentsPage) { ui->batchEdit->setVisible(false); ui->compilerBox->setVisible(true); ui->configureLabel->setText(i18n("Configure C/C++ parser")); } else { ui->batchEdit->setVisible(true); ui->compilerBox->setVisible(false); ui->configureLabel->setText(i18n("Configure which macros and include directories/files will be added to the parser during project parsing:")); } } void ProjectPathsWidget::changeCompilerForPath() { for (int idx = 0; idx < pathsModel->rowCount(); idx++) { const QModelIndex midx = pathsModel->index(idx, 0); if (pathsModel->data(midx, Qt::DisplayRole) == ui->projectPaths->currentText()) { pathsModel->setData(midx, QVariant::fromValue(currentCompiler()), ProjectPathsModel::CompilerDataRole); break; } } } diff --git a/plugins/custommake/makefileresolver/makefileresolver.cpp b/plugins/custommake/makefileresolver/makefileresolver.cpp index 0de730e11f..1eb61ce4c4 100644 --- a/plugins/custommake/makefileresolver/makefileresolver.cpp +++ b/plugins/custommake/makefileresolver/makefileresolver.cpp @@ -1,640 +1,641 @@ /* * KDevelop C++ Language Support * * Copyright 2007 David Nolden * Copyright 2014 Kevin Funk * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU 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 "makefileresolver.h" #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include #include // #define VERBOSE #if defined(VERBOSE) #define ifTest(x) x #else #define ifTest(x) #endif const int maximumInternalResolutionDepth = 3; using namespace std; using namespace KDevelop; namespace { ///After how many seconds should we retry? static const int CACHE_FAIL_FOR_SECONDS = 200; static const int processTimeoutSeconds = 30; struct CacheEntry { CacheEntry() { } ModificationRevisionSet modificationTime; Path::List paths; Path::List frameworkDirectories; QHash defines; QString errorMessage, longErrorMessage; bool failed = false; QMap failedFiles; QDateTime failTime; }; typedef QMap Cache; static Cache s_cache; static QMutex s_cacheMutex; } /** * Compatibility: * make/automake: Should work perfectly * cmake: Thanks to the path-recursion, this works with cmake(tested with version "2.4-patch 6" * with kdelibs out-of-source and with kdevelop4 in-source) **/ class SourcePathInformation { public: explicit SourcePathInformation(const QString& path) : m_path(path) { } QString createCommand(const QString& absoluteFile, const QString& workingDirectory, const QString& makeParameters) const { QString relativeFile = Path(workingDirectory).relativePath(Path(absoluteFile)); #ifndef Q_OS_FREEBSD // GNU make implicitly enables "-w" for sub-makes, we don't want that QLatin1String noPrintDirFlag = QLatin1String(" --no-print-directory"); #else QLatin1String noPrintDirFlag; #endif return "make -k" + noPrintDirFlag + " -W \'" + absoluteFile + "\' -W \'" + relativeFile + "\' -n " + makeParameters; } bool hasMakefile() const { QFileInfo makeFile(m_path, QStringLiteral("Makefile")); return makeFile.exists(); } QStringList possibleTargets(const QString& targetBaseName) const { - QStringList ret; + const QStringList ret{ ///@todo open the make-file, and read the target-names from there. //It would be nice if all targets could be processed in one call, the problem is the exit-status of make, so for now make has to be called multiple times. - ret << targetBaseName + ".o"; - ret << targetBaseName + ".lo"; + targetBaseName + QLatin1String(".o"), + targetBaseName + QLatin1String(".lo"), //ret << targetBaseName + ".lo " + targetBaseName + ".o"; - ret << targetBaseName + ".ko"; + targetBaseName + QLatin1String(".ko"), + }; return ret; } private: QString m_path; }; static void mergePaths(KDevelop::Path::List& destList, const KDevelop::Path::List& srcList) { foreach (const Path& path, srcList) { if (!destList.contains(path)) destList.append(path); } } void PathResolutionResult::mergeWith(const PathResolutionResult& rhs) { mergePaths(paths, rhs.paths); mergePaths(frameworkDirectories, rhs.frameworkDirectories); includePathDependency += rhs.includePathDependency; defines.unite(rhs.defines); } PathResolutionResult::PathResolutionResult(bool success, const QString& errorMessage, const QString& longErrorMessage) : success(success) , errorMessage(errorMessage) , longErrorMessage(longErrorMessage) {} PathResolutionResult::operator bool() const { return success; } ModificationRevisionSet MakeFileResolver::findIncludePathDependency(const QString& file) { QString oldSourceDir = m_source; QString oldBuildDir = m_build; Path currentWd(mapToBuild(file)); ModificationRevisionSet rev; while (currentWd.hasParent()) { currentWd = currentWd.parent(); QString path = currentWd.toLocalFile(); QFileInfo makeFile(QDir(path), QStringLiteral("Makefile")); if (makeFile.exists()) { IndexedString makeFileStr(makeFile.filePath()); rev.addModificationRevision(makeFileStr, ModificationRevision::revisionForFile(makeFileStr)); break; } } setOutOfSourceBuildSystem(oldSourceDir, oldBuildDir); return rev; } bool MakeFileResolver::executeCommand(const QString& command, const QString& workingDirectory, QString& result) const { ifTest(cout << "executing " << command.toUtf8().constData() << endl); ifTest(cout << "in " << workingDirectory.toUtf8().constData() << endl); KProcess proc; proc.setWorkingDirectory(workingDirectory); proc.setOutputChannelMode(KProcess::MergedChannels); QStringList args(command.split(' ')); QString prog = args.takeFirst(); proc.setProgram(prog, args); int status = proc.execute(processTimeoutSeconds * 1000); result = proc.readAll(); return status == 0; } MakeFileResolver::MakeFileResolver() { } ///More efficient solution: Only do exactly one call for each directory. During that call, mark all source-files as changed, and make all targets for those files. PathResolutionResult MakeFileResolver::resolveIncludePath(const QString& file) { if (file.isEmpty()) { // for unit tests with temporary files return PathResolutionResult(); } QFileInfo fi(file); return resolveIncludePath(fi.fileName(), fi.absolutePath()); } QString MakeFileResolver::mapToBuild(const QString &path) const { QString wd = QDir::cleanPath(path); if (m_outOfSource) { if (wd.startsWith(m_source) && !wd.startsWith(m_build)) { //Move the current working-directory out of source, into the build-system wd = QDir::cleanPath(m_build + '/' + wd.mid(m_source.length())); } } return wd; } void MakeFileResolver::clearCache() { QMutexLocker l(&s_cacheMutex); s_cache.clear(); } PathResolutionResult MakeFileResolver::resolveIncludePath(const QString& file, const QString& _workingDirectory, int maxStepsUp) { //Prefer this result when returning a "fail". The include-paths of this result will always be added. PathResolutionResult resultOnFail; if (m_isResolving) return PathResolutionResult(false, i18n("Tried include path resolution while another resolution process was still running")); //Make the working-directory absolute QString workingDirectory = _workingDirectory; if (QFileInfo(workingDirectory).isRelative()) { QUrl u = QUrl::fromLocalFile(QDir::currentPath()); if (workingDirectory == QLatin1String(".")) workingDirectory = QString(); else if (workingDirectory.startsWith(QLatin1String("./"))) workingDirectory = workingDirectory.mid(2); if (!workingDirectory.isEmpty()) { u = u.adjusted(QUrl::StripTrailingSlash); u.setPath(u.path() + '/' + workingDirectory); } workingDirectory = u.toLocalFile(); } else workingDirectory = _workingDirectory; ifTest(cout << "working-directory: " << workingDirectory.toLocal8Bit().data() << " file: " << file.toLocal8Bit().data() << std::endl;) QDir sourceDir(workingDirectory); QDir dir = QDir(mapToBuild(sourceDir.absolutePath())); QFileInfo makeFile(dir, QStringLiteral("Makefile")); if (!makeFile.exists()) { if (maxStepsUp > 0) { //If there is no makefile in this directory, go one up and re-try from there QFileInfo fileName(file); QString localName = sourceDir.dirName(); if (sourceDir.cdUp() && !fileName.isAbsolute()) { - QString checkFor = localName + "/" + file; + const QString checkFor = localName + QLatin1Char('/') + file; PathResolutionResult oneUp = resolveIncludePath(checkFor, sourceDir.path(), maxStepsUp-1); if (oneUp.success) { oneUp.mergeWith(resultOnFail); return oneUp; } } } if (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty() || !resultOnFail.frameworkDirectories.isEmpty()) return resultOnFail; else return PathResolutionResult(false, i18n("Makefile is missing in folder \"%1\"", dir.absolutePath()), i18n("Problem while trying to resolve include paths for %1", file)); } PushValue e(m_isResolving, true); Path::List cachedPaths; //If the call doesn't succeed, use the cached not up-to-date version Path::List cachedFWDirs; QHash cachedDefines; ModificationRevisionSet dependency; dependency.addModificationRevision(IndexedString(makeFile.filePath()), ModificationRevision::revisionForFile(IndexedString(makeFile.filePath()))); dependency += resultOnFail.includePathDependency; Cache::iterator it; { QMutexLocker l(&s_cacheMutex); it = s_cache.find(dir.path()); if (it != s_cache.end()) { cachedPaths = it->paths; cachedFWDirs = it->frameworkDirectories; cachedDefines = it->defines; if (dependency == it->modificationTime) { if (!it->failed) { //We have a valid cached result PathResolutionResult ret(true); ret.paths = it->paths; ret.frameworkDirectories = it->frameworkDirectories; ret.defines = it->defines; ret.mergeWith(resultOnFail); return ret; } else { //We have a cached failed result. We should use that for some time but then try again. Return the failed result if: (there were too many tries within this folder OR this file was already tried) AND The last tries have not expired yet if (/*(it->failedFiles.size() > 3 || it->failedFiles.find(file) != it->failedFiles.end()) &&*/ it->failTime.secsTo(QDateTime::currentDateTime()) < CACHE_FAIL_FOR_SECONDS) { PathResolutionResult ret(false); //Fake that the result is ok ret.errorMessage = i18n("Cached: %1", it->errorMessage); ret.longErrorMessage = it->longErrorMessage; ret.paths = it->paths; ret.frameworkDirectories = it->frameworkDirectories; ret.defines = it->defines; ret.mergeWith(resultOnFail); return ret; } else { //Try getting a correct result again } } } } } ///STEP 1: Prepare paths QString targetName; QFileInfo fi(file); QString absoluteFile = file; if (fi.isRelative()) absoluteFile = workingDirectory + '/' + file; absoluteFile = QDir::cleanPath(absoluteFile); int dot; if ((dot = file.lastIndexOf('.')) == -1) { if (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty() || !resultOnFail.frameworkDirectories.isEmpty()) return resultOnFail; else return PathResolutionResult(false, i18n("Filename %1 seems to be malformed", file)); } targetName = file.left(dot); QString wd = dir.path(); if (QFileInfo(wd).isRelative()) { wd = QDir::cleanPath(QDir::currentPath() + '/' + wd); } wd = mapToBuild(wd); SourcePathInformation source(wd); QStringList possibleTargets = source.possibleTargets(targetName); ///STEP 3: Try resolving the paths, by using once the absolute and once the relative file-path. Which kind is required differs from setup to setup. ///STEP 3.1: Try resolution using the absolute path PathResolutionResult res; //Try for each possible target res = resolveIncludePathInternal(absoluteFile, wd, possibleTargets.join(QLatin1Char(' ')), source, maximumInternalResolutionDepth); if (!res) { ifTest(cout << "Try for absolute file " << absoluteFile.toLocal8Bit().data() << " and targets " << possibleTargets.join(", ").toLocal8Bit().data() << " failed: " << res.longErrorMessage.toLocal8Bit().data() << endl;) } res.includePathDependency = dependency; if (res.paths.isEmpty()) { res.paths = cachedPaths; //We failed, maybe there is an old cached result, use that. res.defines = cachedDefines; } // a build command could contain only one or more -iframework or -F specifications. if (res.frameworkDirectories.isEmpty()) { res.frameworkDirectories = cachedFWDirs; } { QMutexLocker l(&s_cacheMutex); if (it == s_cache.end()) it = s_cache.insert(dir.path(), CacheEntry()); CacheEntry& ce(*it); ce.paths = res.paths; ce.frameworkDirectories = res.frameworkDirectories; ce.modificationTime = dependency; if (!res) { ce.failed = true; ce.errorMessage = res.errorMessage; ce.longErrorMessage = res.longErrorMessage; ce.failTime = QDateTime::currentDateTime(); ce.failedFiles[file] = true; } else { ce.failed = false; ce.failedFiles.clear(); } } if (!res && (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty() || !resultOnFail.frameworkDirectories.isEmpty())) return resultOnFail; return res; } static QRegularExpression includeRegularExpression() { static const QRegularExpression expression( "\\s(--include-dir=|-I\\s*|-isystem\\s+|-iframework\\s+|-F\\s*)(" "\\'.*\\'|\\\".*\\\"" //Matches "hello", 'hello', 'hello"hallo"', etc. "|" "((?:\\\\.)?([\\S^\\\\]?))+" //Matches /usr/I\ am\ a\ strange\ path/include ")(?=\\s)" ); Q_ASSERT(expression.isValid()); return expression; } PathResolutionResult MakeFileResolver::resolveIncludePathInternal(const QString& file, const QString& workingDirectory, const QString& makeParameters, const SourcePathInformation& source, int maxDepth) { --maxDepth; if (maxDepth < 0) return PathResolutionResult(false); QString fullOutput; executeCommand(source.createCommand(file, workingDirectory, makeParameters), workingDirectory, fullOutput); { QRegExp newLineRx("\\\\\\n"); fullOutput.remove(newLineRx); } ///@todo collect multiple outputs at the same time for performance-reasons QString firstLine = fullOutput; int lineEnd; if ((lineEnd = fullOutput.indexOf('\n')) != -1) firstLine.truncate(lineEnd); //Only look at the first line of output /** * There's two possible cases this can currently handle. * 1.: gcc is called, with the parameters we are searching for (so we parse the parameters) * 2.: A recursive make is called, within another directory(so we follow the recursion and try again) "cd /foo/bar && make -f pi/pa/build.make pi/pa/po.o * */ ///STEP 1: Test if it is a recursive make-call // Do not search for recursive make-calls if we already have include-paths available. Happens in kernel modules. if (!includeRegularExpression().match(fullOutput).hasMatch()) { QRegExp makeRx("\\bmake\\s"); int offset = 0; while ((offset = makeRx.indexIn(firstLine, offset)) != -1) { QString prefix = firstLine.left(offset).trimmed(); if (prefix.endsWith(QLatin1String("&&")) || prefix.endsWith(';') || prefix.isEmpty()) { QString newWorkingDirectory = workingDirectory; ///Extract the new working-directory if (!prefix.isEmpty()) { if (prefix.endsWith(QLatin1String("&&"))) prefix.truncate(prefix.length() - 2); else if (prefix.endsWith(';')) prefix.truncate(prefix.length() - 1); ///Now test if what we have as prefix is a simple "cd /foo/bar" call. //In cases like "cd /media/data/kdedev/4.0/build/kdevelop && cd /media/data/kdedev/4.0/build/kdevelop" //We use the second directory. For t hat reason we search for the last index of "cd " int cdIndex = prefix.lastIndexOf(QLatin1String("cd ")); if (cdIndex != -1) { newWorkingDirectory = prefix.right(prefix.length() - 3 - cdIndex).trimmed(); if (QFileInfo(newWorkingDirectory).isRelative()) newWorkingDirectory = workingDirectory + '/' + newWorkingDirectory; newWorkingDirectory = QDir::cleanPath(newWorkingDirectory); } } if (newWorkingDirectory == workingDirectory) { return PathResolutionResult(false, i18n("Failed to extract new working directory"), i18n("Output was: %1", fullOutput)); } QFileInfo d(newWorkingDirectory); if (d.exists()) { ///The recursive working-directory exists. QString makeParams = firstLine.mid(offset+5); if (!makeParams.contains(';') && !makeParams.contains(QLatin1String("&&"))) { ///Looks like valid parameters ///Make the file-name absolute, so it can be referenced from any directory QString absoluteFile = file; if (QFileInfo(absoluteFile).isRelative()) absoluteFile = workingDirectory + '/' + file; Path absolutePath(absoluteFile); ///Try once with absolute, and if that fails with relative path of the file SourcePathInformation newSource(newWorkingDirectory); PathResolutionResult res = resolveIncludePathInternal(absolutePath.toLocalFile(), newWorkingDirectory, makeParams, newSource, maxDepth); if (res) return res; return resolveIncludePathInternal(Path(newWorkingDirectory).relativePath(absolutePath), newWorkingDirectory, makeParams , newSource, maxDepth); }else{ return PathResolutionResult(false, i18n("Recursive make call failed"), i18n("The parameter string \"%1\" does not seem to be valid. Output was: %2.", makeParams, fullOutput)); } } else { return PathResolutionResult(false, i18n("Recursive make call failed"), i18n("The directory \"%1\" does not exist. Output was: %2.", newWorkingDirectory, fullOutput)); } } else { return PathResolutionResult(false, i18n("Malformed recursive make call"), i18n("Output was: %1", fullOutput)); } ++offset; if (offset >= firstLine.length()) break; } } ///STEP 2: Search the output for include-paths PathResolutionResult ret = processOutput(fullOutput, workingDirectory); if (ret.paths.isEmpty() && ret.frameworkDirectories.isEmpty()) return PathResolutionResult(false, i18n("Could not extract include paths from make output"), i18n("Folder: \"%1\" Command: \"%2\" Output: \"%3\"", workingDirectory, source.createCommand(file, workingDirectory, makeParameters), fullOutput)); return ret; } QRegularExpression MakeFileResolver::defineRegularExpression() { static const QRegularExpression pattern( QStringLiteral("-D([^\\s=]+)(?:=(?:\"(.*?)(? 2)) { //probable a quoted path if (path.endsWith(path.left(1))) { //Quotation is ok, remove it path = path.mid(1, path.length() - 2); } } if (QDir::isRelativePath(path)) path = workingDirectory + '/' + path; const auto& internedPath = internPath(path); const auto& type = match.captured(1); const auto isFramework = type.startsWith(QLatin1String("-iframework")) || type.startsWith(QLatin1String("-F")); if (isFramework) { ret.frameworkDirectories << internedPath; } else { ret.paths << internedPath; } } } { const auto& defineRx = defineRegularExpression(); auto it = defineRx.globalMatch(fullOutput); while (it.hasNext()) { const auto match = it.next(); QString value; if (match.lastCapturedIndex() > 1) { value = unescape(match.capturedRef(match.lastCapturedIndex())); } ret.defines[internString(match.captured(1))] = internString(value); } } return ret; } void MakeFileResolver::resetOutOfSourceBuild() { m_outOfSource = false; } void MakeFileResolver::setOutOfSourceBuildSystem(const QString& source, const QString& build) { if (source == build) { resetOutOfSourceBuild(); return; } m_outOfSource = true; m_source = QDir::cleanPath(source); m_build = QDir::cleanPath(m_build); } Path MakeFileResolver::internPath(const QString& path) const { Path& ret = m_pathCache[path]; if (ret.isEmpty() != path.isEmpty()) { ret = Path(path); } return ret; } QString MakeFileResolver::internString(const QString& path) const { auto it = m_stringCache.constFind(path); if (it != m_stringCache.constEnd()) { return *it; } m_stringCache.insert(path); return path; } // kate: indent-width 2; tab-width 2; diff --git a/plugins/customscript/customscript_plugin.cpp b/plugins/customscript/customscript_plugin.cpp index 83806324c5..29463c6298 100644 --- a/plugins/customscript/customscript_plugin.cpp +++ b/plugins/customscript/customscript_plugin.cpp @@ -1,559 +1,560 @@ /* This file is part of KDevelop Copyright (C) 2008 Cédric Pasteur Copyright (C) 2011 David Nolden This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "customscript_plugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; static QPointer indentPluginSingleton; K_PLUGIN_FACTORY_WITH_JSON(CustomScriptFactory, "kdevcustomscript.json", registerPlugin(); ) // Replaces ${KEY} in command with variables[KEY] static QString replaceVariables(QString command, QMap variables) { while (command.contains(QLatin1String("${"))) { int pos = command.indexOf(QLatin1String("${")); int end = command.indexOf(QLatin1Char('}'), pos + 2); if (end == -1) { break; } QString key = command.mid(pos + 2, end - pos - 2); if (variables.contains(key)) { command.replace(pos, 1 + end - pos, variables[key]); } else { qCDebug(CUSTOMSCRIPT) << "found no variable while replacing in shell-command" << command << "key" << key << "available:" << variables; command.remove(pos, 1 + end - pos); } } return command; } CustomScriptPlugin::CustomScriptPlugin(QObject* parent, const QVariantList&) : IPlugin(QStringLiteral("kdevcustomscript"), parent) { m_currentStyle = predefinedStyles().at(0); indentPluginSingleton = this; } CustomScriptPlugin::~CustomScriptPlugin() { } QString CustomScriptPlugin::name() const { // This needs to match the X-KDE-PluginInfo-Name entry from the .desktop file! return QStringLiteral("kdevcustomscript"); } QString CustomScriptPlugin::caption() const { return QStringLiteral("Custom Script Formatter"); } QString CustomScriptPlugin::description() const { return i18n("Indent and Format Source Code.
    " "This plugin allows using powerful external formatting tools " "that can be invoked through the command-line.
    " "For example, the uncrustify, astyle or indent " "formatters can be used.
    " "The advantage of command-line formatters is that formatting configurations " "can be easily shared by all team members, independent of their preferred IDE."); } QString CustomScriptPlugin::formatSourceWithStyle(SourceFormatterStyle style, const QString& text, const QUrl& url, const QMimeType& /*mime*/, const QString& leftContext, const QString& rightContext) const { KProcess proc; QTextStream ios(&proc); std::unique_ptr tmpFile; if (style.content().isEmpty()) { style = predefinedStyle(style.name()); if (style.content().isEmpty()) { qCWarning(CUSTOMSCRIPT) << "Empty contents for style" << style.name() << "for indent plugin"; return text; } } QString useText = text; useText = leftContext + useText + rightContext; QMap projectVariables; foreach (IProject* project, ICore::self()->projectController()->projects()) { projectVariables[project->name()] = project->path().toUrl().toLocalFile(); } QString command = style.content(); // Replace ${Project} with the project path command = replaceVariables(command, projectVariables); command.replace(QLatin1String("$FILE"), url.toLocalFile()); if (command.contains(QLatin1String("$TMPFILE"))) { tmpFile.reset(new QTemporaryFile(QDir::tempPath() + "/code")); tmpFile->setAutoRemove(false); if (tmpFile->open()) { qCDebug(CUSTOMSCRIPT) << "using temporary file" << tmpFile->fileName(); command.replace(QLatin1String("$TMPFILE"), tmpFile->fileName()); QByteArray useTextArray = useText.toLocal8Bit(); if (tmpFile->write(useTextArray) != useTextArray.size()) { qCWarning(CUSTOMSCRIPT) << "failed to write text to temporary file"; return text; } } else { qCWarning(CUSTOMSCRIPT) << "Failed to create a temporary file"; return text; } tmpFile->close(); } qCDebug(CUSTOMSCRIPT) << "using shell command for indentation: " << command; proc.setShellCommand(command); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); proc.start(); if (!proc.waitForStarted()) { qCDebug(CUSTOMSCRIPT) << "Unable to start indent" << endl; return text; } if (!tmpFile.get()) { proc.write(useText.toLocal8Bit()); } proc.closeWriteChannel(); if (!proc.waitForFinished()) { qCDebug(CUSTOMSCRIPT) << "Process doesn't finish" << endl; return text; } QString output; if (tmpFile.get()) { QFile f(tmpFile->fileName()); if (f.open(QIODevice::ReadOnly)) { output = QString::fromLocal8Bit(f.readAll()); } else { qCWarning(CUSTOMSCRIPT) << "Failed opening the temporary file for reading"; return text; } } else { output = ios.readAll(); } if (output.isEmpty()) { qCWarning(CUSTOMSCRIPT) << "indent returned empty text for style" << style.name() << style.content(); return text; } int tabWidth = 4; if ((!leftContext.isEmpty() || !rightContext.isEmpty()) && (text.contains(' ') || output.contains(' '))) { // If we have to do contex-matching with tabs, determine the correct tab-width so that the context // can be matched correctly Indentation indent = indentation(url); if (indent.indentationTabWidth > 0) { tabWidth = indent.indentationTabWidth; } } return KDevelop::extractFormattedTextFromContext(output, text, leftContext, rightContext, tabWidth); } QString CustomScriptPlugin::formatSource(const QString& text, const QUrl& url, const QMimeType& mime, const QString& leftContext, const QString& rightContext) const { auto style = KDevelop::ICore::self()->sourceFormatterController()->styleForUrl(url, mime); return formatSourceWithStyle(style, text, url, mime, leftContext, rightContext); } static QVector stylesFromLanguagePlugins() { QVector styles; foreach (auto lang, ICore::self()->languageController()->loadedLanguages()) { const SourceFormatterItemList& languageStyles = lang->sourceFormatterItems(); for (const SourceFormatterStyleItem& item: languageStyles) { if (item.engine == QLatin1String("customscript")) { styles << item.style; } } } return styles; } KDevelop::SourceFormatterStyle CustomScriptPlugin::predefinedStyle(const QString& name) const { for (auto langStyle: stylesFromLanguagePlugins()) { qCDebug(CUSTOMSCRIPT) << "looking at style from language with custom sample" << langStyle.description() << langStyle.overrideSample(); if (langStyle.name() == name) { return langStyle; } } SourceFormatterStyle result(name); if (name == QLatin1String("GNU_indent_GNU")) { result.setCaption(i18n("Gnu Indent: GNU")); result.setContent(QStringLiteral("indent")); result.setUsePreview(true); } else if (name == QLatin1String("GNU_indent_KR")) { result.setCaption(i18n("Gnu Indent: Kernighan & Ritchie")); result.setContent(QStringLiteral("indent -kr")); result.setUsePreview(true); } else if (name == QLatin1String("GNU_indent_orig")) { result.setCaption(i18n("Gnu Indent: Original Berkeley indent style")); result.setContent(QStringLiteral("indent -orig")); result.setUsePreview(true); } else if (name == QLatin1String("kdev_format_source")) { result.setCaption(QStringLiteral("KDevelop: kdev_format_source")); result.setContent(QStringLiteral("kdev_format_source $FILE $TMPFILE")); result.setUsePreview(false); result.setDescription(i18n("Description:
    " "kdev_format_source is a script bundled with KDevelop " "which allows using fine-grained formatting rules by placing " "meta-files called format_sources into the file-system.

    " "Each line of the format_sources files defines a list of wildcards " "followed by a colon and the used formatting-command.

    " "The formatting-command should use $TMPFILE to reference the " "temporary file to reformat.

    " "Example:
    " "*.cpp *.h : myformatter $TMPFILE
    " "This will reformat all files ending with .cpp or .h using " "the custom formatting script myformatter.

    " "Example:
    " "subdir/* : uncrustify -l CPP -f $TMPFILE -c uncrustify.config -o $TMPFILE
    " "This will reformat all files in subdirectory subdir using the uncrustify " "tool with the config-file uncrustify.config.")); } result.setMimeTypes({ {"text/x-c++src", "C++"}, {"text/x-chdr", "C"}, {"text/x-c++hdr", "C++"}, {"text/x-csrc", "C"}, {"text/x-java", "Java"}, {"text/x-csharp", "C#"} }); return result; } QVector CustomScriptPlugin::predefinedStyles() const { - QVector styles = stylesFromLanguagePlugins(); - styles << predefinedStyle(QStringLiteral("kdev_format_source")); - styles << predefinedStyle(QStringLiteral("GNU_indent_GNU")); - styles << predefinedStyle(QStringLiteral("GNU_indent_KR")); - styles << predefinedStyle(QStringLiteral("GNU_indent_orig")); + const QVector styles = stylesFromLanguagePlugins() + QVector{ + predefinedStyle(QStringLiteral("kdev_format_source")), + predefinedStyle(QStringLiteral("GNU_indent_GNU")), + predefinedStyle(QStringLiteral("GNU_indent_KR")), + predefinedStyle(QStringLiteral("GNU_indent_orig")), + }; return styles; } KDevelop::SettingsWidget* CustomScriptPlugin::editStyleWidget(const QMimeType& mime) const { Q_UNUSED(mime); return new CustomScriptPreferences(); } static QString formattingSample() { return "// Formatting\n" "void func(){\n" "\tif(isFoo(a,b))\n" "\tbar(a,b);\n" "if(isFoo)\n" "\ta=bar((b-c)*a,*d--);\n" "if( isFoo( a,b ) )\n" "\tbar(a, b);\n" "if (isFoo) {isFoo=false;cat << isFoo <::const_iterator it = list.begin();\n" "}\n" "namespace A {\n" "namespace B {\n" "void foo() {\n" " if (true) {\n" " func();\n" " } else {\n" " // bla\n" " }\n" "}\n" "}\n" "}\n"; } static QString indentingSample() { return QLatin1String( "// Indentation\n" "#define foobar(A)\\\n" "{Foo();Bar();}\n" "#define anotherFoo(B)\\\n" "return Bar()\n" "\n" "namespace Bar\n" "{\n" "class Foo\n" "{public:\n" "Foo();\n" "virtual ~Foo();\n" "};\n" "void bar(int foo)\n" "{\n" "switch (foo)\n" "{\n" "case 1:\n" "a+=1;\n" "break;\n" "case 2:\n" "{\n" "a += 2;\n" " break;\n" "}\n" "}\n" "if (isFoo)\n" "{\n" "bar();\n" "}\n" "else\n" "{\n" "anotherBar();\n" "}\n" "}\n" "int foo()\n" "\twhile(isFoo)\n" "\t\t{\n" "\t\t\t// ...\n" "\t\t\tgoto error;\n" "\t\t/* .... */\n" "\t\terror:\n" "\t\t\t//...\n" "\t\t}\n" "\t}\n" "fooArray[]={ red,\n" "\tgreen,\n" "\tdarkblue};\n" "fooFunction(barArg1,\n" "\tbarArg2,\n" "\tbarArg3);\n" ); } QString CustomScriptPlugin::previewText(const SourceFormatterStyle& style, const QMimeType& /*mime*/) const { if (!style.overrideSample().isEmpty()) { return style.overrideSample(); } return formattingSample() + "\n\n" + indentingSample(); } QStringList CustomScriptPlugin::computeIndentationFromSample(const QUrl& url) const { QStringList ret; auto languages = ICore::self()->languageController()->languagesForUrl(url); if (languages.isEmpty()) { return ret; } QString sample = languages[0]->indentationSample(); QString formattedSample = formatSource(sample, url, QMimeDatabase().mimeTypeForUrl(url), QString(), QString()); QStringList lines = formattedSample.split(QLatin1Char('\n')); foreach (QString line, lines) { if (!line.isEmpty() && line[0].isSpace()) { QString indent; foreach (QChar c, line) { if (c.isSpace()) { indent.push_back(c); } else { break; } } if (!indent.isEmpty() && !ret.contains(indent)) { ret.push_back(indent); } } } return ret; } CustomScriptPlugin::Indentation CustomScriptPlugin::indentation(const QUrl& url) const { Indentation ret; QStringList indent = computeIndentationFromSample(url); if (indent.isEmpty()) { qCDebug(CUSTOMSCRIPT) << "failed extracting a valid indentation from sample for url" << url; return ret; // No valid indentation could be extracted } if (indent[0].contains(' ')) { ret.indentWidth = indent[0].count(' '); } if (!indent.join(QString()).contains(' ')) { ret.indentationTabWidth = -1; // Tabs are not used for indentation } if (indent[0] == QLatin1String(" ")) { // The script indents with tabs-only // The problem is that we don't know how // wide a tab is supposed to be. // // We need indentation-width=tab-width // to make the editor do tab-only formatting, // so choose a random with of 4. ret.indentWidth = 4; ret.indentationTabWidth = 4; } else if (ret.indentWidth) { // Tabs are used for indentation, alongside with spaces // Try finding out how many spaces one tab stands for. // Do it by assuming a uniform indentation-step with each level. for (int pos = 0; pos < indent.size(); ++pos) { if (indent[pos] == QLatin1String(" ")&& pos >= 1) { // This line consists of only a tab. int prevWidth = indent[pos - 1].length(); int prevPrevWidth = (pos >= 2) ? indent[pos - 2].length() : 0; int step = prevWidth - prevPrevWidth; qCDebug(CUSTOMSCRIPT) << "found in line " << pos << prevWidth << prevPrevWidth << step; if (step > 0 && step <= prevWidth) { qCDebug(CUSTOMSCRIPT) << "Done"; ret.indentationTabWidth = prevWidth + step; break; } } } } qCDebug(CUSTOMSCRIPT) << "indent-sample" << "\"" + indent.join(QLatin1Char('\n')) + "\"" << "extracted tab-width" << ret.indentationTabWidth << "extracted indentation width" << ret.indentWidth; return ret; } void CustomScriptPreferences::updateTimeout() { const QString& text = indentPluginSingleton.data()->previewText(m_style, QMimeType()); QString formatted = indentPluginSingleton.data()->formatSourceWithStyle(m_style, text, QUrl(), QMimeType()); emit previewTextChanged(formatted); } CustomScriptPreferences::CustomScriptPreferences() { m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect(m_updateTimer, &QTimer::timeout, this, &CustomScriptPreferences::updateTimeout); m_vLayout = new QVBoxLayout(this); m_vLayout->setMargin(0); m_captionLabel = new QLabel; m_vLayout->addWidget(m_captionLabel); m_vLayout->addSpacing(10); m_hLayout = new QHBoxLayout; m_vLayout->addLayout(m_hLayout); m_commandLabel = new QLabel; m_hLayout->addWidget(m_commandLabel); m_commandEdit = new QLineEdit; m_hLayout->addWidget(m_commandEdit); m_commandLabel->setText(i18n("Command:")); m_vLayout->addSpacing(10); m_bottomLabel = new QLabel; m_vLayout->addWidget(m_bottomLabel); m_bottomLabel->setTextFormat(Qt::RichText); m_bottomLabel->setText( i18n("You can enter an arbitrary shell command.
    " "The unformatted source-code is reached to the command
    " "through the standard input, and the
    " "formatted result is read from the standard output.
    " "
    " "If you add $TMPFILE into the command, then
    " "a temporary file is used for transferring the code.")); connect(m_commandEdit, &QLineEdit::textEdited, this, &CustomScriptPreferences::textEdited); m_vLayout->addSpacing(10); m_moreVariablesButton = new QPushButton(i18n("More Variables")); connect(m_moreVariablesButton, &QPushButton::clicked, this, &CustomScriptPreferences::moreVariablesClicked); m_vLayout->addWidget(m_moreVariablesButton); m_vLayout->addStretch(); } void CustomScriptPreferences::load(const KDevelop::SourceFormatterStyle& style) { m_style = style; m_commandEdit->setText(style.content()); m_captionLabel->setText(i18n("Style: %1", style.caption())); updateTimeout(); } QString CustomScriptPreferences::save() { return m_commandEdit->text(); } void CustomScriptPreferences::moreVariablesClicked(bool) { KMessageBox::information(ICore::self()->uiController()->activeMainWindow(), i18n("$TMPFILE will be replaced with the path to a temporary file.
    " "The code will be written into the file, the temporary
    " "file will be substituted into that position, and the result
    " "will be read out of that file.
    " "
    " "$FILE will be replaced with the path of the original file.
    " "The contents of the file must not be modified, changes are allowed
    " "only in $TMPFILE.
    " "
    " "${PROJECT_NAME} will be replaced by the path of
    " "the currently open project with the matching name." ), i18n("Variable Replacements")); } #include "customscript_plugin.moc" // kate: indent-mode cstyle; space-indent off; tab-width 4; diff --git a/plugins/cvs/cvsannotatejob.cpp b/plugins/cvs/cvsannotatejob.cpp index c97d456d99..f37e940084 100644 --- a/plugins/cvs/cvsannotatejob.cpp +++ b/plugins/cvs/cvsannotatejob.cpp @@ -1,89 +1,90 @@ /*************************************************************************** * Copyright 2008 Robert Gruber * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "cvsannotatejob.h" #include "debug.h" #include #include #include #include #include CvsAnnotateJob::CvsAnnotateJob(KDevelop::IPlugin* parent, KDevelop::OutputJob::OutputJobVerbosity verbosity) : CvsJob(parent, verbosity) { } CvsAnnotateJob::~CvsAnnotateJob() { } QVariant CvsAnnotateJob::fetchResults() { // Convert job's output into KDevelop::VcsAnnotation KDevelop::VcsAnnotation annotateInfo; parseOutput(output(), getDirectory(), annotateInfo); QList lines; + lines.reserve(annotateInfo.lineCount()); for(int i=0; i < annotateInfo.lineCount(); i++) { KDevelop::VcsAnnotationLine line = annotateInfo.line(i); lines.append( qVariantFromValue( line ) ); } return lines; } void CvsAnnotateJob::parseOutput(const QString& jobOutput, const QString& workingDirectory, KDevelop::VcsAnnotation& annotateInfo) { // each annotation line looks like this: // 1.1 (kdedev 10-Nov-07): #include static QRegExp re("([^\\s]+)\\s+\\(([^\\s]+)\\s+([^\\s]+)\\):\\s(.*)"); // the file is pomoted like this: // Annotations for main.cpp static QRegExp reFile("Annotations for\\s(.*)"); QStringList lines = jobOutput.split('\n'); for (int i=0, linenumber=0; i * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * Copyright 2016 Aetf * * 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 "midebugsession.h" #include "debuglog.h" #include "midebugger.h" #include "mivariable.h" #include "mi/mi.h" #include "mi/micommand.h" #include "mi/micommandqueue.h" #include "stty.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; MIDebugSession::MIDebugSession(MIDebuggerPlugin *plugin) : m_procLineMaker(new ProcessLineMaker(this)) , m_commandQueue(new CommandQueue) , m_debuggerState(s_dbgNotStarted | s_appNotStarted) , m_tty(nullptr) , m_plugin(plugin) { // setup signals connect(m_procLineMaker, &ProcessLineMaker::receivedStdoutLines, this, &MIDebugSession::inferiorStdoutLines); connect(m_procLineMaker, &ProcessLineMaker::receivedStderrLines, this, &MIDebugSession::inferiorStderrLines); // forward tty output to process line maker connect(this, &MIDebugSession::inferiorTtyStdout, m_procLineMaker, &ProcessLineMaker::slotReceivedStdout); connect(this, &MIDebugSession::inferiorTtyStderr, m_procLineMaker, &ProcessLineMaker::slotReceivedStderr); // FIXME: see if this still works //connect(statusBarIndicator, SIGNAL(doubleClicked()), // controller, SLOT(explainDebuggerStatus())); // FIXME: reimplement / re-enable //connect(this, SIGNAL(addWatchVariable(QString)), controller->variables(), SLOT(slotAddWatchVariable(QString))); //connect(this, SIGNAL(evaluateExpression(QString)), controller->variables(), SLOT(slotEvaluateExpression(QString))); } MIDebugSession::~MIDebugSession() { qCDebug(DEBUGGERCOMMON) << "Destroying MIDebugSession"; // Deleting the session involves shutting down gdb nicely. // When were attached to a process, we must first detach so that the process // can continue running as it was before being attached. gdb is quite slow to // detach from a process, so we must process events within here to get a "clean" // shutdown. if (!debuggerStateIsOn(s_dbgNotStarted)) { stopDebugger(); } } IDebugSession::DebuggerState MIDebugSession::state() const { return m_sessionState; } QMap & MIDebugSession::variableMapping() { return m_allVariables; } MIVariable* MIDebugSession::findVariableByVarobjName(const QString &varobjName) const { if (m_allVariables.count(varobjName) == 0) return nullptr; return m_allVariables.value(varobjName); } void MIDebugSession::markAllVariableDead() { for (auto i = m_allVariables.begin(), e = m_allVariables.end(); i != e; ++i) { i.value()->markAsDead(); } m_allVariables.clear(); } bool MIDebugSession::restartAvaliable() const { if (debuggerStateIsOn(s_attached) || debuggerStateIsOn(s_core)) { return false; } else { return true; } } bool MIDebugSession::startDebugger(ILaunchConfiguration *cfg) { qCDebug(DEBUGGERCOMMON) << "Starting new debugger instance"; if (m_debugger) { qCWarning(DEBUGGERCOMMON) << "m_debugger object still exists"; delete m_debugger; m_debugger = nullptr; } m_debugger = createDebugger(); m_debugger->setParent(this); // output signals connect(m_debugger, &MIDebugger::applicationOutput, this, [this](const QString &output) { auto lines = output.split(QRegularExpression(QStringLiteral("[\r\n]")), QString::SkipEmptyParts); for (auto &line : lines) { int p = line.length(); while (p >= 1 && (line[p-1] == '\r' || line[p-1] == '\n')) p--; if (p != line.length()) - line.remove(p, line.length() - p); + line.truncate(p); } emit inferiorStdoutLines(lines); }); connect(m_debugger, &MIDebugger::userCommandOutput, this, &MIDebugSession::debuggerUserCommandOutput); connect(m_debugger, &MIDebugger::internalCommandOutput, this, &MIDebugSession::debuggerInternalCommandOutput); connect(m_debugger, &MIDebugger::debuggerInternalOutput, this, &MIDebugSession::debuggerInternalOutput); // state signals connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::inferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::inferiorRunning); // internal handlers connect(m_debugger, &MIDebugger::ready, this, &MIDebugSession::slotDebuggerReady); connect(m_debugger, &MIDebugger::exited, this, &MIDebugSession::slotDebuggerExited); connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::slotInferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::slotInferiorRunning); connect(m_debugger, &MIDebugger::notification, this, &MIDebugSession::processNotification); // start the debugger. Do this after connecting all signals so that initial // debugger output, and important events like the debugger died are reported. QStringList extraArguments; if (!m_sourceInitFile) extraArguments << QStringLiteral("--nx"); auto config = cfg ? cfg->config() // FIXME: this is only used when attachToProcess or examineCoreFile. // Change to use a global launch configuration when calling : KConfigGroup(KSharedConfig::openConfig(), "GDB Config"); if (!m_debugger->start(config, extraArguments)) { // debugger failed to start, ensure debugger and session state are correctly updated. setDebuggerStateOn(s_dbgFailedStart); return false; } // FIXME: here, we should wait until the debugger is up and waiting for input. // Then, clear s_dbgNotStarted // It's better to do this right away so that the state bit is always correct. setDebuggerStateOff(s_dbgNotStarted); // Initialise debugger. At this stage debugger is sitting wondering what to do, // and to whom. initializeDebugger(); qCDebug(DEBUGGERCOMMON) << "Debugger instance started"; return true; } bool MIDebugSession::startDebugging(ILaunchConfiguration* cfg, IExecutePlugin* iexec) { qCDebug(DEBUGGERCOMMON) << "Starting new debug session"; Q_ASSERT(cfg); Q_ASSERT(iexec); // Ensure debugger is started first if (debuggerStateIsOn(s_appNotStarted)) { emit showMessage(i18n("Running program"), 1000); } if (debuggerStateIsOn(s_dbgNotStarted)) { if (!startDebugger(cfg)) return false; } if (debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "Tried to run when debugger shutting down"; return false; } // Only dummy err here, actual erros have been checked already in the job and we don't get here if there were any QString err; QString executable = iexec->executable(cfg, err).toLocalFile(); configInferior(cfg, iexec, executable); // Set up the tty for the inferior bool config_useExternalTerminal = iexec->useTerminal(cfg); QString config_ternimalName = iexec->terminal(cfg); if (!config_ternimalName.isEmpty()) { // the external terminal cmd contains additional arguments, just get the terminal name config_ternimalName = KShell::splitArgs(config_ternimalName).first(); } m_tty.reset(new STTY(config_useExternalTerminal, config_ternimalName)); if (!config_useExternalTerminal) { connect(m_tty.get(), &STTY::OutOutput, this, &MIDebugSession::inferiorTtyStdout); connect(m_tty.get(), &STTY::ErrOutput, this, &MIDebugSession::inferiorTtyStderr); } QString tty(m_tty->getSlave()); #ifndef Q_OS_WIN if (tty.isEmpty()) { KMessageBox::information(qApp->activeWindow(), m_tty->lastError(), i18n("warning")); m_tty.reset(nullptr); return false; } #endif addCommand(InferiorTtySet, tty); // Change the working directory to the correct one QString dir = iexec->workingDirectory(cfg).toLocalFile(); if (dir.isEmpty()) { dir = QFileInfo(executable).absolutePath(); } addCommand(EnvironmentCd, '"' + dir + '"'); // Set the run arguments QStringList arguments = iexec->arguments(cfg, err); if (!arguments.isEmpty()) addCommand(ExecArguments, KShell::joinArgs(arguments)); // Do other debugger specific config options and actually start the inferior program if (!execInferior(cfg, iexec, executable)) { return false; } QString config_startWith = cfg->config().readEntry(Config::StartWithEntry, QStringLiteral("ApplicationOutput")); if (config_startWith == QLatin1String("GdbConsole")) { emit raiseDebuggerConsoleViews(); } else if (config_startWith == QLatin1String("FrameStack")) { emit raiseFramestackViews(); } else { // ApplicationOutput is raised in DebugJob (by setting job to Verbose/Silent) } return true; } // FIXME: use same configuration process as startDebugging bool MIDebugSession::attachToProcess(int pid) { qCDebug(DEBUGGERCOMMON) << "Attach to process" << pid; emit showMessage(i18n("Attaching to process %1", pid), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } setDebuggerStateOn(s_attached); //set current state to running, after attaching we will get *stopped response setDebuggerStateOn(s_appRunning); addCommand(TargetAttach, QString::number(pid), this, &MIDebugSession::handleTargetAttach, CmdHandlesError); addCommand(new SentinelCommand(breakpointController(), &MIBreakpointController::initSendBreakpoints)); raiseEvent(connected_to_program); emit raiseFramestackViews(); return true; } void MIDebugSession::handleTargetAttach(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not attach debugger:
    ")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } bool MIDebugSession::examineCoreFile(const QUrl &debugee, const QUrl &coreFile) { emit showMessage(i18n("Examining core file %1", coreFile.toLocalFile()), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } // FIXME: support non-local URLs if (!loadCoreFile(nullptr, debugee.toLocalFile(), coreFile.toLocalFile())) { return false; } raiseEvent(program_state_changed); return true; } #define ENUM_NAME(o,e,v) (o::staticMetaObject.enumerator(o::staticMetaObject.indexOfEnumerator(#e)).valueToKey((v))) void MIDebugSession::setSessionState(DebuggerState state) { qCDebug(DEBUGGERCOMMON) << "Session state changed to" << ENUM_NAME(IDebugSession, DebuggerState, state) << "(" << state << ")"; if (state != m_sessionState) { m_sessionState = state; emit stateChanged(state); } } bool MIDebugSession::debuggerStateIsOn(DBGStateFlags state) const { return m_debuggerState & state; } DBGStateFlags MIDebugSession::debuggerState() const { return m_debuggerState; } void MIDebugSession::setDebuggerStateOn(DBGStateFlags stateOn) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState | stateOn); m_debuggerState |= stateOn; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerStateOff(DBGStateFlags stateOff) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState & ~stateOff); m_debuggerState &= ~stateOff; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerState(DBGStateFlags newState) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, newState); m_debuggerState = newState; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::debuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { int delta = oldState ^ newState; if (delta) { QString out; #define STATE_CHECK(name) \ do { \ if (delta & name) { \ out += ((newState & name) ? " +" : " -"); \ out += #name; \ delta &= ~name; \ } \ } while (0) STATE_CHECK(s_dbgNotStarted); STATE_CHECK(s_appNotStarted); STATE_CHECK(s_programExited); STATE_CHECK(s_attached); STATE_CHECK(s_core); STATE_CHECK(s_shuttingDown); STATE_CHECK(s_dbgBusy); STATE_CHECK(s_appRunning); STATE_CHECK(s_dbgNotListening); STATE_CHECK(s_automaticContinue); #undef STATE_CHECK for (unsigned int i = 0; delta != 0 && i < 32; ++i) { if (delta & (1 << i)) { delta &= ~(1 << i); - out += ((1 << i) & newState) ? " +" : " -"; - out += QString::number(i); + out += (((1 << i) & newState) ? " +" : " -") + QString::number(i); } } } } void MIDebugSession::handleDebuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { QString message; DebuggerState oldSessionState = state(); DebuggerState newSessionState = oldSessionState; DBGStateFlags changedState = oldState ^ newState; if (newState & s_dbgNotStarted) { if (changedState & s_dbgNotStarted) { message = i18n("Debugger stopped"); emit finished(); } if (oldSessionState != NotStartedState || newState & s_dbgFailedStart) { newSessionState = EndedState; } } else { if (newState & s_appNotStarted) { if (oldSessionState == NotStartedState || oldSessionState == StartingState) { newSessionState = StartingState; } else { newSessionState = StoppedState; } } else if (newState & s_programExited) { if (changedState & s_programExited) { message = i18n("Process exited"); } newSessionState = StoppedState; } else if (newState & s_appRunning) { if (changedState & s_appRunning) { message = i18n("Application is running"); } newSessionState = ActiveState; } else { if (changedState & s_appRunning) { message = i18n("Application is paused"); } newSessionState = PausedState; } } // And now? :-) qCDebug(DEBUGGERCOMMON) << "Debugger state changed to:" << newState << message << "- changes:" << changedState; if (!message.isEmpty()) emit showMessage(message, 3000); emit debuggerStateChanged(oldState, newState); // must be last, since it can lead to deletion of the DebugSession if (newSessionState != oldSessionState) { setSessionState(newSessionState); } } void MIDebugSession::restartDebugger() { // We implement restart as kill + slotRun, as opposed as plain "run" // command because kill + slotRun allows any special logic in slotRun // to apply for restart. // // That includes: // - checking for out-of-date project // - special setup for remote debugging. // // Had we used plain 'run' command, restart for remote debugging simply // would not work. if (!debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) { // FIXME: s_dbgBusy or m_debugger->isReady()? if (debuggerStateIsOn(s_dbgBusy)) { interruptDebugger(); } // The -exec-abort is not implemented in gdb // addCommand(ExecAbort); addCommand(NonMI, QStringLiteral("kill")); } run(); } void MIDebugSession::stopDebugger() { if (debuggerStateIsOn(s_dbgNotStarted)) { // we are force to stop even before debugger started, just reset qCDebug(DEBUGGERCOMMON) << "Stopping debugger when it's not started"; return; } m_commandQueue->clear(); qCDebug(DEBUGGERCOMMON) << "try stopping debugger"; if (debuggerStateIsOn(s_shuttingDown) || !m_debugger) return; setDebuggerStateOn(s_shuttingDown); qCDebug(DEBUGGERCOMMON) << "stopping debugger"; // Get debugger's attention if it's busy. We need debugger to be at the // command line so we can stop it. if (!m_debugger->isReady()) { qCDebug(DEBUGGERCOMMON) << "debugger busy on shutdown - interrupting"; interruptDebugger(); } // If the app is attached then we release it here. This doesn't stop // the app running. if (debuggerStateIsOn(s_attached)) { addCommand(TargetDetach); emit debuggerUserCommandOutput(QStringLiteral("(gdb) detach\n")); } // Now try to stop debugger running. addCommand(GdbExit); emit debuggerUserCommandOutput(QStringLiteral("(gdb) quit")); // We cannot wait forever, kill gdb after 5 seconds if it's not yet quit QPointer guarded_this(this); QTimer::singleShot(5000, [guarded_this](){ if (guarded_this) { if (!guarded_this->debuggerStateIsOn(s_programExited) && guarded_this->debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "debugger not shutdown - killing"; guarded_this->m_debugger->kill(); guarded_this->setDebuggerState(s_dbgNotStarted | s_appNotStarted); guarded_this->raiseEvent(debugger_exited); } } }); emit reset(); } void MIDebugSession::interruptDebugger() { Q_ASSERT(m_debugger); // Explicitly send the interrupt in case something went wrong with the usual // ensureGdbListening logic. m_debugger->interrupt(); addCommand(ExecInterrupt, QString(), CmdInterrupt); } void MIDebugSession::run() { if (debuggerStateIsOn(s_appNotStarted|s_dbgNotStarted|s_shuttingDown)) return; addCommand(MI::ExecContinue, QString(), CmdMaybeStartsRunning); } void MIDebugSession::runToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) runUntil(doc->url(), cursor.line() + 1); } } void MIDebugSession::jumpToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) jumpTo(doc->url(), cursor.line() + 1); } } void MIDebugSession::stepOver() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNext, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepIntoInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStepInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepInto() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStep, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOverInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNextInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOut() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecFinish, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::runUntil(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!url.isValid()) { addCommand(ExecUntil, QString::number(line), CmdMaybeStartsRunning | CmdTemporaryRun); } else { addCommand(ExecUntil, QStringLiteral("%1:%2").arg(url.toLocalFile()).arg(line), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::runUntil(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(ExecUntil, QStringLiteral("*%1").arg(address), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::jumpTo(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (url.isValid()) { addCommand(NonMI, QStringLiteral("tbreak %1:%2").arg(url.toLocalFile()).arg(line)); addCommand(NonMI, QStringLiteral("jump %1:%2").arg(url.toLocalFile()).arg(line)); } } void MIDebugSession::jumpToMemoryAddress(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(NonMI, QStringLiteral("tbreak *%1").arg(address)); addCommand(NonMI, QStringLiteral("jump *%1").arg(address)); } } void MIDebugSession::addUserCommand(const QString& cmd) { auto usercmd = createUserCommand(cmd); if (!usercmd) return; queueCmd(usercmd); // User command can theoreticall modify absolutely everything, // so need to force a reload. // We can do it right now, and don't wait for user command to finish // since commands used to reload all view will be executed after // user command anyway. if (!debuggerStateIsOn(s_appNotStarted) && !debuggerStateIsOn(s_programExited)) raiseEvent(program_state_changed); } MICommand *MIDebugSession::createUserCommand(const QString &cmd) const { MICommand *res = nullptr; if (!cmd.isEmpty() && cmd[0].isDigit()) { // Add a space to the beginning, so debugger won't get confused if the // command starts with a number (won't mix it up with command token added) res = new UserCommand(MI::NonMI, QLatin1Char(' ') + cmd); } else { res = new UserCommand(MI::NonMI, cmd); } return res; } MICommand *MIDebugSession::createCommand(CommandType type, const QString& arguments, CommandFlags flags) const { return new MICommand(type, arguments, flags); } void MIDebugSession::addCommand(MICommand* cmd) { queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) { queueCmd(createCommand(type, arguments, flags)); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::MICommandHandler *handler, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(handler); queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, const MI::FunctionCommandHandler::Function& callback, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(callback); queueCmd(cmd); } // Fairly obvious that we'll add whatever command you give me to a queue // Not quite so obvious though is that if we are going to run again. then any // information requests become redundent and must be removed. // We also try and run whatever command happens to be at the head of // the queue. void MIDebugSession::queueCmd(MICommand *cmd) { if (debuggerStateIsOn(s_dbgNotStarted)) { KMessageBox::information( qApp->activeWindow(), i18n("Gdb command sent when debugger is not running
    " "The command was:
    %1", cmd->initialString()), i18n("Internal error")); return; } if (m_stateReloadInProgress) cmd->setStateReloading(true); m_commandQueue->enqueue(cmd); qCDebug(DEBUGGERCOMMON) << "QUEUE: " << cmd->initialString() << (m_stateReloadInProgress ? "(state reloading)" : "") << m_commandQueue->count() << "pending"; bool varCommandWithContext= (cmd->type() >= MI::VarAssign && cmd->type() <= MI::VarUpdate && cmd->type() != MI::VarDelete); bool stackCommandWithContext = (cmd->type() >= MI::StackInfoDepth && cmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { if (cmd->thread() == -1) qCDebug(DEBUGGERCOMMON) << "\t--thread will be added on execution"; if (cmd->frame() == -1) qCDebug(DEBUGGERCOMMON) << "\t--frame will be added on execution"; } setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_busy); executeCmd(); } void MIDebugSession::executeCmd() { Q_ASSERT(m_debugger); if (debuggerStateIsOn(s_dbgNotListening) && m_commandQueue->haveImmediateCommand()) { // We may have to call this even while a command is currently executing, because // debugger can get into a state where a command such as ExecRun does not send a response // while the inferior is running. ensureDebuggerListening(); } if (!m_debugger->isReady()) return; MICommand* currentCmd = m_commandQueue->nextCommand(); if (!currentCmd) return; if (currentCmd->flags() & (CmdMaybeStartsRunning | CmdInterrupt)) { setDebuggerStateOff(s_automaticContinue); } if (currentCmd->flags() & CmdMaybeStartsRunning) { // GDB can be in a state where it is listening for commands while the program is running. // However, when we send a command such as ExecContinue in this state, GDB may return to // the non-listening state without acknowledging that the ExecContinue command has even // finished, let alone sending a new notification about the program's running state. // So let's be extra cautious about ensuring that we will wake GDB up again if required. setDebuggerStateOn(s_dbgNotListening); } bool varCommandWithContext= (currentCmd->type() >= MI::VarAssign && currentCmd->type() <= MI::VarUpdate && currentCmd->type() != MI::VarDelete); bool stackCommandWithContext = (currentCmd->type() >= MI::StackInfoDepth && currentCmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { // Most var commands should be executed in the context // of the selected thread and frame. if (currentCmd->thread() == -1) currentCmd->setThread(frameStackModel()->currentThread()); if (currentCmd->frame() == -1) currentCmd->setFrame(frameStackModel()->currentFrame()); } QString commandText = currentCmd->cmdToSend(); bool bad_command = false; QString message; int length = commandText.length(); // No i18n for message since it's mainly for debugging. if (length == 0) { // The command might decide it's no longer necessary to send // it. if (SentinelCommand* sc = dynamic_cast(currentCmd)) { qCDebug(DEBUGGERCOMMON) << "SEND: sentinel command, not sending"; sc->invokeHandler(); } else { qCDebug(DEBUGGERCOMMON) << "SEND: command " << currentCmd->initialString() << "changed its mind, not sending"; } delete currentCmd; executeCmd(); return; } else { if (commandText[length-1] != '\n') { bad_command = true; message = QStringLiteral("Debugger command does not end with newline"); } } if (bad_command) { KMessageBox::information(qApp->activeWindow(), i18n("Invalid debugger command
    %1", message), i18n("Invalid debugger command")); executeCmd(); return; } m_debugger->execute(currentCmd); } void MIDebugSession::ensureDebuggerListening() { Q_ASSERT(m_debugger); // Note: we don't use interruptDebugger() here since // we don't want to queue more commands before queuing a command m_debugger->interrupt(); setDebuggerStateOn(s_interruptSent); if (debuggerStateIsOn(s_appRunning)) setDebuggerStateOn(s_automaticContinue); setDebuggerStateOff(s_dbgNotListening); } void MIDebugSession::destroyCmds() { m_commandQueue->clear(); } // FIXME: I don't fully remember what is the business with // m_stateReloadInProgress and whether we can lift it to the // generic level. void MIDebugSession::raiseEvent(event_t e) { if (e == program_exited || e == debugger_exited) { m_stateReloadInProgress = false; } if (e == program_state_changed) { m_stateReloadInProgress = true; qCDebug(DEBUGGERCOMMON) << "State reload in progress\n"; } IDebugSession::raiseEvent(e); if (e == program_state_changed) { m_stateReloadInProgress = false; } } bool KDevMI::MIDebugSession::hasCrashed() const { return m_hasCrashed; } void MIDebugSession::slotDebuggerReady() { Q_ASSERT(m_debugger); m_stateReloadInProgress = false; executeCmd(); if (m_debugger->isReady()) { /* There is nothing in the command queue and no command is currently executing. */ if (debuggerStateIsOn(s_automaticContinue)) { if (!debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Posting automatic continue"; addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); } setDebuggerStateOff(s_automaticContinue); return; } if (m_stateReloadNeeded && !debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Finishing program stop"; // Set to false right now, so that if 'actOnProgramPauseMI_part2' // sends some commands, we won't call it again when handling replies // from that commands. m_stateReloadNeeded = false; reloadProgramState(); } qCDebug(DEBUGGERCOMMON) << "No more commands"; setDebuggerStateOff(s_dbgBusy); raiseEvent(debugger_ready); } } void MIDebugSession::slotDebuggerExited(bool abnormal, const QString &msg) { /* Technically speaking, GDB is likely not to kill the application, and we should have some backup mechanism to make sure the application is killed by KDevelop. But even if application stays around, we no longer can control it in any way, so mark it as exited. */ setDebuggerStateOn(s_appNotStarted); setDebuggerStateOn(s_dbgNotStarted); setDebuggerStateOn(s_programExited); setDebuggerStateOff(s_shuttingDown); if (!msg.isEmpty()) emit showMessage(msg, 3000); if (abnormal) { /* The error is reported to user in MIDebugger now. KMessageBox::information( KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Debugger exited abnormally" "

    This is likely a bug in GDB. " "Examine the gdb output window and then stop the debugger"), i18n("Debugger exited abnormally")); */ // FIXME: not sure if the following still applies. // Note: we don't stop the debugger here, becuse that will hide gdb // window and prevent the user from finding the exact reason of the // problem. } /* FIXME: raiseEvent is handled across multiple places where we explicitly * stop/kill the debugger, a better way is to let the debugger itself report * its exited event. */ // raiseEvent(debugger_exited); } void MIDebugSession::slotInferiorStopped(const MI::AsyncRecord& r) { /* By default, reload all state on program stop. */ m_stateReloadNeeded = true; setDebuggerStateOff(s_appRunning); setDebuggerStateOff(s_dbgNotListening); QString reason; if (r.hasField(QStringLiteral("reason"))) reason = r[QStringLiteral("reason")].literal(); if (reason == QLatin1String("exited-normally") || reason == QLatin1String("exited")) { if (r.hasField(QStringLiteral("exit-code"))) { programNoApp(i18n("Exited with return code: %1", r["exit-code"].literal())); } else { programNoApp(i18n("Exited normally")); } m_stateReloadNeeded = false; return; } if (reason == QLatin1String("exited-signalled")) { programNoApp(i18n("Exited on signal %1", r["signal-name"].literal())); m_stateReloadNeeded = false; return; } if (reason == QLatin1String("watchpoint-scope")) { QString number = r[QStringLiteral("wpnum")].literal(); // FIXME: shuld remove this watchpoint // But first, we should consider if removing all // watchpoinst on program exit is the right thing to // do. addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); m_stateReloadNeeded = false; return; } bool wasInterrupt = false; if (reason == QLatin1String("signal-received")) { QString name = r[QStringLiteral("signal-name")].literal(); QString user_name = r[QStringLiteral("signal-meaning")].literal(); // SIGINT is a "break into running program". // We do this when the user set/mod/clears a breakpoint but the // application is running. // And the user does this to stop the program also. if (name == QLatin1String("SIGINT") && debuggerStateIsOn(s_interruptSent)) { wasInterrupt = true; } else { // Whenever we have a signal raised then tell the user, but don't // end the program as we want to allow the user to look at why the // program has a signal that's caused the prog to stop. // Continuing from SIG FPE/SEGV will cause a "Cannot ..." and // that'll end the program. programFinished(i18n("Program received signal %1 (%2)", name, user_name)); m_hasCrashed = true; } } if (!reason.contains(QLatin1String("exited"))) { // FIXME: we should immediately update the current thread and // frame in the framestackmodel, so that any user actions // are in that thread. However, the way current framestack model // is implemented, we can't change thread id until we refresh // the entire list of threads -- otherwise we might set a thread // id that is not already in the list, and it will be upset. //Indicates if program state should be reloaded immediately. bool updateState = false; if (r.hasField(QStringLiteral("frame"))) { const MI::Value& frame = r[QStringLiteral("frame")]; QString file, line, addr; - if (frame.hasField(QStringLiteral("fullname"))) file = frame[QStringLiteral("fullname")].literal();; + if (frame.hasField(QStringLiteral("fullname"))) file = frame[QStringLiteral("fullname")].literal(); if (frame.hasField(QStringLiteral("line"))) line = frame[QStringLiteral("line")].literal(); if (frame.hasField(QStringLiteral("addr"))) addr = frame[QStringLiteral("addr")].literal(); // gdb counts lines from 1 and we don't setCurrentPosition(QUrl::fromLocalFile(file), line.toInt() - 1, addr); updateState = true; } if (updateState) { reloadProgramState(); } } setDebuggerStateOff(s_interruptSent); if (!wasInterrupt) setDebuggerStateOff(s_automaticContinue); } void MIDebugSession::slotInferiorRunning() { setDebuggerStateOn(s_appRunning); raiseEvent(program_running); if (m_commandQueue->haveImmediateCommand() || (m_debugger->currentCommand() && (m_debugger->currentCommand()->flags() & (CmdImmediately | CmdInterrupt)))) { ensureDebuggerListening(); } else { setDebuggerStateOn(s_dbgNotListening); } } void MIDebugSession::processNotification(const MI::AsyncRecord & async) { if (async.reason == QLatin1String("thread-group-started")) { setDebuggerStateOff(s_appNotStarted | s_programExited); } else if (async.reason == QLatin1String("thread-group-exited")) { setDebuggerStateOn(s_programExited); } else if (async.reason == QLatin1String("library-loaded")) { // do nothing } else if (async.reason == QLatin1String("breakpoint-created")) { breakpointController()->notifyBreakpointCreated(async); } else if (async.reason == QLatin1String("breakpoint-modified")) { breakpointController()->notifyBreakpointModified(async); } else if (async.reason == QLatin1String("breakpoint-deleted")) { breakpointController()->notifyBreakpointDeleted(async); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled notification: " << async.reason; } } void MIDebugSession::reloadProgramState() { raiseEvent(program_state_changed); m_stateReloadNeeded = false; } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::programNoApp(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (m_debuggerState & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); programFinished(msg); } void MIDebugSession::programFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } void MIDebugSession::explainDebuggerStatus() { MICommand* currentCmd_ = m_debugger->currentCommand(); QString information = i18np("1 command in queue\n", "%1 commands in queue\n", m_commandQueue->count()) + i18ncp("Only the 0 and 1 cases need to be translated", "1 command being processed by gdb\n", "%1 commands being processed by gdb\n", (currentCmd_ ? 1 : 0)) + i18n("Debugger state: %1\n", m_debuggerState); if (currentCmd_) { QString extra = i18n("Current command class: '%1'\n" "Current command text: '%2'\n" "Current command original text: '%3'\n", typeid(*currentCmd_).name(), currentCmd_->cmdToSend(), currentCmd_->initialString()); information += extra; } KMessageBox::information(qApp->activeWindow(), information, i18n("Debugger status")); } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::handleNoInferior(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (debuggerState() & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); handleInferiorFinished(msg); } void MIDebugSession::handleInferiorFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } // FIXME: connect to debugger's slot. void MIDebugSession::defaultErrorHandler(const MI::ResultRecord& result) { QString msg = result[QStringLiteral("msg")].literal(); if (msg.contains(QLatin1String("No such process"))) { setDebuggerState(s_appNotStarted|s_programExited); raiseEvent(program_exited); return; } KMessageBox::information( qApp->activeWindow(), i18n("Debugger error" "

    Debugger reported the following error:" "

    %1", result["msg"].literal()), i18n("Debugger error")); // Error most likely means that some change made in GUI // was not communicated to the gdb, so GUI is now not // in sync with gdb. Resync it. // // Another approach is to make each widget reload it content // on errors from commands that it sent, but that's too complex. // Errors are supposed to happen rarely, so full reload on error // is not a big deal. Well, maybe except for memory view, but // it's no auto-reloaded anyway. // // Also, don't reload state on errors appeared during state // reloading! if (!m_debugger->currentCommand()->stateReloading()) raiseEvent(program_state_changed); } void MIDebugSession::setSourceInitFile(bool enable) { m_sourceInitFile = enable; } diff --git a/plugins/debuggercommon/mivariablecontroller.cpp b/plugins/debuggercommon/mivariablecontroller.cpp index 638d9e6aac..308dd098bd 100644 --- a/plugins/debuggercommon/mivariablecontroller.cpp +++ b/plugins/debuggercommon/mivariablecontroller.cpp @@ -1,257 +1,259 @@ /* * GDB 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 "mivariablecontroller.h" #include "debuglog.h" #include "midebugsession.h" #include "mivariable.h" #include "mi/mi.h" #include "mi/micommand.h" #include "stringhelpers.h" #include #include #include #include #include #include using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; using KTextEditor::Cursor; using KTextEditor::Document; using KTextEditor::Range; MIVariableController::MIVariableController(MIDebugSession *parent) : IVariableController(parent) { Q_ASSERT(parent); connect(parent, &MIDebugSession::inferiorStopped, this, &MIVariableController::programStopped); connect(parent, &MIDebugSession::stateChanged, this, &MIVariableController::stateChanged); } MIDebugSession *MIVariableController::debugSession() const { return static_cast(const_cast(QObject::parent())); } void MIVariableController::programStopped(const AsyncRecord& r) { if (debugSession()->debuggerStateIsOn(s_shuttingDown)) return; if (r.hasField(QStringLiteral("reason")) && r[QStringLiteral("reason")].literal() == QLatin1String("function-finished") && r.hasField(QStringLiteral("gdb-result-var"))) { variableCollection()->watches()->addFinishResult(r[QStringLiteral("gdb-result-var")].literal()); } else { variableCollection()->watches()->removeFinishResult(); } } void MIVariableController::update() { qCDebug(DEBUGGERCOMMON) << "autoUpdate =" << autoUpdate(); if (autoUpdate() & UpdateWatches) { variableCollection()->watches()->reinstall(); } if (autoUpdate() & UpdateLocals) { updateLocals(); } if ((autoUpdate() & UpdateLocals) || ((autoUpdate() & UpdateWatches) && variableCollection()->watches()->childCount() > 0)) { debugSession()->addCommand(VarUpdate, QStringLiteral("--all-values *"), this, &MIVariableController::handleVarUpdate); } } void MIVariableController::handleVarUpdate(const ResultRecord& r) { const Value& changed = r[QStringLiteral("changelist")]; for (int i = 0; i < changed.size(); ++i) { const Value& var = changed[i]; MIVariable* v = debugSession()->findVariableByVarobjName(var[QStringLiteral("name")].literal()); // v can be NULL here if we've already removed locals after step, // but the corresponding -var-delete command is still in the queue. if (v) { v->handleUpdate(var); } } } class StackListArgumentsHandler : public MICommandHandler { public: explicit StackListArgumentsHandler(QStringList localsName) : m_localsName(localsName) {} void handle(const ResultRecord &r) override { if (!KDevelop::ICore::self()->debugController()) return; //happens on shutdown if (r.hasField(QStringLiteral("stack-args")) && r[QStringLiteral("stack-args")].size() > 0) { const Value& locals = r[QStringLiteral("stack-args")][0][QStringLiteral("args")]; + m_localsName.reserve(m_localsName.size() + locals.size()); for (int i = 0; i < locals.size(); i++) { m_localsName << locals[i].literal(); } QList variables = KDevelop::ICore::self()->debugController()->variableCollection() ->locals()->updateLocals(m_localsName); foreach (Variable *v, variables) { v->attachMaybe(); } } } private: QStringList m_localsName; }; class StackListLocalsHandler : public MICommandHandler { public: explicit StackListLocalsHandler(MIDebugSession *session) : m_session(session) {} void handle(const ResultRecord &r) override { if (r.hasField(QStringLiteral("locals"))) { const Value& locals = r[QStringLiteral("locals")]; QStringList localsName; + localsName.reserve(locals.size()); for (int i = 0; i < locals.size(); i++) { const Value& var = locals[i]; localsName << var[QStringLiteral("name")].literal(); } int frame = m_session->frameStackModel()->currentFrame(); m_session->addCommand(StackListArguments, // do not show value, low-frame, high-frame QStringLiteral("0 %1 %2").arg(frame).arg(frame), new StackListArgumentsHandler(localsName)); } } private: MIDebugSession *m_session; }; void MIVariableController::updateLocals() { debugSession()->addCommand(StackListLocals, QStringLiteral("--simple-values"), new StackListLocalsHandler(debugSession())); } Range MIVariableController::expressionRangeUnderCursor(Document* doc, const Cursor& cursor) { QString line = doc->line(cursor.line()); int index = cursor.column(); QChar c = line[index]; if (!c.isLetterOrNumber() && c != '_') return {}; int start = Utils::expressionAt(line, index+1); int end = index; for (; end < line.size(); ++end) { QChar c = line[end]; if (!(c.isLetterOrNumber() || c == '_')) break; } if (!(start < end)) return {}; return { Cursor{cursor.line(), start}, Cursor{cursor.line(), end} }; } void MIVariableController::addWatch(KDevelop::Variable* variable) { // FIXME: should add async 'get full expression' method // to MIVariable, not poke at varobj. In that case, // we will be able to make addWatch a generic method, not // gdb-specific one. if (MIVariable *gv = dynamic_cast(variable)) { debugSession()->addCommand(VarInfoPathExpression, gv->varobj(), this, &MIVariableController::addWatch); } } void MIVariableController::addWatchpoint(KDevelop::Variable* variable) { // FIXME: should add async 'get full expression' method // to MIVariable, not poke at varobj. In that case, // we will be able to make addWatchpoint a generic method, not // gdb-specific one. if (MIVariable *gv = dynamic_cast(variable)) { debugSession()->addCommand(VarInfoPathExpression, gv->varobj(), this, &MIVariableController::addWatchpoint); } } void MIVariableController::addWatch(const ResultRecord& r) { if (r.reason == QLatin1String("done") && r.hasField(QStringLiteral("path_expr")) && !r[QStringLiteral("path_expr")].literal().isEmpty()) { variableCollection()->watches()->add(r[QStringLiteral("path_expr")].literal()); } } void MIVariableController::addWatchpoint(const ResultRecord& r) { if (r.reason == QLatin1String("done") && !r[QStringLiteral("path_expr")].literal().isEmpty()) { KDevelop::ICore::self()->debugController()->breakpointModel()->addWatchpoint(r[QStringLiteral("path_expr")].literal()); } } Variable* MIVariableController::createVariable(TreeModel* model, TreeItem* parent, const QString& expression, const QString& display) { return new MIVariable(debugSession(), model, parent, expression, display); } void MIVariableController::handleEvent(IDebugSession::event_t event) { IVariableController::handleEvent(event); } void MIVariableController::stateChanged(IDebugSession::DebuggerState state) { if (state == IDebugSession::EndedState) { debugSession()->markAllVariableDead(); } } diff --git a/plugins/debuggercommon/registers/registercontroller.cpp b/plugins/debuggercommon/registers/registercontroller.cpp index dbdba155ec..ac4da69a9f 100644 --- a/plugins/debuggercommon/registers/registercontroller.cpp +++ b/plugins/debuggercommon/registers/registercontroller.cpp @@ -1,406 +1,405 @@ /* * Class to fetch/change/send registers to the debugger. * Copyright 2013 Vlas Puhov * * 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 "registercontroller.h" #include "converters.h" #include "debuglog.h" #include "midebugsession.h" #include "mi/mi.h" #include "mi/micommand.h" #include #include using namespace KDevMI::MI; using namespace KDevMI; void IRegisterController::setSession(MIDebugSession* debugSession) { m_debugSession = debugSession; } void IRegisterController::updateRegisters(const GroupsName& group) { if (!m_debugSession || m_debugSession->debuggerStateIsOn(s_dbgNotStarted | s_shuttingDown)) { return; } if (m_pendingGroups.contains(group)) { qCDebug(DEBUGGERCOMMON) << "Already updating " << group.name(); return; } if (group.name().isEmpty()) { foreach (const GroupsName & g, namesOfRegisterGroups()) { IRegisterController::updateRegisters(g); } return; } else { qCDebug(DEBUGGERCOMMON) << "Updating: " << group.name(); m_pendingGroups << group; } QString registers; Format currentFormat = formats(group).first(); switch (currentFormat) { case Binary: registers = QStringLiteral("t "); break; case Octal: registers = QStringLiteral("o "); break; case Decimal : registers = QStringLiteral("d "); break; case Hexadecimal: registers = QStringLiteral("x "); break; case Raw: registers = QStringLiteral("r "); break; case Unsigned: registers = QStringLiteral("u "); break; default: break; } //float point registers have only two reasonable format. Mode currentMode = modes(group).first(); if (((currentMode >= v4_float && currentMode <= v2_double) || (currentMode >= f32 && currentMode <= f64) || group.type() == floatPoint) && currentFormat != Raw) { registers = QStringLiteral("N "); } if (group.type() == flag) { registers += numberForName(group.flagName()); } else { foreach (const QString & name, registerNamesForGroup(group)) { registers += numberForName(name) + ' '; } } //Not initialized yet. They'll be updated afterwards. if (registers.contains(QLatin1String("-1"))) { qCDebug(DEBUGGERCOMMON) << "Will update later"; m_pendingGroups.clear(); return; } void (IRegisterController::* handler)(const ResultRecord&); if (group.type() == structured && currentFormat != Raw) { handler = &IRegisterController::structuredRegistersHandler; } else { handler = &IRegisterController::generalRegistersHandler; } m_debugSession->addCommand(DataListRegisterValues, registers, this, handler); } void IRegisterController::registerNamesHandler(const ResultRecord& r) { const Value& names = r[QStringLiteral("register-names")]; m_rawRegisterNames.clear(); for (int i = 0; i < names.size(); ++i) { const Value& entry = names[i]; m_rawRegisterNames.push_back(entry.literal()); } //When here probably request for updating registers was sent, but m_rawRegisterNames were not initialized yet, so it wasn't successful. Update everything once again. updateRegisters(); } void IRegisterController::generalRegistersHandler(const ResultRecord& r) { Q_ASSERT(!m_rawRegisterNames.isEmpty()); QString registerName; const Value& values = r[QStringLiteral("register-values")]; for (int i = 0; i < values.size(); ++i) { const Value& entry = values[i]; int number = entry[QStringLiteral("number")].literal().toInt(); Q_ASSERT(m_rawRegisterNames.size() > number); if (!m_rawRegisterNames[number].isEmpty()) { if (registerName.isEmpty()) { registerName = m_rawRegisterNames[number]; } const QString value = entry[QStringLiteral("value")].literal(); m_registers.insert(m_rawRegisterNames[number], value); } } GroupsName group = groupForRegisterName(registerName); if (m_pendingGroups.contains(group)) { emit registersChanged(registersFromGroup(group)); m_pendingGroups.remove(m_pendingGroups.indexOf(group)); } } void IRegisterController::setRegisterValue(const Register& reg) { Q_ASSERT(!m_registers.isEmpty()); const GroupsName group = groupForRegisterName(reg.name); if (!group.name().isEmpty()) { setRegisterValueForGroup(group, reg); } } QString IRegisterController::registerValue(const QString& name) const { QString value; if (!name.isEmpty()) { if (m_registers.contains(name)) { value = m_registers.value(name); } } return value; } bool IRegisterController::initializeRegisters() { if (!m_debugSession || m_debugSession->debuggerStateIsOn(s_dbgNotStarted | s_shuttingDown)) { return false; } m_debugSession->addCommand(DataListRegisterNames, QString(), this, &IRegisterController::registerNamesHandler); return true; } GroupsName IRegisterController::groupForRegisterName(const QString& name) const { foreach (const GroupsName & group, namesOfRegisterGroups()) { const QStringList registersInGroup = registerNamesForGroup(group); if (group.flagName() == name) { return group; } foreach (const QString & n, registersInGroup) { if (n == name) { return group; } } } return GroupsName(); } void IRegisterController::updateValuesForRegisters(RegistersGroup* registers) const { Q_ASSERT(!m_registers.isEmpty()); for (int i = 0; i < registers->registers.size(); i++) { if (m_registers.contains(registers->registers[i].name)) { registers->registers[i].value = m_registers.value(registers->registers[i].name); } } } void IRegisterController::setFlagRegister(const Register& reg, const FlagRegister& flag) { quint32 flagsValue = registerValue(flag.registerName).toUInt(nullptr, 16); const int idx = flag.flags.indexOf(reg.name); if (idx != -1) { flagsValue ^= static_cast(qPow(2, flag.bits[idx].toUInt())); setGeneralRegister(Register(flag.registerName, QStringLiteral("0x%1").arg(flagsValue, 0, 16)), flag.groupName); } else { updateRegisters(flag.groupName); qCDebug(DEBUGGERCOMMON) << reg.name << ' ' << reg.value << "is incorrect flag name/value"; } } void IRegisterController::setGeneralRegister(const Register& reg, const GroupsName& group) { if (!m_debugSession || m_debugSession->debuggerStateIsOn(s_dbgNotStarted | s_shuttingDown)) { return; } const QString command = QStringLiteral("set var $%1=%2").arg(reg.name, reg.value); qCDebug(DEBUGGERCOMMON) << "Setting register: " << command; m_debugSession->addCommand(NonMI, command); updateRegisters(group); } IRegisterController::IRegisterController(MIDebugSession* debugSession, QObject* parent) : QObject(parent), m_debugSession(debugSession) {} IRegisterController::~IRegisterController() {} void IRegisterController::updateFlagValues(RegistersGroup* flagsGroup, const FlagRegister& flagRegister) const { const quint32 flagsValue = registerValue(flagRegister.registerName).toUInt(nullptr, 16); for (int idx = 0; idx < flagRegister.flags.count(); idx++) { flagsGroup->registers[idx].value = ((flagsValue >> flagRegister.bits[idx].toInt()) & 1) ? "1" : "0"; } } QVector IRegisterController::formats(const GroupsName& group) { int idx = -1; foreach (const GroupsName & g, namesOfRegisterGroups()) { if (g == group) { idx = g.index(); } } Q_ASSERT(idx != -1); return m_formatsModes[idx].formats; } GroupsName IRegisterController::createGroupName(const QString& name, int idx, RegisterType t, const QString& flag) const { return GroupsName(name, idx, t, flag); } void IRegisterController::setFormat(Format f, const GroupsName& group) { foreach (const GroupsName & g, namesOfRegisterGroups()) { if (g == group) { int i = m_formatsModes[g.index()].formats.indexOf(f); if (i != -1) { m_formatsModes[g.index()].formats.remove(i); m_formatsModes[g.index()].formats.prepend(f); } } } } QString IRegisterController::numberForName(const QString& name) const { //Requests for number come in order(if the previous was, let's say 10, then most likely the next one will be 11) static int previousNumber = -1; if (m_rawRegisterNames.isEmpty()) { previousNumber = -1; return QString::number(previousNumber); } if (previousNumber != -1 && m_rawRegisterNames.size() > ++previousNumber) { if (m_rawRegisterNames[previousNumber] == name) { return QString::number(previousNumber); } } for (int number = 0; number < m_rawRegisterNames.size(); number++) { if (name == m_rawRegisterNames[number]) { previousNumber = number; return QString::number(number); } } previousNumber = -1; return QString::number(previousNumber); } void IRegisterController::setStructuredRegister(const Register& reg, const GroupsName& group) { Register r = reg; r.value = r.value.trimmed(); r.value.replace(' ', ','); if (r.value.contains(',')) { - r.value.append('}'); - r.value.prepend('{'); + r.value = '{' + r.value + '}'; } r.name += '.' + Converters::modeToString(m_formatsModes[group.index()].modes.first()); setGeneralRegister(r, group); } void IRegisterController::structuredRegistersHandler(const ResultRecord& r) { //Parsing records in format like: //{u8 = {0, 0, 128, 146, 0, 48, 197, 65}, u16 = {0, 37504, 12288, 16837}, u32 = {2457862144, 1103441920}, u64 = 4739246961893310464, f32 = {-8.07793567e-28, 24.6484375}, f64 = 710934821} //{u8 = {0 }, u16 = {0, 0, 0, 0, 0, 0, 0, 0}, u32 = {0, 0, 0, 0}, u64 = {0, 0}, f32 = {0, 0, 0, 0}, f64 = {0, 0}} QRegExp rx("^\\s*=\\s*\\{(.*)\\}"); rx.setMinimal(true); QString registerName; Mode currentMode = LAST_MODE; GroupsName group; const Value& values = r[QStringLiteral("register-values")]; Q_ASSERT(!m_rawRegisterNames.isEmpty()); for (int i = 0; i < values.size(); ++i) { const Value& entry = values[i]; int number = entry[QStringLiteral("number")].literal().toInt(); registerName = m_rawRegisterNames[number]; if (currentMode == LAST_MODE) { group = groupForRegisterName(registerName); currentMode = modes(group).first(); } QString record = entry[QStringLiteral("value")].literal(); int start = record.indexOf(Converters::modeToString(currentMode)); Q_ASSERT(start != -1); start += Converters::modeToString(currentMode).size(); QString value = record.right(record.size() - start); int idx = rx.indexIn(value); value = rx.cap(1); if (idx == -1) { //if here then value without braces: u64 = 4739246961893310464, f32 = {-8.07793567e-28, 24.6484375}, f64 = 710934821} QRegExp rx2("=\\s+(.*)(\\}|,)"); rx2.setMinimal(true); rx2.indexIn(record, start); value = rx2.cap(1); } value = value.trimmed().remove(','); m_registers.insert(registerName, value); } if (m_pendingGroups.contains(group)) { emit registersChanged(registersFromGroup(group)); m_pendingGroups.remove(m_pendingGroups.indexOf(group)); } } QVector< Mode > IRegisterController::modes(const GroupsName& group) { int idx = -1; foreach (const GroupsName & g, namesOfRegisterGroups()) { if (g == group) { idx = g.index(); } } Q_ASSERT(idx != -1); return m_formatsModes[idx].modes; } void IRegisterController::setMode(Mode m, const GroupsName& group) { foreach (const GroupsName & g, namesOfRegisterGroups()) { if (g == group) { int i = m_formatsModes[g.index()].modes.indexOf(m); if (i != -1) { m_formatsModes[g.index()].modes.remove(i); m_formatsModes[g.index()].modes.prepend(m); } } } } diff --git a/plugins/debuggercommon/registers/registercontroller_arm.cpp b/plugins/debuggercommon/registers/registercontroller_arm.cpp index 9de6332f77..d01af64fec 100644 --- a/plugins/debuggercommon/registers/registercontroller_arm.cpp +++ b/plugins/debuggercommon/registers/registercontroller_arm.cpp @@ -1,181 +1,194 @@ /* * Class to fetch/change/send registers to the debugger for arm architecture. * Copyright 2013 Vlas Puhov * * 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 "registercontroller_arm.h" #include "debuglog.h" #include using namespace KDevMI; QVector RegisterController_Arm::m_registerNames; FlagRegister RegisterController_Arm::m_cpsr; void RegisterController_Arm::updateValuesForRegisters(RegistersGroup* registers) const { qCDebug(DEBUGGERCOMMON) << "Updating values for registers: " << registers->groupName.name(); if (registers->groupName == enumToGroupName(Flags)) { updateFlagValues(registers, m_cpsr); } else { IRegisterController::updateValuesForRegisters(registers); } } RegistersGroup RegisterController_Arm::registersFromGroup(const GroupsName& group) const { RegistersGroup registers; registers.groupName = group; registers.format = m_formatsModes[group.index()].formats.first(); - foreach (const QString & name, registerNamesForGroup(group)) { + const auto registerNames = registerNamesForGroup(group); + registers.registers.reserve(registerNames.size()); + for (const auto& name : registerNames) { registers.registers.append(Register(name, QString())); } updateValuesForRegisters(®isters); return registers; } QVector RegisterController_Arm::namesOfRegisterGroups() const { - static const QVector registerGroups = QVector() << enumToGroupName(General) << enumToGroupName(Flags) << enumToGroupName(VFP_single) << enumToGroupName(VFP_double) << enumToGroupName(VFP_quad); + static const QVector registerGroups = QVector{ + enumToGroupName(General), + enumToGroupName(Flags), + enumToGroupName(VFP_single), + enumToGroupName(VFP_double), + enumToGroupName(VFP_quad), + }; return registerGroups; } void RegisterController_Arm::setRegisterValueForGroup(const GroupsName& group, const Register& reg) { if (group == enumToGroupName(General)) { setGeneralRegister(reg, group); } else if (group == enumToGroupName(Flags)) { setFlagRegister(reg, m_cpsr); } else if (group == enumToGroupName(VFP_single)) { setVFPS_Register(reg); } else if (group == enumToGroupName(VFP_double)) { setVFPD_Register(reg); } else if (group == enumToGroupName(VFP_quad)) { setVFPQ_Register(reg); } } void RegisterController_Arm::setVFPS_Register(const Register& reg) { setGeneralRegister(reg, enumToGroupName(VFP_single)); } void RegisterController_Arm::setVFPD_Register(const Register& reg) { setStructuredRegister(reg, enumToGroupName(VFP_double)); } void RegisterController_Arm::setVFPQ_Register(const Register& reg) { setStructuredRegister(reg, enumToGroupName(VFP_quad)); } void RegisterController_Arm::updateRegisters(const GroupsName& group) { if (!m_registerNamesInitialized) { if (initializeRegisters()) { m_registerNamesInitialized = true; } } IRegisterController::updateRegisters(group); } GroupsName RegisterController_Arm::enumToGroupName(ArmRegisterGroups group) const { static const GroupsName groups[LAST_REGISTER] = { createGroupName(i18n("General"), General) , createGroupName(i18n("Flags"), Flags, flag, m_cpsr.registerName), createGroupName(i18n("VFP single-word"), VFP_single, floatPoint), createGroupName(i18n("VFP double-word"), VFP_double, structured), createGroupName(i18n("VFP quad-word"), VFP_quad, structured)}; return groups[group]; } RegisterController_Arm::RegisterController_Arm(MIDebugSession* debugSession, QObject* parent) : IRegisterController(debugSession, parent) { if (m_registerNames.isEmpty()) { const int registerCount = static_cast(LAST_REGISTER); m_registerNames.resize(registerCount); initRegisterNames(); } m_formatsModes.resize(namesOfRegisterGroups().size()); - m_formatsModes[VFP_double].formats.append(Binary); - m_formatsModes[VFP_double].formats.append(Decimal); - m_formatsModes[VFP_double].formats.append(Hexadecimal); - m_formatsModes[VFP_double].formats.append(Octal); - m_formatsModes[VFP_double].formats.append(Unsigned); - m_formatsModes[VFP_double].modes.append(u32); - m_formatsModes[VFP_double].modes.append(u64); - m_formatsModes[VFP_double].modes.append(f32); - m_formatsModes[VFP_double].modes.append(f64); + m_formatsModes[VFP_double].formats = {Binary, Decimal, Hexadecimal, Octal, Unsigned}; + m_formatsModes[VFP_double].modes = {u32, u64, f32, f64}; m_formatsModes[Flags].formats.append(Raw); m_formatsModes[Flags].modes.append(natural); m_formatsModes[VFP_single].formats.append(Decimal); m_formatsModes[VFP_single].modes.append(natural); m_formatsModes[VFP_quad] = m_formatsModes[VFP_double]; m_formatsModes[General].formats.append(Raw); m_formatsModes[General].formats << m_formatsModes[VFP_double].formats; m_formatsModes[General].modes.append(natural); } void RegisterController_Arm::initRegisterNames() { for (int i = 0; i < 32; i++) { m_registerNames[VFP_single] << ("s" + QString::number(i)); } m_cpsr.registerName = QStringLiteral("cpsr"); - m_cpsr.flags << QStringLiteral("Q") << QStringLiteral("V") << QStringLiteral("C") << QStringLiteral("Z") << QStringLiteral("N"); - m_cpsr.bits << QStringLiteral("27") << QStringLiteral("28") << QStringLiteral("29") << QStringLiteral("30") << QStringLiteral("31"); + m_cpsr.flags = QStringList{ + QStringLiteral("Q"), + QStringLiteral("V"), + QStringLiteral("C"), + QStringLiteral("Z"), + QStringLiteral("N"), + }; + m_cpsr.bits = QStringList{ + QStringLiteral("27"), + QStringLiteral("28"), + QStringLiteral("29"), + QStringLiteral("30"), + QStringLiteral("31"), + }; m_cpsr.groupName = enumToGroupName(Flags); m_registerNames[Flags] = m_cpsr.flags; for (int i = 0; i < 13; i++) { m_registerNames[General] << ("r" + QString::number(i)); } m_registerNames[General] << QStringLiteral("sp") << QStringLiteral("lr") << QStringLiteral("pc"); for (int i = 0; i < 32; i++) { m_registerNames[VFP_double] << ("d" + QString::number(i)); } for (int i = 0; i < 16; i++) { m_registerNames[VFP_quad] << ("q" + QString::number(i)); } } QStringList RegisterController_Arm::registerNamesForGroup(const GroupsName& group) const { for (int i = 0; i < static_cast(LAST_REGISTER); i++) { if (group == enumToGroupName(static_cast(i))) { return m_registerNames[i]; } } return QStringList(); } diff --git a/plugins/debuggercommon/registers/registercontroller_x86.cpp b/plugins/debuggercommon/registers/registercontroller_x86.cpp index 43b5ddb819..94ff13f59d 100644 --- a/plugins/debuggercommon/registers/registercontroller_x86.cpp +++ b/plugins/debuggercommon/registers/registercontroller_x86.cpp @@ -1,239 +1,256 @@ /* * Class to fetch/change/send registers to the debugger for x86, x86_64 architectures. * Copyright 2013 Vlas Puhov * * 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 "registercontroller_x86.h" #include "debuglog.h" #include using namespace KDevMI; QVector RegisterControllerGeneral_x86::m_registerNames; FlagRegister RegisterControllerGeneral_x86::m_eflags; void RegisterControllerGeneral_x86::updateValuesForRegisters(RegistersGroup* registers) const { qCDebug(DEBUGGERCOMMON) << "Updating values for registers: " << registers->groupName.name(); if (registers->groupName == enumToGroupName(Flags)) { updateFlagValues(registers, m_eflags); } else { IRegisterController::updateValuesForRegisters(registers); } } RegistersGroup RegisterControllerGeneral_x86::registersFromGroup(const GroupsName& group) const { RegistersGroup registers; registers.groupName = group; registers.format = m_formatsModes[group.index()].formats.first(); - foreach (const QString & name, registerNamesForGroup(group)) { + const auto registerNames = registerNamesForGroup(group); + registers.registers.reserve(registerNames.size()); + for (const auto& name : registerNames) { registers.registers.append(Register(name, QString())); } updateValuesForRegisters(®isters); return registers; } QVector RegisterControllerGeneral_x86::namesOfRegisterGroups() const { - static const QVector registerGroups = QVector() << enumToGroupName(General) << enumToGroupName(Flags) << enumToGroupName(FPU) << enumToGroupName(XMM) << enumToGroupName(Segment); + static const QVector registerGroups = QVector{ + enumToGroupName(General), + enumToGroupName(Flags), + enumToGroupName(FPU), + enumToGroupName(XMM), + enumToGroupName(Segment), + }; return registerGroups; } void RegisterControllerGeneral_x86::setRegisterValueForGroup(const GroupsName& group, const Register& reg) { if (group == enumToGroupName(General)) { setGeneralRegister(reg, group); } else if (group == enumToGroupName(Flags)) { setFlagRegister(reg, m_eflags); } else if (group == enumToGroupName(FPU)) { setFPURegister(reg); } else if (group == enumToGroupName(XMM)) { setXMMRegister(reg); } else if (group == enumToGroupName(Segment)) { setSegmentRegister(reg); } } void RegisterControllerGeneral_x86::setFPURegister(const Register& reg) { setGeneralRegister(reg, enumToGroupName(FPU)); } void RegisterControllerGeneral_x86::setXMMRegister(const Register& reg) { setStructuredRegister(reg, enumToGroupName(XMM)); } void RegisterControllerGeneral_x86::setSegmentRegister(const Register& reg) { setGeneralRegister(reg, enumToGroupName(Segment)); } void RegisterControllerGeneral_x86::updateRegisters(const GroupsName& group) { if (!m_registerNamesInitialized) { if (initializeRegisters()) { m_registerNamesInitialized = true; } } IRegisterController::updateRegisters(group); } GroupsName RegisterControllerGeneral_x86::enumToGroupName(X86RegisterGroups group) const { static const GroupsName groups[LAST_REGISTER] = { createGroupName(i18n("General"), General), createGroupName(i18n("Flags"), Flags, flag, m_eflags.registerName), createGroupName(i18n("FPU"), FPU, floatPoint), createGroupName(i18n("XMM"), XMM, structured), createGroupName(i18n("Segment"), Segment)}; return groups[group]; } RegisterController_x86::RegisterController_x86(MIDebugSession* debugSession, QObject* parent) : RegisterControllerGeneral_x86(debugSession, parent) { initRegisterNames(); } RegisterController_x86_64::RegisterController_x86_64(MIDebugSession* debugSession, QObject* parent) : RegisterControllerGeneral_x86(debugSession, parent) { initRegisterNames(); } RegisterControllerGeneral_x86::RegisterControllerGeneral_x86(MIDebugSession* debugSession, QObject* parent) : IRegisterController(debugSession, parent) { if (m_registerNames.isEmpty()) { const int registerCount = static_cast(LAST_REGISTER); m_registerNames.resize(registerCount); initRegisterNames(); } m_formatsModes.resize(namesOfRegisterGroups().size()); - m_formatsModes[XMM].formats.append(Binary); - m_formatsModes[XMM].formats.append(Decimal); - m_formatsModes[XMM].formats.append(Hexadecimal); - m_formatsModes[XMM].formats.append(Octal); - m_formatsModes[XMM].formats.append(Unsigned); - m_formatsModes[XMM].modes.append(v4_float); - m_formatsModes[XMM].modes.append(v2_double); - m_formatsModes[XMM].modes.append(v4_int32); - m_formatsModes[XMM].modes.append(v2_int64); + m_formatsModes[XMM].formats = {Binary, Decimal, Hexadecimal, Octal, Unsigned}; + m_formatsModes[XMM].modes = {v4_float, v2_double, v4_int32, v2_int64}; m_formatsModes[Flags].formats.append(Raw); m_formatsModes[Flags].modes.append(natural); m_formatsModes[FPU].formats.append(Decimal); m_formatsModes[FPU].modes.append(natural); m_formatsModes[General].modes.append(natural); m_formatsModes[General].formats.append(Raw); m_formatsModes[General].formats << m_formatsModes[XMM].formats; m_formatsModes[Segment] = m_formatsModes[General]; } void RegisterControllerGeneral_x86::initRegisterNames() { for (int i = 0; i < 8; i++) { m_registerNames[FPU] << ("st" + QString::number(i)); } m_registerNames[Flags] = QStringList{ QStringLiteral("C"), QStringLiteral("P"), QStringLiteral("A"), QStringLiteral("Z"), QStringLiteral("S"), QStringLiteral("T"), QStringLiteral("D"), QStringLiteral("O") }; - m_registerNames[Segment] << QStringLiteral("cs") << QStringLiteral("ss") << QStringLiteral("ds") << QStringLiteral("es") << QStringLiteral("fs") << QStringLiteral("gs"); + m_registerNames[Segment] = QStringList{ + QStringLiteral("cs"), + QStringLiteral("ss"), + QStringLiteral("ds"), + QStringLiteral("es"), + QStringLiteral("fs"), + QStringLiteral("gs"), + }; m_eflags.flags = m_registerNames[Flags]; - m_eflags.bits << QStringLiteral("0") << QStringLiteral("2") << QStringLiteral("4") << QStringLiteral("6") << QStringLiteral("7") << QStringLiteral("8") << QStringLiteral("10") << QStringLiteral("11"); + m_eflags.bits = QStringList{ + QStringLiteral("0"), + QStringLiteral("2"), + QStringLiteral("4"), + QStringLiteral("6"), + QStringLiteral("7"), + QStringLiteral("8"), + QStringLiteral("10"), + QStringLiteral("11"), + }; m_eflags.registerName = QStringLiteral("eflags"); m_eflags.groupName = enumToGroupName(Flags); } void RegisterController_x86::initRegisterNames() { m_registerNames[General] = QStringList{ QStringLiteral("eax"), QStringLiteral("ebx"), QStringLiteral("ecx"), QStringLiteral("edx"), QStringLiteral("esi"), QStringLiteral("edi"), QStringLiteral("ebp"), QStringLiteral("esp"), QStringLiteral("eip") }; m_registerNames[XMM].clear(); for (int i = 0; i < 8; i++) { m_registerNames[XMM] << ("xmm" + QString::number(i)); } } void RegisterController_x86_64::initRegisterNames() { m_registerNames[General] = QStringList{ QStringLiteral("rax"), QStringLiteral("rbx"), QStringLiteral("rcx"), QStringLiteral("rdx"), QStringLiteral("rsi"), QStringLiteral("rdi"), QStringLiteral("rbp"), QStringLiteral("rsp"), QStringLiteral("r8"), QStringLiteral("r9"), QStringLiteral("r10"), QStringLiteral("r11"), QStringLiteral("r12"), QStringLiteral("r13"), QStringLiteral("r14"), QStringLiteral("r15"), QStringLiteral("rip") }; m_registerNames[XMM].clear(); for (int i = 0; i < 16; i++) { m_registerNames[XMM] << ("xmm" + QString::number(i)); } } QStringList RegisterControllerGeneral_x86::registerNamesForGroup(const GroupsName& group) const { for (int i = 0; i < static_cast(LAST_REGISTER); i++) { if (group == enumToGroupName(static_cast(i))) { return m_registerNames[i]; } } return QStringList(); } diff --git a/plugins/debuggercommon/stringhelpers.cpp b/plugins/debuggercommon/stringhelpers.cpp index ed369be3c5..4afb14f491 100644 --- a/plugins/debuggercommon/stringhelpers.cpp +++ b/plugins/debuggercommon/stringhelpers.cpp @@ -1,285 +1,285 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "stringhelpers.h" #include "debuglog.h" #include #include #include #include namespace { bool parenFits( QChar c1, QChar c2 ) { if( c1 == '<' && c2 == '>' ) return true; else if( c1 == '(' && c2 == ')' ) return true; else if( c1 == '[' && c2 == ']' ) return true; else if( c1 == '{' && c2 == '}' ) return true; else return false; } bool isParen( QChar c1 ) { if( c1 == '<' || c1 == '>' ) return true; else if( c1 == '(' || c1 == ')' ) return true; else if( c1 == '[' || c1 == ']' ) return true; else if( c1 == '{' || c1 == '}' ) return true; else return false; } bool isTypeParen( QChar c1 ) { if( c1 == '<' || c1 == '>' ) return true; else return false; } bool isTypeOpenParen( QChar c1 ) { if( c1 == '<' ) return true; else return false; } bool isTypeCloseParen( QChar c1 ) { if( c1 == '>' ) return true; else return false; } bool isLeftParen( QChar c1 ) { if( c1 == '<' ) return true; else if( c1 == '(' ) return true; else if( c1 == '[' ) return true; else if( c1 == '{' ) return true; else return false; } enum { T_ACCESS, T_PAREN, T_BRACKET, T_IDE, T_UNKNOWN, T_TEMP }; } int Utils::expressionAt( const QString& text, int index ) { if( index == 0 ) return 0; int last = T_UNKNOWN; int start = index; --index; while ( index > 0 ) { while ( index > 0 && text[ index ].isSpace() ) { --index; } QChar ch = text[ index ]; QString ch2 = text.mid( index - 1, 2 ); if ( ( last != T_IDE ) && ( ch.isLetterOrNumber() || ch == '_' ) ) { while ( index > 0 && ( text[ index ].isLetterOrNumber() || text[ index ] == '_' ) ) { --index; } last = T_IDE; } else if ( last != T_IDE && ch == ')' ) { int count = 0; while ( index > 0 ) { QChar ch = text[ index ]; if ( ch == '(' ) { ++count; } else if ( ch == ')' ) { --count; } --index; if ( count == 0 ) { //index; last = T_PAREN; break; } } } else if ( last != T_IDE && ch == '>' && ch2 != QLatin1String("->") ) { int count = 0; while ( index > 0 ) { QChar ch = text[ index ]; if ( ch == '<' ) { ++count; } else if ( ch == '>' ) { --count; } else if ( count == 0 ) { //--index; last = T_TEMP; break; } --index; } } else if ( ch == ']' ) { int count = 0; while ( index > 0 ) { QChar ch = text[ index ]; if ( ch == '[' ) { ++count; } else if ( ch == ']' ) { --count; } else if ( count == 0 ) { //--index; last = T_BRACKET; break; } --index; } } else if ( ch == '.' ) { --index; last = T_ACCESS; } else if ( ch2 == QLatin1String("::") ) { index -= 2; last = T_ACCESS; } else if ( ch2 == QLatin1String("->") ) { index -= 2; last = T_ACCESS; } else { if ( start > index ) { ++index; } last = T_UNKNOWN; break; } } ///If we're at the first item, the above algorithm cannot be used safely, ///so just determine whether the sign is valid for the beginning of an expression, if it isn't reject it. if ( index == 0 && start > index && !( text[ index ].isLetterOrNumber() || text[ index ] == '_' || text[ index ] == ':' ) ) { ++index; } return index; } QString Utils::quoteExpression(const QString& expr) { return quote(expr, '"'); } QString Utils::unquoteExpression(const QString& expr) { return unquote(expr, false); } QString Utils::quote(const QString& str, char quoteCh) { QString res = str; - res.replace(QLatin1Char('\\'), QLatin1String("\\\\")).replace(quoteCh, QStringLiteral("\\") + quoteCh); - return res.prepend(quoteCh).append(quoteCh); + res.replace(QLatin1Char('\\'), QLatin1String("\\\\")).replace(quoteCh, QLatin1Char('\\') + quoteCh); + return quoteCh + res + quoteCh; } QString Utils::unquote(const QString &str, bool unescapeUnicode, char quoteCh) { if (str.startsWith(quoteCh) && str.endsWith(quoteCh)) { QString res; res.reserve(str.length()); bool esc = false; int type = 0; QString escSeq; escSeq.reserve(4); // skip beginning and ending quoteCh, no need for str = str.mid(1, str.length() - 2) for (int i = 1; i != str.length() - 1; i++) { auto ch = str[i]; if (esc) { switch (ch.unicode()) { case '\\': if (type != 0) { escSeq += ch; qCDebug(DEBUGGERCOMMON) << "Unrecognized escape sequence:" << escSeq; res += '\\'; res += escSeq; escSeq.clear(); esc = false; type = 0; } else { res.append('\\'); // escSeq.clear(); // escSeq must be empty. esc = false; } break; case 'u': case 'x': if (type != 0 || !unescapeUnicode) { escSeq += ch; qCDebug(DEBUGGERCOMMON) << "Unrecognized escape sequence:" << escSeq; res += '\\'; res += escSeq; escSeq.clear(); esc = false; type = 0; } else { type = ch == 'u' ? 1 : 2; } break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': escSeq += ch; if (type == 0) { qCDebug(DEBUGGERCOMMON) << "Unrecognized escape sequence:" << escSeq; res += '\\'; res += escSeq; escSeq.clear(); esc = false; type = 0; } else { // \uNNNN // \xNN if (escSeq.length() == (type == 1 ? 4 : 2)) { // no need to handle error, we know for sure escSeq is '[0-9a-fA-F]+' auto code = escSeq.toInt(nullptr, 16); res += QChar(code); escSeq.clear(); esc = false; type = 0; } } break; default: if (type == 0 && ch == quoteCh) { res += ch; } else { escSeq += ch; qCDebug(DEBUGGERCOMMON) << "Unrecognized escape sequence:" << escSeq; res += '\\'; res += escSeq; escSeq.clear(); } esc = false; type = 0; break; } } else { if (ch == '\\') { esc = true; continue; } res += ch; } } return res; } else { return str; } } diff --git a/plugins/debuggercommon/stty.cpp b/plugins/debuggercommon/stty.cpp index 26455694ec..e6f9cd22c9 100644 --- a/plugins/debuggercommon/stty.cpp +++ b/plugins/debuggercommon/stty.cpp @@ -1,355 +1,363 @@ /*************************************************************************** begin : Mon Sep 13 1999 copyright : (C) 1999 by John Birch email : jbb@kdevelop.org This code was originally written by Judin Maxim, from the KDEStudio project. It was then updated with later code from konsole (KDE). It has also been enhanced with an idea from the code in kdbg written by Johannes Sixt ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif #ifdef __osf__ #define _XOPEN_SOURCE_EXTENDED #define O_NDELAY O_NONBLOCK #endif #include #ifndef _MSC_VER #include #include #include #include #endif #include #ifdef HAVE_SYS_STROPTS_H #include #define _NEW_TTY_CTRL #endif #include #include #ifndef _MSC_VER #include #include #include #endif #include #include #include #include #include #if defined (_HPUX_SOURCE) #define _TERMIOS_INCLUDED #include #endif #include #include #include #include #include #include #include #include #include "stty.h" #include "debuglog.h" #define PTY_FILENO 3 #define BASE_CHOWN "konsole_grantpty" using namespace KDevMI; static int chownpty(int fd, int grant) // param fd: the fd of a master pty. // param grant: 1 to grant, 0 to revoke // returns 1 on success 0 on fail { #ifndef Q_OS_WIN void(*tmp)(int) = signal(SIGCHLD,SIG_DFL); pid_t pid = fork(); if (pid < 0) { signal(SIGCHLD,tmp); return 0; } if (pid == 0) { /* We pass the master pseudo terminal as file descriptor PTY_FILENO. */ if (fd != PTY_FILENO && dup2(fd, PTY_FILENO) < 0) ::exit(1); QString path = QStandardPaths::findExecutable(BASE_CHOWN); execle(QFile::encodeName(path), BASE_CHOWN, grant?"--grant":"--revoke", (void *)nullptr, NULL); ::exit(1); // should not be reached } if (pid > 0) { int w; // retry: int rc = waitpid (pid, &w, 0); if (rc != pid) ::exit(1); // { // signal from other child, behave like catchChild. // // guess this gives quite some control chaos... // Shell* sh = shells.indexOf(rc); // if (sh) { shells.remove(rc); sh->doneShell(w); } // goto retry; // } signal(SIGCHLD,tmp); return (rc != -1 && WIFEXITED(w) && WEXITSTATUS(w) == 0); } signal(SIGCHLD,tmp); #endif return 0; //dummy. } // ************************************************************************** STTY::STTY(bool ext, const QString &termAppName) : QObject(), m_externalTerminal(nullptr), external_(ext) { if (ext) { findExternalTTY(termAppName); } else { fout = findTTY(); if (fout >= 0) { ttySlave = QString(tty_slave); out = new QSocketNotifier(fout, QSocketNotifier::Read, this); connect( out, &QSocketNotifier::activated, this, &STTY::OutReceived ); } } } // ************************************************************************** STTY::~STTY() { #ifndef Q_OS_WIN if (out) { ::close(fout); delete out; } #endif } // ************************************************************************** int STTY::findTTY() { int ptyfd = -1; bool needGrantPty = true; #ifndef Q_OS_WIN // Find a master pty that we can open //////////////////////////////// #ifdef __sgi__ ptyfd = open("/dev/ptmx",O_RDWR); #elif defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) ptyfd = posix_openpt(O_RDWR); #endif #if defined(__sgi__) || defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) if (ptyfd == -1) { perror("Can't open a pseudo teletype"); return(-1); } else if (ptyfd >= 0) { strncpy(tty_slave, ptsname(ptyfd), 50); grantpt(ptyfd); unlockpt(ptyfd); needGrantPty = false; } #endif // first we try UNIX PTY's #if defined(TIOCGPTN) && !defined(Q_OS_FREEBSD) strcpy(pty_master,"/dev/ptmx"); strcpy(tty_slave,"/dev/pts/"); ptyfd = open(pty_master,O_RDWR); if (ptyfd >= 0) { // got the master pty int ptyno; if (ioctl(ptyfd, TIOCGPTN, &ptyno) == 0) { struct stat sbuf; sprintf(tty_slave,"/dev/pts/%d",ptyno); if (stat(tty_slave,&sbuf) == 0 && S_ISCHR(sbuf.st_mode)) needGrantPty = false; else { close(ptyfd); ptyfd = -1; } } else { close(ptyfd); ptyfd = -1; } } #endif #if defined(_SCO_DS) || defined(__USLC__) /* SCO OSr5 and UnixWare */ if (ptyfd < 0) { for (int idx = 0; idx < 256; idx++) { sprintf(pty_master, "/dev/ptyp%d", idx); sprintf(tty_slave, "/dev/ttyp%d", idx); if (access(tty_slave, F_OK) < 0) { idx = 256; break; } if ((ptyfd = open (pty_master, O_RDWR)) >= 0) { if (access (tty_slave, R_OK|W_OK) == 0) break; close(ptyfd); ptyfd = -1; } } } #endif if (ptyfd < 0) { /// \FIXME Linux, Trouble on other systems? for (const char* s3 = "pqrstuvwxyzabcde"; *s3 != 0; s3++) { for (const char* s4 = "0123456789abcdef"; *s4 != 0; s4++) { sprintf(pty_master,"/dev/pty%c%c",*s3,*s4); sprintf(tty_slave,"/dev/tty%c%c",*s3,*s4); if ((ptyfd = open(pty_master, O_RDWR)) >= 0) { if (geteuid() == 0 || access(tty_slave, R_OK|W_OK) == 0) break; close(ptyfd); ptyfd = -1; } } if (ptyfd >= 0) break; } } if (ptyfd >= 0) { if (needGrantPty && !chownpty(ptyfd, true)) { fprintf(stderr,"kdevelop: chownpty failed for device %s::%s.\n",pty_master,tty_slave); fprintf(stderr," : This means the session can be eavesdroped.\n"); fprintf(stderr," : Make sure konsole_grantpty is installed and setuid root.\n"); } ::fcntl(ptyfd, F_SETFL, O_NDELAY); #ifdef TIOCSPTLCK int flag = 0; ioctl(ptyfd, TIOCSPTLCK, &flag); // unlock pty #endif } if (ptyfd==-1) { m_lastError = i18n("Cannot use the tty* or pty* devices.\n" "Check the settings on /dev/tty* and /dev/pty*\n" "As root you may need to \"chmod ug+rw\" tty* and pty* devices " "and/or add the user to the tty group using " "\"usermod -aG tty username\"."); } #endif return ptyfd; } // ************************************************************************** void STTY::OutReceived(int f) { #ifndef Q_OS_WIN char buf[1024]; int n; // read until socket is empty. We shouldn't be receiving a continuous // stream of data, so the loop is unlikely to cause problems. while ((n = ::read(f, buf, sizeof(buf)-1)) > 0) { *(buf+n) = 0; // a standard string QByteArray ba(buf); emit OutOutput(ba); } // Note: for some reason, n can be 0 here. // I can understand that non-blocking read returns 0, // but I don't understand how OutReceived can be even // called when there's no input. if (n == 0 /* eof */ || (n == -1 && errno != EAGAIN)) { // Found eof or error. Disable socket notifier, otherwise Qt // will repeatedly call this method, eating CPU // cycles. out->setEnabled(false); } #endif } void STTY::readRemaining() { if (!external_) OutReceived(fout); } bool STTY::findExternalTTY(const QString& termApp) { #ifndef Q_OS_WIN QString appName(termApp.isEmpty() ? QStringLiteral("xterm") : termApp); if (QStandardPaths::findExecutable(appName).isEmpty()) { m_lastError = i18n("%1 is incorrect terminal name", termApp); return false; } QTemporaryFile file; if (!file.open()) { m_lastError = i18n("Can't create a temporary file"); return false; } m_externalTerminal.reset(new QProcess(this)); if (appName == QLatin1String("konsole")) { - m_externalTerminal->start(appName, QStringList() << QStringLiteral("-e") << QStringLiteral("sh") << QStringLiteral("-c") << "tty>" + file.fileName() + ";exec<&-;exec>&-;while :;do sleep 3600;done"); + m_externalTerminal->start(appName, QStringList{ + QStringLiteral("-e"), + QStringLiteral("sh"), + QStringLiteral("-c"), + QLatin1String("tty>") + file.fileName() + QLatin1String(";exec<&-;exec>&-;while :;do sleep 3600;done")}); } else if (appName == QLatin1String("xfce4-terminal")) { - m_externalTerminal->start(appName, QStringList() << QStringLiteral("-e") << " sh -c \"tty>" + file.fileName() + ";\"\"<&\\-\"\">&\\-;\"\"while :;\"\"do sleep 3600;\"\"done\""); + m_externalTerminal->start(appName, QStringList{ + QStringLiteral("-e"), + QLatin1String("sh -c \"tty>") + file.fileName() + QLatin1String(";\"\"<&\\-\"\">&\\-;\"\"while :;\"\"do sleep 3600;\"\"done\"")}); } else { - m_externalTerminal->start(appName, QStringList() << QStringLiteral("-e") << "sh -c \"tty>" + file.fileName() + ";exec<&-;exec>&-;while :;do sleep 3600;done\""); + m_externalTerminal->start(appName, QStringList{ + QStringLiteral("-e"), + QLatin1String("sh -c \"tty>") + file.fileName() + QLatin1String(";exec<&-;exec>&-;while :;do sleep 3600;done\"")}); } if (!m_externalTerminal->waitForStarted(500)) { m_lastError = "Can't run terminal: " + appName; m_externalTerminal->terminate(); return false; } for (int i = 0; i < 800; i++) { if (!file.bytesAvailable()) { if (m_externalTerminal->state() == QProcess::NotRunning && m_externalTerminal->exitCode()) { break; } QCoreApplication::processEvents(QEventLoop::AllEvents, 100); usleep(8000); } else { qCDebug(DEBUGGERCOMMON) << "Received terminal output(tty)"; break; } } usleep(1000); ttySlave = file.readAll().trimmed(); file.close(); if (ttySlave.isEmpty()) { m_lastError = i18n("Can't receive %1 tty/pty. Check that %1 is actually a terminal and that it accepts these arguments: -e sh -c \"tty> %2 ;exec<&-;exec>&-;while :;do sleep 3600;done\"", appName, file.fileName()); } #endif return true; } // ************************************************************************** diff --git a/plugins/debuggercommon/widgets/disassemblewidget.cpp b/plugins/debuggercommon/widgets/disassemblewidget.cpp index 8702679553..136188212e 100644 --- a/plugins/debuggercommon/widgets/disassemblewidget.cpp +++ b/plugins/debuggercommon/widgets/disassemblewidget.cpp @@ -1,538 +1,538 @@ /* * GDB Debugger Support * * Copyright 2000 John Birch * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2013 Vlas Puhov * * 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 "disassemblewidget.h" #include "midebuggerplugin.h" #include "debuglog.h" #include "midebugsession.h" #include "mi/micommand.h" #include "registers/registersmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI; using namespace KDevMI::MI; SelectAddressDialog::SelectAddressDialog(QWidget* parent) : QDialog(parent) { m_ui.setupUi(this); setWindowTitle(i18n("Address Selector")); connect(m_ui.comboBox, &KHistoryComboBox::editTextChanged, this, &SelectAddressDialog::validateInput); connect(m_ui.comboBox, static_cast(&KHistoryComboBox::returnPressed), this, &SelectAddressDialog::itemSelected); } QString SelectAddressDialog::address() const { return hasValidAddress() ? m_ui.comboBox->currentText() : QString(); } bool SelectAddressDialog::hasValidAddress() const { bool ok; m_ui.comboBox->currentText().toLongLong(&ok, 16); return ok; } void SelectAddressDialog::updateOkState() { m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(hasValidAddress()); } void SelectAddressDialog::validateInput() { updateOkState(); } void SelectAddressDialog::itemSelected() { QString text = m_ui.comboBox->currentText(); if( hasValidAddress() && m_ui.comboBox->findText(text) < 0 ) m_ui.comboBox->addItem(text); } DisassembleWindow::DisassembleWindow(QWidget *parent, DisassembleWidget* widget) : QTreeWidget(parent) { /*context menu commands */{ m_selectAddrAction = new QAction(i18n("Change &address"), this); m_selectAddrAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(m_selectAddrAction, &QAction::triggered, widget, &DisassembleWidget::slotChangeAddress); m_jumpToLocation = new QAction(QIcon::fromTheme(QStringLiteral("debug-execute-to-cursor")), i18n("&Jump to Cursor"), this); m_jumpToLocation->setWhatsThis(i18n("Sets the execution pointer to the current cursor position.")); connect(m_jumpToLocation,&QAction::triggered, widget, &DisassembleWidget::jumpToCursor); m_runUntilCursor = new QAction(QIcon::fromTheme(QStringLiteral("debug-run-cursor")), i18n("&Run to Cursor"), this); m_runUntilCursor->setWhatsThis(i18n("Continues execution until the cursor position is reached.")); connect(m_runUntilCursor,&QAction::triggered, widget, &DisassembleWidget::runToCursor); m_disassemblyFlavorAtt = new QAction(i18n("&AT&&T"), this); m_disassemblyFlavorAtt->setToolTip(i18n("GDB will use the AT&T disassembly flavor (e.g. mov 0xc(%ebp),%eax).")); m_disassemblyFlavorAtt->setData(DisassemblyFlavorATT); m_disassemblyFlavorAtt->setCheckable(true); m_disassemblyFlavorIntel = new QAction(i18n("&Intel"), this); m_disassemblyFlavorIntel->setToolTip(i18n("GDB will use the Intel disassembly flavor (e.g. mov eax, DWORD PTR [ebp+0xc]).")); m_disassemblyFlavorIntel->setData(DisassemblyFlavorIntel); m_disassemblyFlavorIntel->setCheckable(true); m_disassemblyFlavorActionGroup = new QActionGroup(this); m_disassemblyFlavorActionGroup->setExclusive(true); m_disassemblyFlavorActionGroup->addAction(m_disassemblyFlavorAtt); m_disassemblyFlavorActionGroup->addAction(m_disassemblyFlavorIntel); connect(m_disassemblyFlavorActionGroup, &QActionGroup::triggered, widget, &DisassembleWidget::setDisassemblyFlavor); } } void DisassembleWindow::setDisassemblyFlavor(DisassemblyFlavor flavor) { switch(flavor) { case DisassemblyFlavorUnknown: m_disassemblyFlavorAtt->setChecked(false); m_disassemblyFlavorIntel->setChecked(false); break; case DisassemblyFlavorATT: m_disassemblyFlavorAtt->setChecked(true); m_disassemblyFlavorIntel->setChecked(false); break; case DisassemblyFlavorIntel: m_disassemblyFlavorAtt->setChecked(false); m_disassemblyFlavorIntel->setChecked(true); break; } } void DisassembleWindow::contextMenuEvent(QContextMenuEvent *e) { QMenu popup(this); popup.addAction(m_selectAddrAction); popup.addAction(m_jumpToLocation); popup.addAction(m_runUntilCursor); QMenu* disassemblyFlavorMenu = popup.addMenu(i18n("Disassembly flavor")); disassemblyFlavorMenu->addAction(m_disassemblyFlavorAtt); disassemblyFlavorMenu->addAction(m_disassemblyFlavorIntel); popup.exec(e->globalPos()); } /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ DisassembleWidget::DisassembleWidget(MIDebuggerPlugin* plugin, QWidget *parent) : QWidget(parent), active_(false), lower_(0), upper_(0), address_(0), m_splitter(new KDevelop::AutoOrientedSplitter(this)) { QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setMargin(0); QHBoxLayout* controlsLayout = new QHBoxLayout; topLayout->addLayout(controlsLayout); { // initialize disasm/registers views topLayout->addWidget(m_splitter); //topLayout->setMargin(0); m_disassembleWindow = new DisassembleWindow(m_splitter, this); m_disassembleWindow->setWhatsThis(i18n("Machine code display

    " "A machine code view into your running " "executable with the current instruction " "highlighted. You can step instruction by " "instruction using the debuggers toolbar " "buttons of \"step over\" instruction and " "\"step into\" instruction.")); m_disassembleWindow->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_disassembleWindow->setSelectionMode(QTreeWidget::SingleSelection); m_disassembleWindow->setColumnCount(ColumnCount); m_disassembleWindow->setUniformRowHeights(true); m_disassembleWindow->setRootIsDecorated(false); - m_disassembleWindow->setHeaderLabels(QStringList() << QString() << i18n("Address") << i18n("Function") << i18n("Instruction")); + m_disassembleWindow->setHeaderLabels(QStringList{QString(), i18n("Address"), i18n("Function"), i18n("Instruction")}); m_splitter->setStretchFactor(0, 1); m_splitter->setContentsMargins(0, 0, 0, 0); m_registersManager = new RegistersManager(m_splitter); m_config = KSharedConfig::openConfig()->group("Disassemble/Registers View"); QByteArray state = m_config.readEntry("splitterState", QByteArray()); if (!state.isEmpty()) { m_splitter->restoreState(state); } } setLayout(topLayout); setWindowIcon( QIcon::fromTheme(QStringLiteral("system-run"), windowIcon()) ); setWindowTitle(i18n("Disassemble/Registers View")); KDevelop::IDebugController* pDC=KDevelop::ICore::self()->debugController(); Q_ASSERT(pDC); connect(pDC, &KDevelop::IDebugController::currentSessionChanged, this, &DisassembleWidget::currentSessionChanged); connect(plugin, &MIDebuggerPlugin::reset, this, &DisassembleWidget::slotDeactivate); m_dlg = new SelectAddressDialog(this); // show the data if debug session is active KDevelop::IDebugSession* pS = pDC->currentSession(); currentSessionChanged(pS); if(pS && !pS->currentAddr().isEmpty()) slotShowStepInSource(pS->currentUrl(), pS->currentLine(), pS->currentAddr()); } void DisassembleWidget::jumpToCursor() { MIDebugSession *s = qobject_cast(KDevelop::ICore:: self()->debugController()->currentSession()); if (s && s->isRunning()) { QString address = m_disassembleWindow->selectedItems().at(0)->text(Address); s->jumpToMemoryAddress(address); } } void DisassembleWidget::runToCursor(){ MIDebugSession *s = qobject_cast(KDevelop::ICore:: self()->debugController()->currentSession()); if (s && s->isRunning()) { QString address = m_disassembleWindow->selectedItems().at(0)->text(Address); s->runUntil(address); } } void DisassembleWidget::currentSessionChanged(KDevelop::IDebugSession* s) { MIDebugSession *session = qobject_cast(s); enableControls( session != nullptr ); // disable if session closed m_registersManager->setSession(session); if (session) { connect(session, &MIDebugSession::showStepInSource, this, &DisassembleWidget::slotShowStepInSource); connect(session,&MIDebugSession::showStepInDisassemble,this, &DisassembleWidget::update); } } /***************************************************************************/ DisassembleWidget::~DisassembleWidget() { m_config.writeEntry("splitterState", m_splitter->saveState()); } /***************************************************************************/ bool DisassembleWidget::displayCurrent() { if(address_ < lower_ || address_ > upper_) return false; bool bFound=false; for (int line=0; line < m_disassembleWindow->topLevelItemCount(); line++) { QTreeWidgetItem* item = m_disassembleWindow->topLevelItem(line); unsigned long address = item->text(Address).toULong(&ok,16); if (address == address_) { // put cursor at start of line and highlight the line m_disassembleWindow->setCurrentItem(item); static const QIcon icon = QIcon::fromTheme(QStringLiteral("go-next")); item->setIcon(Icon, icon); bFound = true; // need to process all items to clear icons } else if(!item->icon(Icon).isNull()) item->setIcon(Icon, QIcon()); } return bFound; } /***************************************************************************/ void DisassembleWidget::slotActivate(bool activate) { qCDebug(DEBUGGERCOMMON) << "Disassemble widget active: " << activate; if (active_ != activate) { active_ = activate; if (active_) { updateDisassemblyFlavor(); m_registersManager->updateRegisters(); if (!displayCurrent()) disassembleMemoryRegion(); } } } /***************************************************************************/ void DisassembleWidget::slotShowStepInSource(const QUrl&, int, const QString& currentAddress) { update(currentAddress); } void DisassembleWidget::updateExecutionAddressHandler(const ResultRecord& r) { const Value& content = r[QStringLiteral("asm_insns")]; const Value& pc = content[0]; if( pc.hasField(QStringLiteral("address")) ){ QString addr = pc[QStringLiteral("address")].literal(); address_ = addr.toULong(&ok,16); disassembleMemoryRegion(addr); } } /***************************************************************************/ void DisassembleWidget::disassembleMemoryRegion(const QString& from, const QString& to) { MIDebugSession *s = qobject_cast(KDevelop::ICore:: self()->debugController()->currentSession()); if(!s || !s->isRunning()) return; //only get $pc if (from.isEmpty()){ s->addCommand(DataDisassemble, QStringLiteral("-s \"$pc\" -e \"$pc+1\" -- 0"), this, &DisassembleWidget::updateExecutionAddressHandler); }else{ QString cmd = (to.isEmpty())? QStringLiteral("-s %1 -e \"%1 + 256\" -- 0").arg(from ): QStringLiteral("-s %1 -e %2+1 -- 0").arg(from, to); // if both addr set s->addCommand(DataDisassemble, cmd, this, &DisassembleWidget::disassembleMemoryHandler); } } /***************************************************************************/ void DisassembleWidget::disassembleMemoryHandler(const ResultRecord& r) { const Value& content = r[QStringLiteral("asm_insns")]; QString currentFunction; m_disassembleWindow->clear(); for(int i = 0; i < content.size(); ++i) { const Value& line = content[i]; QString addr, fct, offs, inst; if( line.hasField(QStringLiteral("address")) ) addr = line[QStringLiteral("address")].literal(); if( line.hasField(QStringLiteral("func-name")) ) fct = line[QStringLiteral("func-name")].literal(); if( line.hasField(QStringLiteral("offset")) ) offs = line[QStringLiteral("offset")].literal(); if( line.hasField(QStringLiteral("inst")) ) inst = line[QStringLiteral("inst")].literal(); //We use offset at the same column where function is. if(currentFunction == fct){ if(!fct.isEmpty()){ - fct = QStringLiteral("+") + offs; + fct = QLatin1Char('+') + offs; } }else { currentFunction = fct; } m_disassembleWindow->addTopLevelItem(new QTreeWidgetItem(m_disassembleWindow, - QStringList() << QString() << addr << fct << inst)); + QStringList{QString(), addr, fct, inst})); if (i == 0) { lower_ = addr.toULong(&ok,16); } else if (i == content.size()-1) { upper_ = addr.toULong(&ok,16); } } displayCurrent(); m_disassembleWindow->resizeColumnToContents(Icon); // make Icon always visible m_disassembleWindow->resizeColumnToContents(Address); // make entire address always visible } void DisassembleWidget::showEvent(QShowEvent*) { slotActivate(true); //it doesn't work for large names of functions // for (int i = 0; i < m_disassembleWindow->model()->columnCount(); ++i) // m_disassembleWindow->resizeColumnToContents(i); } void DisassembleWidget::hideEvent(QHideEvent*) { slotActivate(false); } void DisassembleWidget::slotDeactivate() { slotActivate(false); } void DisassembleWidget::enableControls(bool enabled) { m_disassembleWindow->setEnabled(enabled); } void DisassembleWidget::slotChangeAddress() { if(!m_dlg) return; m_dlg->updateOkState(); if (!m_disassembleWindow->selectedItems().isEmpty()) { m_dlg->setAddress(m_disassembleWindow->selectedItems().first()->text(Address)); } if (m_dlg->exec() == QDialog::Rejected) return; unsigned long addr = m_dlg->address().toULong(&ok,16); if (addr < lower_ || addr > upper_ || !displayCurrent()) disassembleMemoryRegion(m_dlg->address()); } void SelectAddressDialog::setAddress ( const QString& address ) { m_ui.comboBox->setCurrentItem ( address, true ); } void DisassembleWidget::update(const QString &address) { if (!active_) { return; } address_ = address.toULong(&ok, 16); if (!displayCurrent()) { disassembleMemoryRegion(); } m_registersManager->updateRegisters(); } void DisassembleWidget::setDisassemblyFlavor(QAction* action) { MIDebugSession* s = qobject_cast(KDevelop::ICore:: self()->debugController()->currentSession()); if(!s || !s->isRunning()) { return; } DisassemblyFlavor disassemblyFlavor = static_cast(action->data().toInt()); QString cmd; switch(disassemblyFlavor) { default: // unknown flavor, do not build a GDB command break; case DisassemblyFlavorATT: cmd = QStringLiteral("disassembly-flavor att"); break; case DisassemblyFlavorIntel: cmd = QStringLiteral("disassembly-flavor intel"); break; } qCDebug(DEBUGGERCOMMON) << "Disassemble widget set " << cmd; if (!cmd.isEmpty()) { s->addCommand(GdbSet, cmd, this, &DisassembleWidget::setDisassemblyFlavorHandler); } } void DisassembleWidget::setDisassemblyFlavorHandler(const ResultRecord& r) { if (r.reason == QLatin1String("done") && active_) { disassembleMemoryRegion(); } } void DisassembleWidget::updateDisassemblyFlavor() { MIDebugSession* s = qobject_cast(KDevelop::ICore:: self()->debugController()->currentSession()); if(!s || !s->isRunning()) { return; } s->addCommand(GdbShow, QStringLiteral("disassembly-flavor"), this, &DisassembleWidget::showDisassemblyFlavorHandler); } void DisassembleWidget::showDisassemblyFlavorHandler(const ResultRecord& r) { const Value& value = r[QStringLiteral("value")]; qCDebug(DEBUGGERCOMMON) << "Disassemble widget disassembly flavor" << value.literal(); DisassemblyFlavor disassemblyFlavor = DisassemblyFlavorUnknown; if (value.literal() == QLatin1String("att")) { disassemblyFlavor = DisassemblyFlavorATT; } else if (value.literal() == QLatin1String("intel")) { disassemblyFlavor = DisassemblyFlavorIntel; } else if (value.literal() == QLatin1String("default")) { disassemblyFlavor = DisassemblyFlavorATT; } m_disassembleWindow->setDisassemblyFlavor(disassemblyFlavor); } diff --git a/plugins/documentswitcher/documentswitcherplugin.cpp b/plugins/documentswitcher/documentswitcherplugin.cpp index 500bd30e02..e6c538fab1 100644 --- a/plugins/documentswitcher/documentswitcherplugin.cpp +++ b/plugins/documentswitcher/documentswitcherplugin.cpp @@ -1,407 +1,407 @@ /*************************************************************************** * Copyright 2009,2013 Andreas Pakulat * * Copyright 2013 Jarosław Sierant * * * * 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 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 "documentswitcherplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "documentswitchertreeview.h" #include "debug.h" #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(DocumentSwitcherFactory, "kdevdocumentswitcher.json", registerPlugin();) //TODO: Show frame around view's widget while walking through //TODO: Make the widget transparent DocumentSwitcherPlugin::DocumentSwitcherPlugin(QObject *parent, const QVariantList &/*args*/) :KDevelop::IPlugin(QStringLiteral("kdevdocumentswitcher"), parent), view(nullptr) { setXMLFile(QStringLiteral("kdevdocumentswitcher.rc")); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "Adding active mainwindow from constructor" << KDevelop::ICore::self()->uiController()->activeMainWindow(); addMainWindow( qobject_cast( KDevelop::ICore::self()->uiController()->activeMainWindow() ) ); connect( KDevelop::ICore::self()->uiController()->controller(), &Sublime::Controller::mainWindowAdded, this, &DocumentSwitcherPlugin::addMainWindow ); #ifdef Q_OS_MACOS // Qt/Mac swaps the Ctrl and Meta (Command) keys by default, so that shortcuts defined as Ctrl+X // become the platform-standard Command+X . Ideally we would map the document switcher shortcut to // Control+Tab (and thus Qt::META+Qt::Key_Tab) everywhere because Command+Tab and Command+Shift+Tab // are reserved system shortcuts that bring up the application switcher. The Control+Tab shortcut is // inoperable on Mac, so we resort to the Alt (Option) key, unless the AA_MacDontSwapCtrlAndMeta // attribute is set. const Qt::Modifier shortcutAccelerator = QCoreApplication::testAttribute(Qt::AA_MacDontSwapCtrlAndMeta) ? Qt::CTRL : Qt::ALT; #else const Qt::Modifier shortcutAccelerator = Qt::CTRL; #endif forwardAction = actionCollection()->addAction ( QStringLiteral( "last_used_views_forward" ) ); forwardAction->setText( i18n( "Last Used Views" ) ); forwardAction->setIcon( QIcon::fromTheme( QStringLiteral( "go-next-view-page") ) ); actionCollection()->setDefaultShortcut( forwardAction, shortcutAccelerator | Qt::Key_Tab ); forwardAction->setWhatsThis( i18n( "Opens a list to walk through the list of last used views." ) ); forwardAction->setStatusTip( i18n( "Walk through the list of last used views" ) ); connect( forwardAction, &QAction::triggered, this, &DocumentSwitcherPlugin::walkForward ); backwardAction = actionCollection()->addAction ( QStringLiteral( "last_used_views_backward" ) ); backwardAction->setText( i18n( "Last Used Views (Reverse)" ) ); backwardAction->setIcon( QIcon::fromTheme( QStringLiteral( "go-previous-view-page") ) ); actionCollection()->setDefaultShortcut( backwardAction, shortcutAccelerator | Qt::SHIFT | Qt::Key_Tab ); backwardAction->setWhatsThis( i18n( "Opens a list to walk through the list of last used views in reverse." ) ); backwardAction->setStatusTip( i18n( "Walk through the list of last used views" ) ); connect( backwardAction, &QAction::triggered, this, &DocumentSwitcherPlugin::walkBackward ); view = new DocumentSwitcherTreeView( this ); view->setSelectionBehavior( QAbstractItemView::SelectRows ); view->setSelectionMode( QAbstractItemView::SingleSelection ); view->setUniformRowHeights( true ); view->setTextElideMode( Qt::ElideMiddle ); view->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); view->addAction( forwardAction ); view->addAction( backwardAction ); view->setHeaderHidden( true ); view->setIndentation( 10 ); connect( view, &QListView::pressed, this, &DocumentSwitcherPlugin::switchToClicked ); connect( view, &QListView::activated, this, &DocumentSwitcherPlugin::itemActivated ); model = new QStandardItemModel( view ); view->setModel( model ); } void DocumentSwitcherPlugin::setViewGeometry(Sublime::MainWindow* window) { const QSize centralSize = window->centralWidget()->size(); // Maximum size of the view is 3/4th of the central widget (the editor area) so the view does not overlap the // mainwindow since that looks awkward. const QSize viewMaxSize( centralSize.width() * 3/4, centralSize.height() * 3/4 ); // The actual view size should be as big as the columns/rows need it, but smaller than the max-size. This means // the view will get quite high with many open files but I think that is ok. Otherwise one can easily tweak the // max size to be only 1/2th of the central widget size const int rowHeight = view->sizeHintForRow(0); const int frameWidth = view->frameWidth(); const QSize viewSize( std::min( view->sizeHintForColumn(0) + 2 * frameWidth + view->verticalScrollBar()->width(), viewMaxSize.width() ), std::min( std::max( rowHeight * view->model()->rowCount() + 2 * frameWidth, rowHeight * 6 ), viewMaxSize.height() ) ); // Position should be central over the editor area, so map to global from parent of central widget since // the view is positioned in global coords QPoint centralWidgetPos = window->mapToGlobal( window->centralWidget()->pos() ); const int xPos = std::max(0, centralWidgetPos.x() + (centralSize.width() - viewSize.width() ) / 2); const int yPos = std::max(0, centralWidgetPos.y() + (centralSize.height() - viewSize.height() ) / 2); view->setFixedSize(viewSize); view->move(xPos, yPos); } void DocumentSwitcherPlugin::walk(const int from, const int to) { Sublime::MainWindow* window = qobject_cast( KDevelop::ICore::self()->uiController()->activeMainWindow() ); if( !window || !documentLists.contains( window ) || !documentLists[window].contains( window->area() ) ) { qCWarning(PLUGIN_DOCUMENTSWITCHER) << "This should not happen, tried to walk through document list of an unknown mainwindow!"; return; } QModelIndex idx; const int step = from < to ? 1 : -1; if(!view->isVisible()) { fillModel(window); setViewGeometry(window); idx = model->index(from + step, 0); if(!idx.isValid()) { idx = model->index(0, 0); } view->show(); } else { int newRow = view->selectionModel()->currentIndex().row() + step; if(newRow == to + step) { newRow = from; } idx = model->index(newRow, 0); } view->selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); view->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); } void DocumentSwitcherPlugin::walkForward() { walk(0, model->rowCount()-1); } void DocumentSwitcherPlugin::walkBackward() { walk(model->rowCount()-1, 0); } void DocumentSwitcherPlugin::fillModel( Sublime::MainWindow* window ) { model->clear(); auto projectController = KDevelop::ICore::self()->projectController(); foreach( Sublime::View* v, documentLists[window][window->area()] ) { using namespace KDevelop; Sublime::Document const* const slDoc = v->document(); if( !slDoc ) { continue; } QString itemText = slDoc->title();// file name IDocument const* const doc = dynamic_cast(v->document()); IProject* project = nullptr; if( doc ) { QString path = projectController->prettyFilePath(doc->url(), IProjectController::FormatPlain); const bool isPartOfOpenProject = QDir::isRelativePath(path); if( path.endsWith('/') ) { - path.remove(path.length() - 1, 1); + path.chop(1); } if( isPartOfOpenProject ) { const int projectNameSize = path.indexOf(QLatin1Char(':')); // first: project name, second: path to file in project (might be just '/' when the file is in the project root dir) const QPair fileInProjectInfo = (projectNameSize < 0) ? qMakePair(path, QStringLiteral("/")) : qMakePair(path.left(projectNameSize), path.mid(projectNameSize + 1)); itemText = QStringLiteral("%1 (%2:%3)").arg(itemText, fileInProjectInfo.first, fileInProjectInfo.second); } else { itemText = itemText + " (" + path + ')'; } project = projectController->findProjectForUrl(doc->url()); } auto item = new QStandardItem( slDoc->icon(), itemText ); item->setData(QVariant::fromValue(project), DocumentSwitcherTreeView::ProjectRole); model->appendRow( item ); } } DocumentSwitcherPlugin::~DocumentSwitcherPlugin() { } void DocumentSwitcherPlugin::switchToClicked( const QModelIndex& idx ) { view->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect); itemActivated(idx); } void DocumentSwitcherPlugin::itemActivated( const QModelIndex& idx ) { Q_UNUSED( idx ); if( view->selectionModel()->selectedRows().isEmpty() ) { return; } int row = view->selectionModel()->selectedRows().first().row(); Sublime::MainWindow* window = qobject_cast( KDevelop::ICore::self()->uiController()->activeMainWindow() ); Sublime::View* activatedView = nullptr; if( window && documentLists.contains( window ) && documentLists[window].contains( window->area() ) ) { const QList l = documentLists[window][window->area()]; if( row >= 0 && row < l.size() ) { activatedView = l.at( row ); } } if( activatedView ) { if( QApplication::mouseButtons() & Qt::MiddleButton ) { window->area()->closeView( activatedView ); fillModel( window ); if ( model->rowCount() == 0 ) { view->hide(); } else { view->selectionModel()->select( view->model()->index(0, 0), QItemSelectionModel::ClearAndSelect ); } } else { window->activateView( activatedView ); view->hide(); } } } void DocumentSwitcherPlugin::unload() { foreach( QObject* mw, documentLists.keys() ) { removeMainWindow( mw ); } delete forwardAction; delete backwardAction; view->deleteLater(); } void DocumentSwitcherPlugin::storeAreaViewList( Sublime::MainWindow* mainwindow, Sublime::Area* area ) { if( !documentLists.contains( mainwindow ) || !documentLists[mainwindow].contains(area) ) { QHash > areas; qCDebug(PLUGIN_DOCUMENTSWITCHER) << "adding area views for area:" << area << area->title() << "mainwindow:" << mainwindow << mainwindow->windowTitle(); foreach( Sublime::View* v, area->views() ) { qCDebug(PLUGIN_DOCUMENTSWITCHER) << "view:" << v << v->document()->title(); } qCDebug(PLUGIN_DOCUMENTSWITCHER) << "done"; areas.insert( area, area->views() ); documentLists.insert( mainwindow, areas ); } } void DocumentSwitcherPlugin::addMainWindow( Sublime::MainWindow* mainwindow ) { if( !mainwindow ) { return; } qCDebug(PLUGIN_DOCUMENTSWITCHER) << "adding mainwindow:" << mainwindow << mainwindow->windowTitle(); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "storing all views from area:" << mainwindow->area()->title() << mainwindow->area(); storeAreaViewList( mainwindow, mainwindow->area() ); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "connecting signals on mainwindow"; connect( mainwindow, &Sublime::MainWindow::areaChanged, this, &DocumentSwitcherPlugin::changeArea ); connect( mainwindow, &Sublime::MainWindow::activeViewChanged, this, &DocumentSwitcherPlugin::changeView ); connect( mainwindow, &Sublime::MainWindow::viewAdded, this, &DocumentSwitcherPlugin::addView ); connect( mainwindow, &Sublime::MainWindow::aboutToRemoveView, this, &DocumentSwitcherPlugin::removeView ); connect( mainwindow, &Sublime::MainWindow::destroyed, this, &DocumentSwitcherPlugin::removeMainWindow); mainwindow->installEventFilter( this ); } bool DocumentSwitcherPlugin::eventFilter( QObject* watched, QEvent* ev ) { Sublime::MainWindow* mw = dynamic_cast( watched ); if( mw && ev->type() == QEvent::WindowActivate ) { enableActions(); } return QObject::eventFilter( watched, ev ); } void DocumentSwitcherPlugin::addView( Sublime::View* view ) { Sublime::MainWindow* mainwindow = qobject_cast( sender() ); if( !mainwindow ) return; qCDebug(PLUGIN_DOCUMENTSWITCHER) << "got signal from mainwindow:" << mainwindow << mainwindow->windowTitle() << "its area is:" << mainwindow->area() << mainwindow->area()->title() << "adding view:" << view << view->document()->title(); enableActions(); documentLists[mainwindow][mainwindow->area()].append( view ); } void DocumentSwitcherPlugin::enableActions() { forwardAction->setEnabled(true); backwardAction->setEnabled(true); } void DocumentSwitcherPlugin::removeMainWindow( QObject* obj ) { if( !obj || !documentLists.contains(obj) ) { return; } obj->removeEventFilter( this ); disconnect( obj, nullptr, this, nullptr ); documentLists.remove( obj ); } void DocumentSwitcherPlugin::changeArea( Sublime::Area* area ) { Sublime::MainWindow* mainwindow = qobject_cast( sender() ); Q_ASSERT( mainwindow ); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "area changed:" << area << area->title() << "mainwindow:" << mainwindow << mainwindow->windowTitle(); //Since the main-window only emits aboutToRemoveView for views within the current area, we must forget all areas except the active one documentLists.remove(mainwindow); if( !documentLists[mainwindow].contains( area ) ) { qCDebug(PLUGIN_DOCUMENTSWITCHER) << "got area change, storing its views"; storeAreaViewList( mainwindow, area ); } enableActions(); } void DocumentSwitcherPlugin::changeView( Sublime::View* view ) { if( !view ) return; Sublime::MainWindow* mainwindow = qobject_cast( sender() ); Q_ASSERT( mainwindow ); Sublime::Area* area = mainwindow->area(); int idx = documentLists[mainwindow][area].indexOf( view ); if( idx != -1 ) { documentLists[mainwindow][area].removeAt( idx ); } qCDebug(PLUGIN_DOCUMENTSWITCHER) << "moving view to front, list should now not contain this view anymore" << view << view->document()->title(); - qCDebug(PLUGIN_DOCUMENTSWITCHER) << "current area is:" << area << area->title() << "mainwnidow:" << mainwindow << mainwindow->windowTitle();; + qCDebug(PLUGIN_DOCUMENTSWITCHER) << "current area is:" << area << area->title() << "mainwindow:" << mainwindow << mainwindow->windowTitle(); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "idx of this view in list:" << documentLists[mainwindow][area].indexOf( view ); documentLists[mainwindow][area].prepend( view ); enableActions(); } void DocumentSwitcherPlugin::removeView( Sublime::View* view ) { if( !view ) return; Sublime::MainWindow* mainwindow = qobject_cast( sender() ); Q_ASSERT( mainwindow ); Sublime::Area* area = mainwindow->area(); int idx = documentLists[mainwindow][area].indexOf( view ); if( idx != -1 ) { documentLists[mainwindow][area].removeAt( idx ); } qCDebug(PLUGIN_DOCUMENTSWITCHER) << "removing view, list should now not contain this view anymore" << view << view->document()->title(); - qCDebug(PLUGIN_DOCUMENTSWITCHER) << "current area is:" << area << area->title() << "mainwnidow:" << mainwindow << mainwindow->windowTitle();; + qCDebug(PLUGIN_DOCUMENTSWITCHER) << "current area is:" << area << area->title() << "mainwindow:" << mainwindow << mainwindow->windowTitle(); qCDebug(PLUGIN_DOCUMENTSWITCHER) << "idx of this view in list:" << documentLists[mainwindow][area].indexOf( view ); enableActions(); } #include "documentswitcherplugin.moc" diff --git a/plugins/execute/projecttargetscombobox.cpp b/plugins/execute/projecttargetscombobox.cpp index 185acbbe7f..45fa1f0eb6 100644 --- a/plugins/execute/projecttargetscombobox.cpp +++ b/plugins/execute/projecttargetscombobox.cpp @@ -1,88 +1,90 @@ /* This file is part of KDevelop Copyright 2010 Aleix Pol Gonzalez This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projecttargetscombobox.h" #include #include #include #include #include #include using namespace KDevelop; ProjectTargetsComboBox::ProjectTargetsComboBox(QWidget* parent) : QComboBox(parent) { } class ExecutablePathsVisitor : public ProjectVisitor { public: explicit ExecutablePathsVisitor(bool exec) : m_onlyExecutables(exec) {} using ProjectVisitor::visit; void visit(ProjectExecutableTargetItem* eit) override { if(!m_onlyExecutables || eit->type()==ProjectTargetItem::ExecutableTarget) m_paths += KDevelop::joinWithEscaping(eit->model()->pathFromIndex(eit->index()), '/', '\\'); } QStringList paths() const { return m_paths; } private: bool m_onlyExecutables; QStringList m_paths; }; void ProjectTargetsComboBox::setBaseItem(ProjectFolderItem* item, bool exec) { clear(); QList items; if(item) { items += item; } else { - foreach(IProject* p, ICore::self()->projectController()->projects()) { + const auto projects = ICore::self()->projectController()->projects(); + items.reserve(projects.size()); + for (auto* p : projects) { items += p->projectItem(); } } ExecutablePathsVisitor walker(exec); foreach(ProjectFolderItem* item, items) { walker.visit(item); } foreach(const QString& item, walker.paths()) addItem(QIcon::fromTheme(QStringLiteral("system-run")), item); } QStringList ProjectTargetsComboBox::currentItemPath() const { return KDevelop::splitWithEscaping(currentText(), '/', '\\'); } void ProjectTargetsComboBox::setCurrentItemPath(const QStringList& str) { setCurrentIndex(str.isEmpty() && count() ? 0 : findText(KDevelop::joinWithEscaping(str, '/', '\\'))); } diff --git a/plugins/executeplasmoid/plasmoidexecutionconfig.cpp b/plugins/executeplasmoid/plasmoidexecutionconfig.cpp index c63a2d5aa3..5081cd2e4e 100644 --- a/plugins/executeplasmoid/plasmoidexecutionconfig.cpp +++ b/plugins/executeplasmoid/plasmoidexecutionconfig.cpp @@ -1,329 +1,330 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat Copyright 2009 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plasmoidexecutionconfig.h" #include "plasmoidexecutionjob.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class la; Q_DECLARE_METATYPE(KDevelop::IProject*) QIcon PlasmoidExecutionConfig::icon() const { return QIcon::fromTheme("system-run"); } QStringList readProcess(QProcess* p) { QStringList ret; while(!p->atEnd()) { QByteArray line = p->readLine(); int nameEnd=line.indexOf(' '); if(nameEnd>0) { ret += line.left(nameEnd); } } return ret; } PlasmoidExecutionConfig::PlasmoidExecutionConfig( QWidget* parent ) : LaunchConfigurationPage( parent ) { setupUi(this); connect( identifier->lineEdit(), &QLineEdit::textEdited, this, &PlasmoidExecutionConfig::changed ); QProcess pPlasmoids; pPlasmoids.start(QStringLiteral("plasmoidviewer"), QStringList(QStringLiteral("--list")), QIODevice::ReadOnly); QProcess pThemes; pThemes.start(QStringLiteral("plasmoidviewer"), QStringList(QStringLiteral("--list-themes")), QIODevice::ReadOnly); pThemes.waitForFinished(); pPlasmoids.waitForFinished(); foreach(const QString& plasmoid, readProcess(&pPlasmoids)) { identifier->addItem(plasmoid); } themes->addItem(QString()); foreach(const QString& theme, readProcess(&pThemes)) { themes->addItem(theme); } connect( dependencies, &KDevelop::DependenciesWidget::changed, this, &PlasmoidExecutionConfig::changed ); } void PlasmoidExecutionConfig::saveToConfiguration( KConfigGroup cfg, KDevelop::IProject* project ) const { Q_UNUSED( project ); cfg.writeEntry("PlasmoidIdentifier", identifier->lineEdit()->text()); - QStringList args; - args += QStringLiteral("--formfactor"); - args += formFactor->currentText(); + QStringList args{ + QStringLiteral("--formfactor"), + formFactor->currentText(), + }; if(!themes->currentText().isEmpty()) { args += QStringLiteral("--theme"); args += themes->currentText(); } cfg.writeEntry("Arguments", args); QVariantList deps = dependencies->dependencies(); cfg.writeEntry( "Dependencies", KDevelop::qvariantToString( QVariant( deps ) ) ); } void PlasmoidExecutionConfig::loadFromConfiguration(const KConfigGroup& cfg, KDevelop::IProject* ) { bool b = blockSignals( true ); identifier->lineEdit()->setText(cfg.readEntry("PlasmoidIdentifier", "")); blockSignals( b ); QStringList arguments = cfg.readEntry("Arguments", QStringList()); int idxFormFactor = arguments.indexOf(QStringLiteral("--formfactor"))+1; if(idxFormFactor>0) formFactor->setCurrentIndex(formFactor->findText(arguments[idxFormFactor])); int idxTheme = arguments.indexOf(QStringLiteral("--theme"))+1; if(idxTheme>0) themes->setCurrentIndex(themes->findText(arguments[idxTheme])); dependencies->setDependencies( KDevelop::stringToQVariant( cfg.readEntry( "Dependencies", QString() ) ).toList()); } QString PlasmoidExecutionConfig::title() const { return i18n("Configure Plasmoid Execution"); } QList< KDevelop::LaunchConfigurationPageFactory* > PlasmoidLauncher::configPages() const { return QList(); } QString PlasmoidLauncher::description() const { return i18n("Display a plasmoid"); } QString PlasmoidLauncher::id() { return QStringLiteral("PlasmoidLauncher"); } QString PlasmoidLauncher::name() const { return i18n("Plasmoid Launcher"); } PlasmoidLauncher::PlasmoidLauncher(ExecutePlasmoidPlugin* plugin) : m_plugin(plugin) { } KJob* PlasmoidLauncher::start(const QString& launchMode, KDevelop::ILaunchConfiguration* cfg) { Q_ASSERT(cfg); if( !cfg ) { return nullptr; } if( launchMode == QLatin1String("execute") ) { KJob* depsJob = dependencies(cfg); QList jobs; if(depsJob) jobs << depsJob; jobs << new PlasmoidExecutionJob(m_plugin, cfg); return new KDevelop::ExecuteCompositeJob( KDevelop::ICore::self()->runController(), jobs ); } qCWarning(EXECUTEPLASMOID) << "Unknown launch mode " << launchMode << "for config:" << cfg->name(); return nullptr; } KJob* PlasmoidLauncher::calculateDependencies(KDevelop::ILaunchConfiguration* cfg) { QVariantList deps = KDevelop::stringToQVariant( cfg->config().readEntry( "Dependencies", QString() ) ).toList(); if( !deps.isEmpty() ) { KDevelop::ProjectModel* model = KDevelop::ICore::self()->projectController()->projectModel(); QList items; foreach( const QVariant& dep, deps ) { KDevelop::ProjectBaseItem* item = model->itemFromIndex( model->pathToIndex( dep.toStringList() ) ); if( item ) { items << item; } else { KMessageBox::error(KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Could not resolve the dependency: %1", dep.toString())); } } KDevelop::BuilderJob* job = new KDevelop::BuilderJob; job->addItems( KDevelop::BuilderJob::Install, items ); job->updateJobName(); return job; } return nullptr; } KJob* PlasmoidLauncher::dependencies(KDevelop::ILaunchConfiguration* cfg) { return calculateDependencies(cfg); } QStringList PlasmoidLauncher::supportedModes() const { return QStringList() << QStringLiteral("execute"); } KDevelop::LaunchConfigurationPage* PlasmoidPageFactory::createWidget(QWidget* parent) { return new PlasmoidExecutionConfig( parent ); } PlasmoidPageFactory::PlasmoidPageFactory() {} PlasmoidExecutionConfigType::PlasmoidExecutionConfigType() { factoryList.append( new PlasmoidPageFactory ); } PlasmoidExecutionConfigType::~PlasmoidExecutionConfigType() { qDeleteAll(factoryList); factoryList.clear(); } QString PlasmoidExecutionConfigType::name() const { return i18n("Plasmoid Launcher"); } QList PlasmoidExecutionConfigType::configPages() const { return factoryList; } QString PlasmoidExecutionConfigType::typeId() { return QStringLiteral("PlasmoidLauncherType"); } QIcon PlasmoidExecutionConfigType::icon() const { return QIcon::fromTheme("plasma"); } static bool canLaunchMetadataFile(const KDevelop::Path &path) { KConfig cfg(path.toLocalFile(), KConfig::SimpleConfig); KConfigGroup group(&cfg, "Desktop Entry"); QStringList services = group.readEntry("ServiceTypes", group.readEntry("X-KDE-ServiceTypes", QStringList())); return services.contains(QStringLiteral("Plasma/Applet")); } //don't bother, nobody uses this interface bool PlasmoidExecutionConfigType::canLaunch(const QUrl& ) const { return false; } bool PlasmoidExecutionConfigType::canLaunch(KDevelop::ProjectBaseItem* item) const { KDevelop::ProjectFolderItem* folder = item->folder(); if(folder && folder->hasFileOrFolder(QStringLiteral("metadata.desktop"))) { return canLaunchMetadataFile(KDevelop::Path(folder->path(), QStringLiteral("metadata.desktop"))); } return false; } void PlasmoidExecutionConfigType::configureLaunchFromItem(KConfigGroup config, KDevelop::ProjectBaseItem* item) const { config.writeEntry("PlasmoidIdentifier", item->path().toUrl().toLocalFile()); } void PlasmoidExecutionConfigType::configureLaunchFromCmdLineArguments(KConfigGroup /*config*/, const QStringList &/*args*/) const {} QMenu* PlasmoidExecutionConfigType::launcherSuggestions() { QList found; QList projects = KDevelop::ICore::self()->projectController()->projects(); foreach(KDevelop::IProject* p, projects) { QSet files = p->fileSet(); foreach(const KDevelop::IndexedString& file, files) { KDevelop::Path path(file.str()); if (path.lastPathSegment() == QLatin1String("metadata.desktop") && canLaunchMetadataFile(path)) { path = path.parent(); QString relUrl = p->path().relativePath(path); QAction* action = new QAction(relUrl, this); action->setProperty("url", relUrl); action->setProperty("project", qVariantFromValue(p)); connect(action, &QAction::triggered, this, &PlasmoidExecutionConfigType::suggestionTriggered); found.append(action); } } } QMenu *m = nullptr; if(!found.isEmpty()) { m = new QMenu(i18n("Plasmoids")); m->addActions(found); } return m; } void PlasmoidExecutionConfigType::suggestionTriggered() { QAction* action = qobject_cast(sender()); KDevelop::IProject* p = action->property("project").value(); QString relUrl = action->property("url").toString(); KDevelop::ILauncher* launcherInstance = launchers().at( 0 ); QPair launcher = qMakePair( launcherInstance->supportedModes().at(0), launcherInstance->id() ); QString name = relUrl.mid(relUrl.lastIndexOf('/')+1); KDevelop::ILaunchConfiguration* config = KDevelop::ICore::self()->runController()->createLaunchConfiguration(this, launcher, p, name); KConfigGroup cfg = config->config(); cfg.writeEntry("PlasmoidIdentifier", relUrl); emit signalAddLaunchConfiguration(config); } diff --git a/plugins/executeplasmoid/plasmoidexecutionjob.cpp b/plugins/executeplasmoid/plasmoidexecutionjob.cpp index db312706c2..3415a83d94 100644 --- a/plugins/executeplasmoid/plasmoidexecutionjob.cpp +++ b/plugins/executeplasmoid/plasmoidexecutionjob.cpp @@ -1,148 +1,148 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat Copyright 2009 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plasmoidexecutionjob.h" #include "executeplasmoidplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; PlasmoidExecutionJob::PlasmoidExecutionJob(ExecutePlasmoidPlugin* iface, ILaunchConfiguration* cfg) : OutputJob( iface ) { QString identifier = cfg->config().readEntry("PlasmoidIdentifier", ""); Q_ASSERT(!identifier.isEmpty()); setToolTitle(i18n("Plasmoid Viewer")); setCapabilities(Killable); setStandardToolView( IOutputView::RunView ); setBehaviours(IOutputView::AllowUserClose | IOutputView::AutoScroll ); setObjectName("plasmoidviewer "+identifier); setDelegate(new OutputDelegate); m_process = new CommandExecutor(executable(cfg), this); m_process->setArguments( arguments(cfg) ); m_process->setWorkingDirectory(workingDirectory(cfg)); OutputModel* model = new OutputModel(QUrl::fromLocalFile(m_process->workingDirectory()), this); model->setFilteringStrategy(OutputModel::CompilerFilter); setModel( model ); connect(m_process, &CommandExecutor::receivedStandardError, model, &OutputModel::appendLines ); connect(m_process, &CommandExecutor::receivedStandardOutput, model, &OutputModel::appendLines ); connect( m_process, &CommandExecutor::failed, this, &PlasmoidExecutionJob::slotFailed ); connect( m_process, &CommandExecutor::completed, this, &PlasmoidExecutionJob::slotCompleted ); } void PlasmoidExecutionJob::start() { startOutput(); - model()->appendLine( m_process->workingDirectory() + "> " + m_process->command() + " " + m_process->arguments().join(QLatin1Char(' ')) ); + model()->appendLine( m_process->workingDirectory() + "> " + m_process->command() + ' ' + m_process->arguments().join(QLatin1Char(' ')) ); m_process->start(); } bool PlasmoidExecutionJob::doKill() { m_process->kill(); model()->appendLine( i18n("** Killed **") ); return true; } OutputModel* PlasmoidExecutionJob::model() { return qobject_cast( OutputJob::model() ); } void PlasmoidExecutionJob::slotCompleted(int code) { if( code != 0 ) { setError( FailedShownError ); model()->appendLine( i18n("*** Failed ***") ); } else { model()->appendLine( i18n("*** Finished ***") ); } emitResult(); } void PlasmoidExecutionJob::slotFailed(QProcess::ProcessError error) { setError(error); // FIXME need more detail setErrorText(i18n("Plasmoid failed to execute on %1", m_process->workingDirectory())); model()->appendLine( i18n("*** Failed ***") ); emitResult(); } QString PlasmoidExecutionJob::executable(ILaunchConfiguration*) { return QStandardPaths::findExecutable(QStringLiteral("plasmoidviewer")); } QStringList PlasmoidExecutionJob::arguments(ILaunchConfiguration* cfg) { QStringList arguments = cfg->config().readEntry("Arguments", QStringList()); if(workingDirectory(cfg) == QDir::tempPath()) { QString identifier = cfg->config().readEntry("PlasmoidIdentifier", ""); arguments += QStringLiteral("-a"); arguments += identifier; } else { arguments += { "-a", "." }; } return arguments; } QString PlasmoidExecutionJob::workingDirectory(ILaunchConfiguration* cfg) { QString workingDirectory; IProject* p = cfg->project(); if(p) { QString identifier = cfg->config().readEntry("PlasmoidIdentifier", ""); QString possiblePath = Path(p->path(), identifier).toLocalFile(); if(QFileInfo(possiblePath).isDir()) { workingDirectory = possiblePath; } } if(workingDirectory.isEmpty()) { workingDirectory = QDir::tempPath(); } return workingDirectory; } diff --git a/plugins/filemanager/filemanager.cpp b/plugins/filemanager/filemanager.cpp index d948231290..dadfbac94d 100644 --- a/plugins/filemanager/filemanager.cpp +++ b/plugins/filemanager/filemanager.cpp @@ -1,212 +1,215 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * Copyright 2006 Andreas Pakulat * * Copyright 2016 Imran Tatriev * * * * 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 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 "filemanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../openwith/iopenwith.h" #include "kdevfilemanagerplugin.h" #include "bookmarkhandler.h" #include "debug.h" FileManager::FileManager(KDevFileManagerPlugin *plugin, QWidget* parent) : QWidget(parent), m_plugin(plugin) { setObjectName(QStringLiteral("FileManager")); setWindowIcon(QIcon::fromTheme(QStringLiteral("folder-sync"), windowIcon())); setWindowTitle(i18n("File System")); KConfigGroup cg = KDevelop::ICore::self()->activeSession()->config()->group( "Filesystem" ); QVBoxLayout *l = new QVBoxLayout(this); l->setMargin(0); l->setSpacing(0); KFilePlacesModel* model = new KFilePlacesModel( this ); urlnav = new KUrlNavigator(model, QUrl(cg.readEntry( "LastLocation", QUrl::fromLocalFile( QDir::homePath() ) )), this ); connect(urlnav, &KUrlNavigator::urlChanged, this, &FileManager::gotoUrl); l->addWidget(urlnav); dirop = new KDirOperator( urlnav->locationUrl(), this); dirop->setView( KFile::Tree ); dirop->setupMenu( KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::NavActions | KDirOperator::ViewActions ); connect(dirop, &KDirOperator::urlEntered, this, &FileManager::updateNav); connect(dirop, &KDirOperator::contextMenuAboutToShow, this, &FileManager::fillContextMenu); l->addWidget(dirop); connect( dirop, &KDirOperator::fileSelected, this, &FileManager::openFile ); setFocusProxy(dirop); // includes some actions, but not hooked into the shortcut dialog atm m_actionCollection = new KActionCollection(this); m_actionCollection->addAssociatedWidget(this); setupActions(); // Connect the bookmark handler connect(m_bookmarkHandler, &BookmarkHandler::openUrl, this, &FileManager::gotoUrl); connect(m_bookmarkHandler, &BookmarkHandler::openUrl, this, &FileManager::updateNav); } FileManager::~FileManager() { KConfigGroup cg = KDevelop::ICore::self()->activeSession()->config()->group( "Filesystem" ); cg.writeEntry( "LastLocation", urlnav->locationUrl() ); cg.sync(); } void FileManager::fillContextMenu(const KFileItem& item, QMenu* menu) { foreach(QAction* a, contextActions){ if(menu->actions().contains(a)){ menu->removeAction(a); } } contextActions.clear(); contextActions.append(menu->addSeparator()); menu->addAction(newFileAction); contextActions.append(newFileAction); KDevelop::FileContext context(QList() << item.url()); QList extensions = KDevelop::ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); KDevelop::ContextMenuExtension::populateMenu(menu, extensions); QMenu* tmpMenu = new QMenu(); KDevelop::ContextMenuExtension::populateMenu(tmpMenu, extensions); contextActions.append(tmpMenu->actions()); delete tmpMenu; } void FileManager::openFile(const KFileItem& file) { KDevelop::IOpenWith::openFiles(QList() << file.url()); } void FileManager::gotoUrl( const QUrl& url ) { dirop->setUrl( url, true ); } void FileManager::updateNav( const QUrl& url ) { urlnav->setLocationUrl( url ); } void FileManager::setupActions() { KActionMenu *acmBookmarks = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), this); acmBookmarks->setDelayed(false); m_bookmarkHandler = new BookmarkHandler(this, acmBookmarks->menu()); acmBookmarks->setShortcutContext(Qt::WidgetWithChildrenShortcut); QAction* action = new QAction(this); action->setShortcutContext(Qt::WidgetWithChildrenShortcut); action->setText(i18n("Current Document Directory")); action->setIcon(QIcon::fromTheme(QStringLiteral("dirsync"))); connect(action, &QAction::triggered, this, &FileManager::syncCurrentDocumentDirectory); - tbActions << (dirop->actionCollection()->action(QStringLiteral("back"))); - tbActions << (dirop->actionCollection()->action(QStringLiteral("up"))); - tbActions << (dirop->actionCollection()->action(QStringLiteral("home"))); - tbActions << (dirop->actionCollection()->action(QStringLiteral("forward"))); - tbActions << (dirop->actionCollection()->action(QStringLiteral("reload"))); - tbActions << acmBookmarks; - tbActions << action; - tbActions << (dirop->actionCollection()->action(QStringLiteral("sorting menu"))); - tbActions << (dirop->actionCollection()->action(QStringLiteral("show hidden"))); + auto* diropActionCollection = dirop->actionCollection(); + tbActions = { + diropActionCollection->action(QStringLiteral("back")), + diropActionCollection->action(QStringLiteral("up")), + diropActionCollection->action(QStringLiteral("home")), + diropActionCollection->action(QStringLiteral("forward")), + diropActionCollection->action(QStringLiteral("reload")), + acmBookmarks, + action, + diropActionCollection->action(QStringLiteral("sorting menu")), + diropActionCollection->action(QStringLiteral("show hidden")), + }; newFileAction = new QAction(this); newFileAction->setText(i18n("New File...")); newFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect(newFileAction, &QAction::triggered, this, &FileManager::createNewFile); } void FileManager::createNewFile() { QUrl destUrl = QFileDialog::getSaveFileUrl(KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Create New File")); if (destUrl.isEmpty()) { return; } KJob* job = KIO::storedPut(QByteArray(), destUrl, -1); KJobWidgets::setWindow(job, this); connect(job, &KJob::result, this, &FileManager::fileCreated); } void FileManager::fileCreated(KJob* job) { auto transferJob = qobject_cast(job); Q_ASSERT(transferJob); if (!transferJob->error()) { KDevelop::ICore::self()->documentController()->openDocument( transferJob->url() ); } else { KMessageBox::error(KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Unable to create file '%1'", transferJob->url().toDisplayString(QUrl::PreferLocalFile))); } } void FileManager::syncCurrentDocumentDirectory() { if( KDevelop::IDocument* activeDoc = KDevelop::ICore::self()->documentController()->activeDocument() ) updateNav( activeDoc->url().adjusted(QUrl::RemoveFilename) ); } QList FileManager::toolBarActions() const { return tbActions; } KActionCollection* FileManager::actionCollection() const { return m_actionCollection; } KDirOperator* FileManager::dirOperator() const { return dirop; } KDevFileManagerPlugin* FileManager::plugin() const { return m_plugin; } #include "moc_filemanager.cpp" diff --git a/plugins/filetemplates/filetemplatesplugin.cpp b/plugins/filetemplates/filetemplatesplugin.cpp index d1a3e33374..cfb35a7c81 100644 --- a/plugins/filetemplates/filetemplatesplugin.cpp +++ b/plugins/filetemplates/filetemplatesplugin.cpp @@ -1,317 +1,317 @@ #include "filetemplatesplugin.h" #include "templateclassassistant.h" #include "templatepreviewtoolview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(FileTemplatesFactory, "kdevfiletemplates.json", registerPlugin();) class TemplatePreviewFactory : public KDevelop::IToolViewFactory { public: explicit TemplatePreviewFactory(FileTemplatesPlugin* plugin) : KDevelop::IToolViewFactory() , m_plugin(plugin) { } QWidget* create(QWidget* parent = nullptr) override { return new TemplatePreviewToolView(m_plugin, parent); } QString id() const override { return QStringLiteral("org.kdevelop.TemplateFilePreview"); } Qt::DockWidgetArea defaultPosition() override { return Qt::RightDockWidgetArea; } private: FileTemplatesPlugin* m_plugin; }; FileTemplatesPlugin::FileTemplatesPlugin(QObject* parent, const QVariantList& args) : IPlugin(QStringLiteral("kdevfiletemplates"), parent) , m_model(nullptr) { Q_UNUSED(args); setXMLFile(QStringLiteral("kdevfiletemplates.rc")); QAction* action = actionCollection()->addAction(QStringLiteral("new_from_template")); action->setText(i18n("New From Template...")); action->setIcon( QIcon::fromTheme( QStringLiteral("code-class") ) ); action->setWhatsThis( i18n( "Allows you to create new source code files, such as classes or unit tests, using templates." ) ); action->setStatusTip( i18n( "Create new files from a template" ) ); connect (action, &QAction::triggered, this, &FileTemplatesPlugin::createFromTemplate); m_toolView = new TemplatePreviewFactory(this); core()->uiController()->addToolView(i18n("Template Preview"), m_toolView); } FileTemplatesPlugin::~FileTemplatesPlugin() { } void FileTemplatesPlugin::unload() { core()->uiController()->removeToolView(m_toolView); } ContextMenuExtension FileTemplatesPlugin::contextMenuExtension(Context* context, QWidget* parent) { ContextMenuExtension ext; QUrl fileUrl; if (context->type() == Context::ProjectItemContext) { ProjectItemContext* projectContext = static_cast(context); QList items = projectContext->items(); if (items.size() != 1) { return ext; } QUrl url; ProjectBaseItem* item = items.first(); if (item->folder()) { url = item->path().toUrl(); } else if (item->target()) { url = item->parent()->path().toUrl(); } if (url.isValid()) { QAction* action = new QAction(i18n("Create From Template..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("code-class"))); action->setData(url); connect(action, &QAction::triggered, this, &FileTemplatesPlugin::createFromTemplate); ext.addAction(ContextMenuExtension::FileGroup, action); } if (item->file()) { fileUrl = item->path().toUrl(); } } else if (context->type() == Context::EditorContext) { KDevelop::EditorContext* editorContext = static_cast(context); fileUrl = editorContext->url(); } if (fileUrl.isValid() && determineTemplateType(fileUrl) != NoTemplate) { QAction* action = new QAction(i18n("Show Template Preview"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview"))); action->setData(fileUrl); connect(action, &QAction::triggered, this, &FileTemplatesPlugin::previewTemplate); ext.addAction(ContextMenuExtension::ExtensionGroup, action); } return ext; } QString FileTemplatesPlugin::name() const { return i18n("File Templates"); } QIcon FileTemplatesPlugin::icon() const { return QIcon::fromTheme(QStringLiteral("code-class")); } QAbstractItemModel* FileTemplatesPlugin::templatesModel() { if(!m_model) { m_model = new TemplatesModel(QStringLiteral("kdevfiletemplates"), this); } return m_model; } QString FileTemplatesPlugin::knsConfigurationFile() const { return QStringLiteral("kdevfiletemplates.knsrc"); } QStringList FileTemplatesPlugin::supportedMimeTypes() const { - QStringList types; - types << QStringLiteral("application/x-desktop"); - types << QStringLiteral("application/x-bzip-compressed-tar"); - types << QStringLiteral("application/zip"); + const QStringList types{ + QStringLiteral("application/x-desktop"), + QStringLiteral("application/x-bzip-compressed-tar"), + QStringLiteral("application/zip"), + }; return types; } void FileTemplatesPlugin::reload() { templatesModel(); m_model->refresh(); } void FileTemplatesPlugin::loadTemplate(const QString& fileName) { templatesModel(); m_model->loadTemplateFile(fileName); } void FileTemplatesPlugin::createFromTemplate() { QUrl baseUrl; if (QAction* action = qobject_cast(sender())) { baseUrl = action->data().toUrl(); } if (!baseUrl.isValid()) { // fall-back to currently active document's parent directory IDocument* doc = ICore::self()->documentController()->activeDocument(); if (doc && doc->url().isValid()) { baseUrl = doc->url().adjusted(QUrl::RemoveFilename); } } if (!baseUrl.isValid()) { // fall-back to currently selected project's or item's base directory ProjectItemContext* projectContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (projectContext) { const QList items = projectContext->items(); if (items.size() == 1) { ProjectBaseItem* item = items.at(0); if (item->folder()) { baseUrl = item->path().toUrl(); } else if (item->target()) { baseUrl = item->parent()->path().toUrl(); } } } } if (!baseUrl.isValid()) { // fall back to base directory of currently open project, if there is only one const QList projects = ICore::self()->projectController()->projects(); if (projects.size() == 1) { baseUrl = projects.at(0)->path().toUrl(); } } if (!baseUrl.isValid()) { // last resort baseUrl = ICore::self()->projectController()->projectsBaseDirectory(); } TemplateClassAssistant* assistant = new TemplateClassAssistant(QApplication::activeWindow(), baseUrl); assistant->setAttribute(Qt::WA_DeleteOnClose); assistant->show(); } FileTemplatesPlugin::TemplateType FileTemplatesPlugin::determineTemplateType(const QUrl& url) { QDir dir(url.toLocalFile()); /* * Search for a description file in the url's directory. * If it is not found there, try cascading up a maximum of 5 directories. */ int level = 0; while (dir.cdUp() && level < 5) { - QStringList filters; - filters << QStringLiteral("*.kdevtemplate") << QStringLiteral("*.desktop"); + const QStringList filters{QStringLiteral("*.kdevtemplate"), QStringLiteral("*.desktop")}; foreach (const QString& entry, dir.entryList(filters)) { qCDebug(PLUGIN_FILETEMPLATES) << "Trying entry" << entry; /* * This logic is not perfect, but it works for most cases. * * Project template description files usually have the suffix * ".kdevtemplate", so those are easy to find. For project templates, * all the files in the directory are template files. * * On the other hand, file templates use the generic suffix ".desktop". * Fortunately, those explicitly list input and output files, so we * only match the explicitly listed files */ if (entry.endsWith(QLatin1String(".kdevtemplate"))) { return ProjectTemplate; } KConfig* config = new KConfig(dir.absoluteFilePath(entry), KConfig::SimpleConfig); KConfigGroup group = config->group("General"); qCDebug(PLUGIN_FILETEMPLATES) << "General group keys:" << group.keyList(); if (!group.hasKey("Name") || !group.hasKey("Category")) { continue; } if (group.hasKey("Files")) { qCDebug(PLUGIN_FILETEMPLATES) << "Group has files " << group.readEntry("Files", QStringList()); foreach (const QString& outputFile, group.readEntry("Files", QStringList())) { if (dir.absoluteFilePath(config->group(outputFile).readEntry("File")) == url.toLocalFile()) { return FileTemplate; } } } if (group.hasKey("ShowFilesAfterGeneration")) { return ProjectTemplate; } } ++level; } return NoTemplate; } void FileTemplatesPlugin::previewTemplate() { QAction* action = qobject_cast(sender()); if (!action || !action->data().toUrl().isValid()) { return; } TemplatePreviewToolView* preview = qobject_cast(core()->uiController()->findToolView(i18n("Template Preview"), m_toolView)); if (!preview) { return; } core()->documentController()->activateDocument(core()->documentController()->openDocument(action->data().toUrl())); } #include "filetemplatesplugin.moc" diff --git a/plugins/filetemplates/templateclassassistant.cpp b/plugins/filetemplates/templateclassassistant.cpp index 493ce2b896..142648fab9 100644 --- a/plugins/filetemplates/templateclassassistant.cpp +++ b/plugins/filetemplates/templateclassassistant.cpp @@ -1,595 +1,597 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templateclassassistant.h" #include "templateselectionpage.h" #include "templateoptionspage.h" #include "classmemberspage.h" #include "classidentifierpage.h" #include "overridespage.h" #include "licensepage.h" #include "outputpage.h" #include "testcasespage.h" #include "defaultcreateclasshelper.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 #define REMOVE_PAGE(name) \ if (d->name##Page) \ { \ removePage(d->name##Page); \ d->name##Page = nullptr; \ d->name##PageWidget = nullptr; \ } #define ZERO_PAGE(name) \ d->name##Page = nullptr; \ d->name##PageWidget = nullptr; using namespace KDevelop; class KDevelop::TemplateClassAssistantPrivate { public: explicit TemplateClassAssistantPrivate(const QUrl& baseUrl); ~TemplateClassAssistantPrivate(); void addFilesToTarget (const QHash& fileUrls); KPageWidgetItem* templateSelectionPage; KPageWidgetItem* classIdentifierPage; KPageWidgetItem* overridesPage; KPageWidgetItem* membersPage; KPageWidgetItem* testCasesPage; KPageWidgetItem* licensePage; KPageWidgetItem* templateOptionsPage; KPageWidgetItem* outputPage; KPageWidgetItem* dummyPage; TemplateSelectionPage* templateSelectionPageWidget; ClassIdentifierPage* classIdentifierPageWidget; OverridesPage* overridesPageWidget; ClassMembersPage* membersPageWidget; TestCasesPage* testCasesPageWidget; LicensePage* licensePageWidget; TemplateOptionsPage* templateOptionsPageWidget; OutputPage* outputPageWidget; QUrl baseUrl; SourceFileTemplate fileTemplate; ICreateClassHelper* helper; TemplateClassGenerator* generator; TemplateRenderer* renderer; QVariantHash templateOptions; }; TemplateClassAssistantPrivate::TemplateClassAssistantPrivate(const QUrl& baseUrl) : baseUrl(baseUrl) , helper(nullptr) , generator(nullptr) , renderer(nullptr) { } TemplateClassAssistantPrivate::~TemplateClassAssistantPrivate() { delete helper; if (generator) { delete generator; } else { // if we got a generator, it should keep ownership of the renderer // otherwise, we created a templaterenderer on our own delete renderer; } } void TemplateClassAssistantPrivate::addFilesToTarget (const QHash< QString, QUrl >& fileUrls) { // Add the generated files to a target, if one is found QUrl url = baseUrl; if (!url.isValid()) { // This was probably not launched from the project manager view // Still, we try to find the common URL where the generated files are located if (!fileUrls.isEmpty()) { url = fileUrls.constBegin().value().adjusted(QUrl::RemoveFilename); } } qCDebug(PLUGIN_FILETEMPLATES) << "Searching for targets with URL" << url; IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if (!project || !project->buildSystemManager()) { qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project found"; return; } QList items = project->itemsForPath(IndexedString(url)); if (items.isEmpty()) { qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project items found"; return; } QList targets; ProjectTargetItem* target = nullptr; foreach (ProjectBaseItem* item, items) { if (ProjectTargetItem* target = item->target()) { targets << target; } } if (targets.isEmpty()) { // If no target was explicitly found yet, try all the targets in the current folder + targets.reserve(items.size()); foreach (ProjectBaseItem* item, items) { targets << item->targetList(); } } if (targets.isEmpty()) { // If still no targets, we traverse the tree up to the first directory with targets ProjectBaseItem* item = items.first()->parent(); while (targets.isEmpty() && item) { targets = item->targetList(); item = item->parent(); } } if (targets.size() == 1) { qCDebug(PLUGIN_FILETEMPLATES) << "Only one candidate target," << targets.first()->text() << ", using it"; target = targets.first(); } else if (targets.size() > 1) { // More than one candidate target, show the chooser dialog ScopedDialog d; auto mainLayout = new QVBoxLayout(d); mainLayout->addWidget(new QLabel(i18n("Choose one target to add the file or cancel if you do not want to do so."))); QListWidget* targetsWidget = new QListWidget(d); targetsWidget->setSelectionMode(QAbstractItemView::SingleSelection); foreach(ProjectTargetItem* target, targets) { targetsWidget->addItem(target->text()); } targetsWidget->setCurrentRow(0); mainLayout->addWidget(targetsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); d->connect(buttonBox, &QDialogButtonBox::accepted, d.data(), &QDialog::accept); d->connect(buttonBox, &QDialogButtonBox::rejected, d.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if(d->exec() == QDialog::Accepted) { if (!targetsWidget->selectedItems().isEmpty()) { target = targets[targetsWidget->currentRow()]; } else { qCDebug(PLUGIN_FILETEMPLATES) << "Did not select anything, not adding to a target"; return; } } else { qCDebug(PLUGIN_FILETEMPLATES) << "Canceled select target dialog, not adding to a target"; return; } } else { // No target, not doing anything qCDebug(PLUGIN_FILETEMPLATES) << "No possible targets for URL" << url; return; } Q_ASSERT(target); QList fileItems; foreach (const QUrl &fileUrl, fileUrls) { foreach (ProjectBaseItem* item, project->itemsForPath(IndexedString(KIO::upUrl(fileUrl)))) { if (ProjectFolderItem* folder = item->folder()) { ///FIXME: use Path instead of QUrl in the template class assistant if (ProjectFileItem* file = project->projectFileManager()->addFile(Path(fileUrl), folder)) { fileItems << file; break; } } } } if (!fileItems.isEmpty()) { project->buildSystemManager()->addFilesToTarget(fileItems, target); } } TemplateClassAssistant::TemplateClassAssistant(QWidget* parent, const QUrl& baseUrl) : KAssistantDialog(parent) , d(new TemplateClassAssistantPrivate(baseUrl)) { ZERO_PAGE(templateSelection) ZERO_PAGE(templateOptions) ZERO_PAGE(members) ZERO_PAGE(classIdentifier) ZERO_PAGE(overrides) ZERO_PAGE(license) ZERO_PAGE(output) ZERO_PAGE(testCases) setup(); } TemplateClassAssistant::~TemplateClassAssistant() { delete d; } void TemplateClassAssistant::setup() { if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template in %1", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(i18n("Create Files from Template")); } d->templateSelectionPageWidget = new TemplateSelectionPage(this); connect(this, &TemplateClassAssistant::accepted, d->templateSelectionPageWidget, &TemplateSelectionPage::saveConfig); d->templateSelectionPage = addPage(d->templateSelectionPageWidget, i18n("Language and Template")); d->templateSelectionPage->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); // KAssistantDialog creates a help button by default, no option to prevent that QPushButton *helpButton = button(QDialogButtonBox::Help); if (helpButton) { buttonBox()->removeButton(helpButton); delete helpButton; } } void TemplateClassAssistant::templateChosen(const QString& templateDescription) { d->fileTemplate.setTemplateDescription(templateDescription); const auto type = d->fileTemplate.type(); d->generator = nullptr; if (!d->fileTemplate.isValid()) { return; } qCDebug(PLUGIN_FILETEMPLATES) << "Selected template" << templateDescription << "of type" << type; removePage(d->dummyPage); if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template %1 in %2", d->fileTemplate.name(), d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(xi18n("Create Files from Template %1", d->fileTemplate.name())); } if (type == QLatin1String("Class")) { d->classIdentifierPageWidget = new ClassIdentifierPage(this); d->classIdentifierPage = addPage(d->classIdentifierPageWidget, i18n("Class Basics")); d->classIdentifierPage->setIcon(QIcon::fromTheme(QStringLiteral("classnew"))); connect(d->classIdentifierPageWidget, &ClassIdentifierPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->classIdentifierPage, false); d->overridesPageWidget = new OverridesPage(this); d->overridesPage = addPage(d->overridesPageWidget, i18n("Override Methods")); d->overridesPage->setIcon(QIcon::fromTheme(QStringLiteral("code-class"))); setValid(d->overridesPage, true); d->membersPageWidget = new ClassMembersPage(this); d->membersPage = addPage(d->membersPageWidget, i18n("Class Members")); d->membersPage->setIcon(QIcon::fromTheme(QStringLiteral("field"))); setValid(d->membersPage, true); d->helper = nullptr; QString languageName = d->fileTemplate.languageName(); auto language = ICore::self()->languageController()->language(languageName); if (language) { d->helper = language->createClassHelper(); } if (!d->helper) { qCDebug(PLUGIN_FILETEMPLATES) << "No class creation helper for language" << languageName; d->helper = new DefaultCreateClassHelper; } d->generator = d->helper->createGenerator(d->baseUrl); Q_ASSERT(d->generator); d->generator->setTemplateDescription(d->fileTemplate); d->renderer = d->generator->renderer(); } else { if (type == QLatin1String("Test")) { d->testCasesPageWidget = new TestCasesPage(this); d->testCasesPage = addPage(d->testCasesPageWidget, i18n("Test Cases")); connect(d->testCasesPageWidget, &TestCasesPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->testCasesPage, false); } d->renderer = new TemplateRenderer; d->renderer->setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); } d->licensePageWidget = new LicensePage(this); d->licensePage = addPage(d->licensePageWidget, i18n("License")); d->licensePage->setIcon(QIcon::fromTheme(QStringLiteral("text-x-copying"))); setValid(d->licensePage, true); d->outputPageWidget = new OutputPage(this); d->outputPageWidget->prepareForm(d->fileTemplate); d->outputPage = addPage(d->outputPageWidget, i18n("Output")); d->outputPage->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); connect(d->outputPageWidget, &OutputPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); setValid(d->outputPage, false); if (d->fileTemplate.hasCustomOptions()) { qCDebug(PLUGIN_FILETEMPLATES) << "Class generator has custom options"; d->templateOptionsPageWidget = new TemplateOptionsPage(this); d->templateOptionsPage = insertPage(d->outputPage, d->templateOptionsPageWidget, i18n("Template Options")); } setCurrentPage(d->templateSelectionPage); } void TemplateClassAssistant::next() { qCDebug(PLUGIN_FILETEMPLATES) << currentPage()->name() << currentPage()->header(); if (currentPage() == d->templateSelectionPage) { // We have chosen the template // Depending on the template's language, we can now create a helper QString description = d->templateSelectionPageWidget->selectedTemplate(); templateChosen(description); if (!d->fileTemplate.isValid()) { return; } } else if (currentPage() == d->classIdentifierPage) { d->generator->setIdentifier(d->classIdentifierPageWidget->identifier()); d->generator->setBaseClasses(d->classIdentifierPageWidget->inheritanceList()); } else if (currentPage() == d->overridesPage) { ClassDescription desc = d->generator->description(); desc.methods.clear(); - foreach (const DeclarationPointer& declaration, d->overridesPageWidget->selectedOverrides()) - { + const auto overrides = d->overridesPageWidget->selectedOverrides(); + desc.methods.reserve(overrides.size()); + for (const DeclarationPointer& declaration : overrides) { desc.methods << FunctionDescription(declaration); } d->generator->setDescription(desc); } else if (currentPage() == d->membersPage) { ClassDescription desc = d->generator->description(); desc.members = d->membersPageWidget->members(); d->generator->setDescription(desc); } else if (currentPage() == d->licensePage) { if (d->generator) { d->generator->setLicense(d->licensePageWidget->license()); } else { d->renderer->addVariable(QStringLiteral("license"), d->licensePageWidget->license()); } } else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) { if (d->generator) { d->generator->addVariables(d->templateOptionsPageWidget->templateOptions()); } else { d->renderer->addVariables(d->templateOptionsPageWidget->templateOptions()); } } else if (currentPage() == d->testCasesPage) { d->renderer->addVariable(QStringLiteral("name"), d->testCasesPageWidget->name()); d->renderer->addVariable(QStringLiteral("testCases"), d->testCasesPageWidget->testCases()); } KAssistantDialog::next(); if (currentPage() == d->classIdentifierPage) { d->classIdentifierPageWidget->setInheritanceList(d->fileTemplate.defaultBaseClasses()); } else if (currentPage() == d->membersPage) { d->membersPageWidget->setMembers(d->generator->description().members); } else if (currentPage() == d->overridesPage) { d->overridesPageWidget->clear(); d->overridesPageWidget->addCustomDeclarations(i18n("Default"), d->helper->defaultMethods(d->generator->name())); d->overridesPageWidget->addBaseClasses(d->generator->directBaseClasses(), d->generator->allBaseClasses()); } else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) { d->templateOptionsPageWidget->load(d->fileTemplate, d->renderer); } else if (currentPage() == d->outputPage) { d->outputPageWidget->loadFileTemplate(d->fileTemplate, d->baseUrl, d->renderer); } if (auto* pageFocus = dynamic_cast(currentPage()->widget())) { pageFocus->setFocusToFirstEditWidget(); } } void TemplateClassAssistant::back() { KAssistantDialog::back(); if (currentPage() == d->templateSelectionPage) { REMOVE_PAGE(classIdentifier) REMOVE_PAGE(overrides) REMOVE_PAGE(members) REMOVE_PAGE(testCases) REMOVE_PAGE(output) REMOVE_PAGE(templateOptions) REMOVE_PAGE(license) delete d->helper; d->helper = nullptr; if (d->generator) { delete d->generator; } else { delete d->renderer; } d->generator = nullptr; d->renderer = nullptr; if (d->baseUrl.isValid()) { setWindowTitle(xi18n("Create Files from Template in %1", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); } else { setWindowTitle(i18n("Create Files from Template")); } d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); } } void TemplateClassAssistant::accept() { // next() is not called for the last page (when the user clicks Finish), so we have to set output locations here QHash fileUrls = d->outputPageWidget->fileUrls(); QHash filePositions = d->outputPageWidget->filePositions(); DocumentChangeSet changes; if (d->generator) { QHash::const_iterator it = fileUrls.constBegin(); for (; it != fileUrls.constEnd(); ++it) { d->generator->setFileUrl(it.key(), it.value()); d->generator->setFilePosition(it.key(), filePositions.value(it.key())); } d->generator->addVariables(d->templateOptions); changes = d->generator->generate(); } else { changes = d->renderer->renderFileTemplate(d->fileTemplate, d->baseUrl, fileUrls); } d->addFilesToTarget(fileUrls); changes.applyAllChanges(); // Open the generated files in the editor foreach (const QUrl& url, fileUrls) { ICore::self()->documentController()->openDocument(url); } KAssistantDialog::accept(); } void TemplateClassAssistant::setCurrentPageValid(bool valid) { setValid(currentPage(), valid); } QUrl TemplateClassAssistant::baseUrl() const { return d->baseUrl; } diff --git a/plugins/filetemplates/templateoptionspage.cpp b/plugins/filetemplates/templateoptionspage.cpp index 80eb9fe8c0..2215557090 100644 --- a/plugins/filetemplates/templateoptionspage.cpp +++ b/plugins/filetemplates/templateoptionspage.cpp @@ -1,175 +1,178 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templateoptionspage.h" #include "templateclassassistant.h" #include "debug.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; class KDevelop::TemplateOptionsPagePrivate { public: QVector entries; QHash controls; QHash typeProperties; QWidget *firstEditWidget; QList groupBoxes; }; TemplateOptionsPage::TemplateOptionsPage(QWidget* parent) : QWidget(parent) , d(new TemplateOptionsPagePrivate) { d->firstEditWidget = nullptr; d->typeProperties.insert(QStringLiteral("String"), "text"); d->typeProperties.insert(QStringLiteral("Enum"), "currentText"); d->typeProperties.insert(QStringLiteral("Int"), "value"); d->typeProperties.insert(QStringLiteral("Bool"), "checked"); } TemplateOptionsPage::~TemplateOptionsPage() { delete d; } void TemplateOptionsPage::load(const SourceFileTemplate& fileTemplate, TemplateRenderer* renderer) { // TODO: keep any old changed values, as it comes by surprise to have them lost // when going back and forward // clear anything as there is on reentering the page d->entries.clear(); d->controls.clear(); // clear any old option group boxes & the base layout d->firstEditWidget = nullptr; qDeleteAll(d->groupBoxes); d->groupBoxes.clear(); delete layout(); QVBoxLayout* layout = new QVBoxLayout(); - for (const auto& optionGroup : fileTemplate.customOptions(renderer)) { + const auto customOptions = fileTemplate.customOptions(renderer); + d->groupBoxes.reserve(customOptions.size()); + d->entries.reserve(customOptions.size()); + for (const auto& optionGroup : customOptions) { QGroupBox* box = new QGroupBox(this); d->groupBoxes.append(box); box->setTitle(optionGroup.name); QFormLayout* formLayout = new QFormLayout; d->entries << optionGroup.options; for (const auto& entry : optionGroup.options) { QWidget* control = nullptr; const QString type = entry.type; if (type == QLatin1String("String")) { control = new QLineEdit(entry.value.toString(), box); } else if (type == QLatin1String("Enum")) { auto input = new QComboBox(box); input->addItems(entry.values); input->setCurrentText(entry.value.toString()); control = input; } else if (type == QLatin1String("Int")) { auto input = new QSpinBox(box); input->setValue(entry.value.toInt()); if (!entry.minValue.isEmpty()) { input->setMinimum(entry.minValue.toInt()); } if (!entry.maxValue.isEmpty()) { input->setMaximum(entry.maxValue.toInt()); } control = input; } else if (type == QLatin1String("Bool")) { bool checked = (QString::compare(entry.value.toString(), QStringLiteral("true"), Qt::CaseInsensitive) == 0); QCheckBox* checkBox = new QCheckBox(box); checkBox->setCheckState(checked ? Qt::Checked : Qt::Unchecked); control = checkBox; } else { qCDebug(PLUGIN_FILETEMPLATES) << "Unrecognized option type" << entry.type; } if (control) { const QString entryLabelText = i18n("%1:", entry.label); QLabel* label = new QLabel(entryLabelText, box); formLayout->addRow(label, control); d->controls.insert(entry.name, control); if (d->firstEditWidget == nullptr) { d->firstEditWidget = control; } } } box->setLayout(formLayout); layout->addWidget(box); } layout->addStretch(); setLayout(layout); } QVariantHash TemplateOptionsPage::templateOptions() const { QVariantHash values; foreach (const SourceFileTemplate::ConfigOption& entry, d->entries) { Q_ASSERT(d->controls.contains(entry.name)); Q_ASSERT(d->typeProperties.contains(entry.type)); values.insert(entry.name, d->controls[entry.name]->property(d->typeProperties[entry.type])); } qCDebug(PLUGIN_FILETEMPLATES) << values.size() << d->entries.size(); return values; } void TemplateOptionsPage::setFocusToFirstEditWidget() { if (d->firstEditWidget) { d->firstEditWidget->setFocus(); } } diff --git a/plugins/filetemplates/templateselectionpage.cpp b/plugins/filetemplates/templateselectionpage.cpp index ae728ed36a..1833082112 100644 --- a/plugins/filetemplates/templateselectionpage.cpp +++ b/plugins/filetemplates/templateselectionpage.cpp @@ -1,276 +1,276 @@ /* This file is part of KDevelop Copyright 2012 Miha Čančula This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templateselectionpage.h" #include "templateclassassistant.h" #include "templatepreview.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include #include #include "ui_templateselection.h" #include #include #include #include #include #include #include using namespace KDevelop; static const char LastUsedTemplateEntry[] = "LastUsedTemplate"; static const char FileTemplatesGroup[] = "SourceFileTemplates"; class KDevelop::TemplateSelectionPagePrivate { public: explicit TemplateSelectionPagePrivate(TemplateSelectionPage* page_) : page(page_) {} TemplateSelectionPage* page; Ui::TemplateSelection* ui; QString selectedTemplate; TemplateClassAssistant* assistant; TemplatesModel* model; void currentTemplateChanged(const QModelIndex& index); void getMoreClicked(); void loadFileClicked(); void previewTemplate(const QString& templateFile); }; void TemplateSelectionPagePrivate::currentTemplateChanged(const QModelIndex& index) { // delete preview tabs if (!index.isValid() || index.model()->hasChildren(index)) { // invalid or has child assistant->setValid(assistant->currentPage(), false); ui->previewLabel->setVisible(false); ui->tabWidget->setVisible(false); } else { selectedTemplate = model->data(index, TemplatesModel::DescriptionFileRole).toString(); assistant->setValid(assistant->currentPage(), true); previewTemplate(selectedTemplate); ui->previewLabel->setVisible(true); ui->tabWidget->setVisible(true); ui->previewLabel->setText(i18nc("%1: template comment", "Preview: %1", index.data(TemplatesModel::CommentRole).toString())); } } void TemplateSelectionPagePrivate::previewTemplate(const QString& file) { SourceFileTemplate fileTemplate(file); if (!fileTemplate.isValid() || fileTemplate.outputFiles().isEmpty()) { return; } TemplatePreviewRenderer renderer; // set default option values if (fileTemplate.hasCustomOptions()) { QVariantHash extraVars; for (const auto& optionGroup : fileTemplate.customOptions(&renderer)) { for (const auto& entry : optionGroup.options) { extraVars[entry.name] = entry.value; } } renderer.addVariables(extraVars); } renderer.setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); QTemporaryDir dir; QUrl base = QUrl::fromLocalFile(dir.path() + QLatin1Char('/')); QHash fileUrls; foreach(const SourceFileTemplate::OutputFile& out, fileTemplate.outputFiles()) { QUrl url = base.resolved(QUrl(renderer.render(out.outputName))); fileUrls.insert(out.identifier, url); } DocumentChangeSet changes = renderer.renderFileTemplate(fileTemplate, base, fileUrls); changes.setActivationPolicy(DocumentChangeSet::DoNotActivate); changes.setUpdateHandling(DocumentChangeSet::NoUpdate); DocumentChangeSet::ChangeResult result = changes.applyAllChanges(); if (!result) { return; } int idx = 0; foreach(const SourceFileTemplate::OutputFile& out, fileTemplate.outputFiles()) { TemplatePreview* preview = nullptr; if (ui->tabWidget->count() > idx) { // reuse existing tab preview = qobject_cast(ui->tabWidget->widget(idx)); ui->tabWidget->setTabText(idx, out.label); Q_ASSERT(preview); } else { // create new tabs on demand preview = new TemplatePreview(page); ui->tabWidget->addTab(preview, out.label); } preview->document()->openUrl(fileUrls.value(out.identifier)); ++idx; } // remove superfluous tabs from last time while (ui->tabWidget->count() > fileUrls.size()) { delete ui->tabWidget->widget(fileUrls.size()); } return; } void TemplateSelectionPagePrivate::getMoreClicked() { KNS3::DownloadDialog(QStringLiteral("kdevfiletemplates.knsrc"), ui->view).exec(); model->refresh(); } void TemplateSelectionPagePrivate::loadFileClicked() { const QStringList filters{ QStringLiteral("application/x-desktop"), QStringLiteral("application/x-bzip-compressed-tar"), QStringLiteral("application/zip") }; ScopedDialog dlg(page); dlg->setMimeTypeFilters(filters); dlg->setFileMode(QFileDialog::ExistingFiles); if (!dlg->exec()) { return; } foreach(const QString& fileName, dlg->selectedFiles()) { QString destination = model->loadTemplateFile(fileName); QModelIndexList indexes = model->templateIndexes(destination); int n = indexes.size(); if (n > 1) { ui->view->setCurrentIndex(indexes[1]); } } } void TemplateSelectionPage::saveConfig() { KSharedConfigPtr config; if (IProject* project = ICore::self()->projectController()->findProjectForUrl(d->assistant->baseUrl())) { config = project->projectConfiguration(); } else { config = ICore::self()->activeSession()->config(); } KConfigGroup group(config, FileTemplatesGroup); group.writeEntry(LastUsedTemplateEntry, d->selectedTemplate); group.sync(); } TemplateSelectionPage::TemplateSelectionPage(TemplateClassAssistant* parent) : QWidget(parent) , d(new TemplateSelectionPagePrivate(this)) { d->assistant = parent; d->ui = new Ui::TemplateSelection; d->ui->setupUi(this); d->model = new TemplatesModel(QStringLiteral("kdevfiletemplates"), this); d->model->refresh(); d->ui->view->setLevels(3); - d->ui->view->setHeaderLabels(QStringList() << i18n("Language") << i18n("Framework") << i18n("Template")); + d->ui->view->setHeaderLabels(QStringList{i18n("Language"), i18n("Framework"), i18n("Template")}); d->ui->view->setModel(d->model); connect(d->ui->view, &MultiLevelListView::currentIndexChanged, this, [&] (const QModelIndex& index) { d->currentTemplateChanged(index); }); QModelIndex templateIndex; while (d->model->hasIndex(0, 0, templateIndex)) { templateIndex = d->model->index(0, 0, templateIndex); } KSharedConfigPtr config; if (IProject* project = ICore::self()->projectController()->findProjectForUrl(d->assistant->baseUrl())) { config = project->projectConfiguration(); } else { config = ICore::self()->activeSession()->config(); } KConfigGroup group(config, FileTemplatesGroup); QString lastTemplate = group.readEntry(LastUsedTemplateEntry); QModelIndexList indexes = d->model->match(d->model->index(0, 0), TemplatesModel::DescriptionFileRole, lastTemplate, 1, Qt::MatchRecursive); if (!indexes.isEmpty()) { templateIndex = indexes.first(); } d->ui->view->setCurrentIndex(templateIndex); QPushButton* getMoreButton = new QPushButton(i18n("Get More Templates..."), d->ui->view); getMoreButton->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); connect (getMoreButton, &QPushButton::clicked, this, [&] { d->getMoreClicked(); }); d->ui->view->addWidget(0, getMoreButton); QPushButton* loadButton = new QPushButton(QIcon::fromTheme(QStringLiteral("application-x-archive")), i18n("Load Template From File"), d->ui->view); connect (loadButton, &QPushButton::clicked, this, [&] { d->loadFileClicked(); }); d->ui->view->addWidget(0, loadButton); d->ui->view->setContentsMargins(0, 0, 0, 0); } TemplateSelectionPage::~TemplateSelectionPage() { delete d->ui; delete d; } QSize TemplateSelectionPage::minimumSizeHint() const { return QSize(400, 600); } QString TemplateSelectionPage::selectedTemplate() const { return d->selectedTemplate; } #include "moc_templateselectionpage.cpp" diff --git a/plugins/gdb/debugsession.cpp b/plugins/gdb/debugsession.cpp index 87224e1365..b5ff64ce56 100644 --- a/plugins/gdb/debugsession.cpp +++ b/plugins/gdb/debugsession.cpp @@ -1,323 +1,324 @@ /* * GDB Debugger Support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * 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 "debugsession.h" #include "debuglog.h" #include "debuggerplugin.h" #include "gdb.h" #include "gdbbreakpointcontroller.h" #include "gdbframestackmodel.h" #include "mi/micommand.h" #include "stty.h" #include "variablecontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI::GDB; using namespace KDevMI::MI; using namespace KDevelop; DebugSession::DebugSession(CppDebuggerPlugin *plugin) : MIDebugSession(plugin) { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new GdbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } void DebugSession::setAutoDisableASLR(bool enable) { m_autoDisableASLR = enable; } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } GdbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } GdbDebugger *DebugSession::createDebugger() const { return new GdbDebugger; } void DebugSession::initializeDebugger() { //addCommand(new GDBCommand(GDBMI::EnableTimings, "yes")); addCommand(new CliCommand(MI::GdbShow, QStringLiteral("version"), this, &DebugSession::handleVersion)); // This makes gdb pump a variable out on one line. addCommand(MI::GdbSet, QStringLiteral("width 0")); addCommand(MI::GdbSet, QStringLiteral("height 0")); addCommand(MI::SignalHandle, QStringLiteral("SIG32 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG41 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG42 pass nostop noprint")); addCommand(MI::SignalHandle, QStringLiteral("SIG43 pass nostop noprint")); addCommand(MI::EnablePrettyPrinting); addCommand(MI::GdbSet, QStringLiteral("charset UTF-8")); addCommand(MI::GdbSet, QStringLiteral("print sevenbit-strings off")); QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevgdb/printers/gdbinit")); if (!fileName.isEmpty()) { QFileInfo fileInfo(fileName); QString quotedPrintersPath = fileInfo.dir().path() .replace('\\', QLatin1String("\\\\")) .replace('"', QLatin1String("\\\"")); addCommand(MI::NonMI, QStringLiteral("python sys.path.insert(0, \"%0\")").arg(quotedPrintersPath)); addCommand(MI::NonMI, "source " + fileName); } // GDB can't disable ASLR on CI server. addCommand(MI::GdbSet, QStringLiteral("disable-randomization %1").arg(m_autoDisableASLR ? "on" : "off")); qCDebug(DEBUGGERGDB) << "Initialized GDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &) { // Read Configuration values KConfigGroup grp = cfg->config(); bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); bool displayStaticMembers = grp.readEntry(Config::StaticMembersEntry, false); bool asmDemangle = grp.readEntry(Config::DemangleNamesEntry, true); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; foreach (Breakpoint *b, m->breakpoints()) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); if (displayStaticMembers) { addCommand(MI::GdbSet, QStringLiteral("print static-members on")); } else { addCommand(MI::GdbSet, QStringLiteral("print static-members off")); } if (asmDemangle) { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle on")); } else { addCommand(MI::GdbSet, QStringLiteral("print asm-demangle off")); } // Set the environment variables const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { qCWarning(DEBUGGERGDB) << i18n("No environment profile specified, looks like a broken " "configuration, please check run configuration '%1'. " "Using default environment profile.", cfg->name()); envProfileName = environmentProfiles.defaultProfileName(); } for (const auto &envvar : environmentProfiles.createEnvironment(envProfileName, {})) { addCommand(GdbSet, "environment " + envvar); } qCDebug(DEBUGGERGDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(KDevelop::ILaunchConfiguration *cfg, IExecutePlugin *, const QString &executable) { qCDebug(DEBUGGERGDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); QUrl configGdbScript = grp.readEntry(Config::RemoteGdbConfigEntry, QUrl()); QUrl runShellScript = grp.readEntry(Config::RemoteGdbShellEntry, QUrl()); QUrl runGdbScript = grp.readEntry(Config::RemoteGdbRunEntry, QUrl()); // handle remote debug if (configGdbScript.isValid()) { addCommand(MI::NonMI, "source " + KShell::quoteArg(configGdbScript.toLocalFile())); } // FIXME: have a check box option that controls remote debugging if (runShellScript.isValid()) { // Special for remote debug, the remote inferior is started by this shell script QByteArray tty(m_tty->getSlave().toLatin1()); QByteArray options = QByteArray(">") + tty + QByteArray(" 2>&1 <") + tty; QProcess *proc = new QProcess; - QStringList arguments; - arguments << QStringLiteral("-c") << KShell::quoteArg(runShellScript.toLocalFile()) + - ' ' + KShell::quoteArg(executable) + QString::fromLatin1(options); + const QStringList arguments{ + QStringLiteral("-c"), + KShell::quoteArg(runShellScript.toLocalFile()) + QLatin1Char(' ') + KShell::quoteArg(executable) + QString::fromLatin1(options), + }; qCDebug(DEBUGGERGDB) << "starting sh" << arguments; proc->start(QStringLiteral("sh"), arguments); //PORTING TODO QProcess::DontCare); } if (runGdbScript.isValid()) { // Special for remote debug, gdb script at run is requested, to connect to remote inferior // Race notice: wait for the remote gdbserver/executable // - but that might be an issue for this script to handle... // Note: script could contain "run" or "continue" // Future: the shell script should be able to pass info (like pid) // to the gdb script... addCommand(new SentinelCommand([this, runGdbScript]() { breakpointController()->initSendBreakpoints(); breakpointController()->setDeleteDuplicateBreakpoints(true); qCDebug(DEBUGGERGDB) << "Running gdb script " << KShell::quoteArg(runGdbScript.toLocalFile()); addCommand(MI::NonMI, "source " + KShell::quoteArg(runGdbScript.toLocalFile()), [this](const MI::ResultRecord&) { breakpointController()->setDeleteDuplicateBreakpoints(false); }, CmdMaybeStartsRunning); raiseEvent(connected_to_program); }, CmdMaybeStartsRunning)); } else { // normal local debugging addCommand(MI::FileExecAndSymbols, KShell::quoteArg(executable), this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new SentinelCommand([this]() { breakpointController()->initSendBreakpoints(); addCommand(MI::ExecRun, QString(), CmdMaybeStartsRunning); }, CmdMaybeStartsRunning)); } return true; } bool DebugSession::loadCoreFile(KDevelop::ILaunchConfiguration*, const QString& debugee, const QString& corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(NonMI, "core " + corefile, this, &DebugSession::handleCoreFile, CmdHandlesError); return true; } void DebugSession::handleVersion(const QStringList& s) { qCDebug(DEBUGGERGDB) << s.first(); // minimal version is 7.0,0 QRegExp rx("([7-9]+)\\.([0-9]+)(\\.([0-9]+))?"); int idx = rx.indexIn(s.first()); if (idx == -1) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } KMessageBox::error( qApp->activeWindow(), i18n("You need gdb 7.0.0 or higher.
    " "You are using: %1", s.first()), i18n("gdb error")); stopDebugger(); } } void DebugSession::handleFileExecAndSymbols(const ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not start debugger:
    ")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleCoreFile(const ResultRecord& r) { if (r.reason != QLatin1String("error")) { setDebuggerStateOn(s_programExited | s_core); } else { KMessageBox::error( qApp->activeWindow(), i18n("Failed to load core file" "

    Debugger reported the following error:" "

    %1", r["msg"].literal()), i18n("Startup error")); stopDebugger(); } } diff --git a/plugins/gdb/gdboutputwidget.cpp b/plugins/gdb/gdboutputwidget.cpp index 54755f4fb5..1c40ff544e 100644 --- a/plugins/gdb/gdboutputwidget.cpp +++ b/plugins/gdb/gdboutputwidget.cpp @@ -1,459 +1,451 @@ /* * GDB Debugger Support * * Copyright 2003 John Birch * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * * 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 "gdboutputwidget.h" #include "dbgglobal.h" #include "debuggerplugin.h" #include "debuglog.h" #include "debugsession.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI::GDB; /***************************************************************************/ GDBOutputWidget::GDBOutputWidget(CppDebuggerPlugin* plugin, QWidget *parent) : QWidget(parent), m_userGDBCmdEditor(nullptr), m_Interrupt(nullptr), m_gdbView(nullptr), m_showInternalCommands(false), m_maxLines(5000) { setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-scripts"), windowIcon())); setWindowTitle(i18n("GDB Output")); setWhatsThis(i18n("GDB output

    " "Shows all gdb commands being executed. " "You can also issue any other gdb command while debugging.

    ")); m_gdbView = new OutputTextEdit(this); m_gdbView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_gdbView->setReadOnly(true); m_userGDBCmdEditor = new KHistoryComboBox (this); QLabel *label = new QLabel(i18n("&GDB cmd:"), this); label->setBuddy(m_userGDBCmdEditor); m_Interrupt = new QToolButton( this ); m_Interrupt->setIcon ( QIcon::fromTheme( QStringLiteral("media-playback-pause") ) ); m_Interrupt->setToolTip( i18n ( "Pause execution of the app to enter gdb commands" ) ); QVBoxLayout *topLayout = new QVBoxLayout(this); topLayout->addWidget(m_gdbView); topLayout->setStretchFactor(m_gdbView, 1); topLayout->setMargin(0); QBoxLayout *userGDBCmdEntry = new QHBoxLayout(); userGDBCmdEntry->addWidget(label); userGDBCmdEntry->addWidget(m_userGDBCmdEditor); userGDBCmdEntry->setStretchFactor(m_userGDBCmdEditor, 1); userGDBCmdEntry->addWidget(m_Interrupt); topLayout->addLayout(userGDBCmdEntry); setLayout(topLayout); slotStateChanged(s_none, s_dbgNotStarted); connect(m_userGDBCmdEditor, static_cast(&KHistoryComboBox::returnPressed), this, &GDBOutputWidget::slotGDBCmd); connect(m_Interrupt, &QToolButton::clicked, this, &GDBOutputWidget::breakInto); m_updateTimer.setSingleShot(true); connect(&m_updateTimer, &QTimer::timeout, this, &GDBOutputWidget::flushPending); connect(KDevelop::ICore::self()->debugController(), &KDevelop::IDebugController::currentSessionChanged, this, &GDBOutputWidget::currentSessionChanged); connect(plugin, &CppDebuggerPlugin::reset, this, &GDBOutputWidget::clear); connect(plugin, &CppDebuggerPlugin::raiseDebuggerConsoleViews, this, &GDBOutputWidget::requestRaise); currentSessionChanged(KDevelop::ICore::self()->debugController()->currentSession()); // TODO Port to KF5 // connect(KGlobalSettings::self(), SIGNAL(kdisplayPaletteChanged()), // this, SLOT(updateColors())); updateColors(); } void GDBOutputWidget::updateColors() { KColorScheme scheme(QPalette::Active); m_gdbColor = scheme.foreground(KColorScheme::LinkText).color(); m_errorColor = scheme.foreground(KColorScheme::NegativeText).color(); } void GDBOutputWidget::currentSessionChanged(KDevelop::IDebugSession* s) { if (!s) return; DebugSession *session = qobject_cast(s); if (!session) return; connect(this, &GDBOutputWidget::userGDBCmd, session, &DebugSession::addUserCommand); connect(this, &GDBOutputWidget::breakInto, session, &DebugSession::interruptDebugger); connect(session, &DebugSession::debuggerInternalCommandOutput, this, &GDBOutputWidget::slotInternalCommandStdout); connect(session, &DebugSession::debuggerUserCommandOutput, this, &GDBOutputWidget::slotUserCommandStdout); // debugger internal output, treat it as an internal command output connect(session, &DebugSession::debuggerInternalOutput, this, &GDBOutputWidget::slotInternalCommandStdout); connect(session, &DebugSession::debuggerStateChanged, this, &GDBOutputWidget::slotStateChanged); slotStateChanged(s_none, session->debuggerState()); } /***************************************************************************/ GDBOutputWidget::~GDBOutputWidget() { delete m_gdbView; delete m_userGDBCmdEditor; } /***************************************************************************/ void GDBOutputWidget::clear() { if (m_gdbView) m_gdbView->clear(); m_userCommands_.clear(); m_allCommands.clear(); } /***************************************************************************/ void GDBOutputWidget::slotInternalCommandStdout(const QString& line) { newStdoutLine(line, true); } void GDBOutputWidget::slotUserCommandStdout(const QString& line) { qCDebug(DEBUGGERGDB) << "User command stdout: " << line; newStdoutLine(line, false); } namespace { QString colorify(QString text, const QColor& color) { - // Make sure the newline is at the end of the newly-added - // string. This is so that we can always correctly remove - // newline inside 'flushPending'. - if (!text.endsWith('\n')) - text.append('\n'); - if (text.endsWith('\n')) { - text.remove(text.length()-1, 1); + text.chop(1); } text = "" + text + "
    "; return text; } } void GDBOutputWidget::newStdoutLine(const QString& line, bool internal) { QString s = line.toHtmlEscaped(); if (s.startsWith(QLatin1String("(gdb)"))) { s = colorify(s, m_gdbColor); } else s.replace('\n', QLatin1String("
    ")); m_allCommands.append(s); m_allCommandsRaw.append(line); trimList(m_allCommands, m_maxLines); trimList(m_allCommandsRaw, m_maxLines); if (!internal) { m_userCommands_.append(s); m_userCommandsRaw.append(line); trimList(m_userCommands_, m_maxLines); trimList(m_userCommandsRaw, m_maxLines); } if (!internal || m_showInternalCommands) showLine(s); } void GDBOutputWidget::showLine(const QString& line) { m_pendingOutput += line; // To improve performance, we update the view after some delay. if (!m_updateTimer.isActive()) { m_updateTimer.start(100); } } void GDBOutputWidget::trimList(QStringList& l, int max_size) { int length = l.count(); if (length > max_size) { for(int to_delete = length - max_size; to_delete; --to_delete) { l.erase(l.begin()); } } } void GDBOutputWidget::setShowInternalCommands(bool show) { if (show != m_showInternalCommands) { m_showInternalCommands = show; // Set of strings to show changes, text edit still has old // set. Refresh. m_gdbView->clear(); QStringList& newList = m_showInternalCommands ? m_allCommands : m_userCommands_; QStringList::iterator i = newList.begin(), e = newList.end(); for(; i != e; ++i) { // Note that color formatting is already applied to '*i'. showLine(*i); } } } /***************************************************************************/ void GDBOutputWidget::slotReceivedStderr(const char* line) { QString colored = colorify(QString::fromLatin1(line).toHtmlEscaped(), m_errorColor); // Errors are shown inside user commands too. m_allCommands.append(colored); trimList(m_allCommands, m_maxLines); m_userCommands_.append(colored); trimList(m_userCommands_, m_maxLines); m_allCommandsRaw.append(line); trimList(m_allCommandsRaw, m_maxLines); m_userCommandsRaw.append(line); trimList(m_userCommandsRaw, m_maxLines); showLine(colored); } /***************************************************************************/ void GDBOutputWidget::slotGDBCmd() { QString GDBCmd(m_userGDBCmdEditor->currentText()); if (!GDBCmd.isEmpty()) { m_userGDBCmdEditor->addToHistory(GDBCmd); m_userGDBCmdEditor->clearEditText(); emit userGDBCmd(GDBCmd); } } void GDBOutputWidget::flushPending() { m_gdbView->setUpdatesEnabled(false); // QTextEdit adds newline after paragraph automatically. // So, remove trailing newline to avoid double newlines. if (m_pendingOutput.endsWith('\n')) - m_pendingOutput.remove(m_pendingOutput.length()-1, 1); + m_pendingOutput.chop(1); Q_ASSERT(!m_pendingOutput.endsWith('\n')); QTextDocument *document = m_gdbView->document(); QTextCursor cursor(document); cursor.movePosition(QTextCursor::End); cursor.insertHtml(m_pendingOutput); m_pendingOutput.clear(); m_gdbView->verticalScrollBar()->setValue(m_gdbView->verticalScrollBar()->maximum()); m_gdbView->setUpdatesEnabled(true); m_gdbView->update(); if (m_cmdEditorHadFocus) { m_userGDBCmdEditor->setFocus(); } } /***************************************************************************/ void GDBOutputWidget::slotStateChanged(KDevMI::DBGStateFlags oldStatus, KDevMI::DBGStateFlags newStatus) { Q_UNUSED(oldStatus) if (newStatus & s_dbgNotStarted) { m_Interrupt->setEnabled(false); m_userGDBCmdEditor->setEnabled(false); return; } else { m_Interrupt->setEnabled(true); } if (newStatus & s_dbgBusy) { if (m_userGDBCmdEditor->isEnabled()) { m_cmdEditorHadFocus = m_userGDBCmdEditor->hasFocus(); } m_userGDBCmdEditor->setEnabled(false); } else { m_userGDBCmdEditor->setEnabled(true); } } /***************************************************************************/ void GDBOutputWidget::focusInEvent(QFocusEvent* /*e*/) { m_gdbView->verticalScrollBar()->setValue(m_gdbView->verticalScrollBar()->maximum()); m_userGDBCmdEditor->setFocus(); } void GDBOutputWidget::savePartialProjectSession() { KConfigGroup config(KSharedConfig::openConfig(), "GDB Debugger"); config.writeEntry("showInternalCommands", m_showInternalCommands); } void GDBOutputWidget::restorePartialProjectSession() { KConfigGroup config(KSharedConfig::openConfig(), "GDB Debugger"); m_showInternalCommands = config.readEntry("showInternalCommands", false); } void GDBOutputWidget::contextMenuEvent(QContextMenuEvent * e) { QScopedPointer popup(new QMenu(this)); QAction* action = popup->addAction(i18n("Show Internal Commands"), this, SLOT(toggleShowInternalCommands())); action->setCheckable(true); action->setChecked(m_showInternalCommands); action->setWhatsThis(i18n( "Controls if commands issued internally by KDevelop " "will be shown or not.
    " "This option will affect only future commands, it will not " "add or remove already issued commands from the view.")); popup->addAction(i18n("Copy All"), this, SLOT(copyAll())); popup->exec(e->globalPos()); } void GDBOutputWidget::copyAll() { /* See comments for allCommandRaw_ for explanations of this complex logic, as opposed to calling text(). */ const QStringList& raw = m_showInternalCommands ? m_allCommandsRaw : m_userCommandsRaw; - QString text; - for (int i = 0; i < raw.size(); ++i) - text += raw.at(i); + const QString text = raw.join(QString()); // Make sure the text is pastable both with Ctrl-C and with // middle click. QApplication::clipboard()->setText(text, QClipboard::Clipboard); QApplication::clipboard()->setText(text, QClipboard::Selection); } void GDBOutputWidget::toggleShowInternalCommands() { setShowInternalCommands(!m_showInternalCommands); } OutputTextEdit::OutputTextEdit(GDBOutputWidget * parent) : QPlainTextEdit(parent) { } void OutputTextEdit::contextMenuEvent(QContextMenuEvent * event) { QScopedPointer popup(createStandardContextMenu()); QAction* action = popup->addAction(i18n("Show Internal Commands"), parent(), SLOT(toggleShowInternalCommands())); action->setCheckable(true); action->setChecked(static_cast(parent())->showInternalCommands()); action->setWhatsThis(i18n( "Controls if commands issued internally by KDevelop " "will be shown or not.
    " "This option will affect only future commands, it will not " "add or remove already issued commands from the view.")); popup->exec(event->globalPos()); } bool GDBOutputWidget::showInternalCommands() const { return m_showInternalCommands; } diff --git a/plugins/ghprovider/ghproviderwidget.cpp b/plugins/ghprovider/ghproviderwidget.cpp index 6fb9a11818..c08bd58c16 100644 --- a/plugins/ghprovider/ghproviderwidget.cpp +++ b/plugins/ghprovider/ghproviderwidget.cpp @@ -1,175 +1,175 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace gh { ProviderWidget::ProviderWidget(QWidget *parent) : IProjectProviderWidget(parent) { setLayout(new QVBoxLayout()); m_projects = new QListView(this); connect(m_projects, &QListView::clicked, this, &ProviderWidget::projectIndexChanged); m_waiting = new QLabel(i18n("Waiting for response"), this); m_waiting->setAlignment(Qt::AlignCenter); m_waiting->hide(); ProviderModel *model = new ProviderModel(this); m_projects->setModel(model); m_projects->setEditTriggers(QAbstractItemView::NoEditTriggers); m_resource = new Resource(this, model); m_account = new Account(m_resource); connect(m_resource, &Resource::reposUpdated, m_waiting, &QLabel::hide); QHBoxLayout *topLayout = new QHBoxLayout(); m_edit = new LineEdit(this); m_edit->setPlaceholderText(i18n("Search")); m_edit->setToolTip(i18n("You can press the Return key if you do not want to wait")); connect(m_edit, &LineEdit::returnPressed, this, &ProviderWidget::searchRepo); topLayout->addWidget(m_edit); m_combo = new QComboBox(this); m_combo->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); connect(m_combo, static_cast(&QComboBox::currentIndexChanged), this, &ProviderWidget::searchRepo); fillCombo(); topLayout->addWidget(m_combo); QPushButton *settings = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), QString(), this); settings->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); settings->setToolTip(i18n("Click this button to configure your GitHub account")); connect(settings, &QPushButton::clicked, this, &ProviderWidget::showSettings); topLayout->addWidget(settings); layout()->addItem(topLayout); layout()->addWidget(m_waiting); layout()->addWidget(m_projects); } KDevelop::VcsJob * ProviderWidget::createWorkingCopy(const QUrl &dest) { QModelIndex pos = m_projects->currentIndex(); if (!pos.isValid()) return nullptr; auto plugin = ICore::self()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl"), QStringLiteral("kdevgit")); if (!plugin) { KMessageBox::error(nullptr, i18n("The Git plugin could not be loaded which is required to import a GitHub project."), i18n("GitHub Provider Error")); return nullptr; } QString url = pos.data(ProviderModel::VcsLocationRole).toString(); if (m_account->validAccount()) - url = "https://" + m_account->token() + "@" + url.mid(8); + url = QLatin1String("https://") + m_account->token() + QLatin1Char('@') + url.midRef(8); QUrl real = QUrl(url); VcsLocation loc(real); auto vc = plugin->extension(); Q_ASSERT(vc); return vc->createWorkingCopy(loc, dest); } void ProviderWidget::fillCombo() { m_combo->clear(); m_combo->addItem(i18n("User"), 1); m_combo->addItem(i18n("Organization"), 3); if (m_account->validAccount()) { m_combo->addItem(m_account->name(), 0); m_combo->setCurrentIndex(2); } const QStringList &orgs = m_account->orgs(); foreach (const QString &org, orgs) m_combo->addItem(org, 2); } bool ProviderWidget::isCorrect() const { return m_projects->currentIndex().isValid(); } void ProviderWidget::projectIndexChanged(const QModelIndex ¤tIndex) { if (currentIndex.isValid()) { QString name = currentIndex.data().toString(); emit changed(name); } } void ProviderWidget::showSettings() { Dialog *dialog = new Dialog(this, m_account); connect(dialog, &Dialog::shouldUpdate, this, &ProviderWidget::fillCombo); dialog->show(); } void ProviderWidget::searchRepo() { bool enabled = true; QString uri, text = m_edit->text(); int idx = m_combo->itemData(m_combo->currentIndex()).toInt(); switch (idx) { case 0: /* Looking for this user's repo */ uri = QStringLiteral("/user/repos"); enabled = false; break; case 1: /* Looking for some user's repo */ if (text == m_account->name()) uri = QStringLiteral("/user/repos"); else uri = QStringLiteral("/users/%1/repos").arg(text); break; case 2: /* Known organization */ text = m_combo->currentText(); enabled = false; default:/* Looking for some organization's repo. */ uri = QStringLiteral("/orgs/%1/repos").arg(text); break; } m_edit->setEnabled(enabled); m_waiting->show(); m_resource->searchRepos(uri, m_account->token()); } } // End of namespace gh diff --git a/plugins/ghprovider/ghresource.cpp b/plugins/ghprovider/ghresource.cpp index 343fb92c42..ec0585aa1f 100644 --- a/plugins/ghprovider/ghresource.cpp +++ b/plugins/ghprovider/ghresource.cpp @@ -1,230 +1,231 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * 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 #include #include #include #include #include #include #include #include #include namespace gh { /// Base url for the Github API v3. const static QUrl baseUrl(QStringLiteral("https://api.github.com")); KIO::StoredTransferJob* createHttpAuthJob(const QString &httpHeader) { QUrl url = baseUrl; url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1String("/authorizations")); // generate a unique token, see bug 372144 const QString tokenName = "KDevelop Github Provider : " + QHostInfo::localHostName() + " - " + QDateTime::currentDateTimeUtc().toString(); const QByteArray data = QByteArrayLiteral("{ \"scopes\": [\"repo\"], \"note\": \"") + tokenName.toUtf8() + QByteArrayLiteral("\" }"); KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); job->setProperty("requestedTokenName", tokenName); job->addMetaData(QStringLiteral("customHTTPHeader"), httpHeader); return job; } Resource::Resource(QObject *parent, ProviderModel *model) : QObject(parent), m_model(model) { /* There's nothing to do here */ } void Resource::searchRepos(const QString &uri, const QString &token) { KIO::TransferJob *job = getTransferJob(uri, token); connect(job, &KIO::TransferJob::data, this, &Resource::slotRepos); } void Resource::getOrgs(const QString &token) { KIO::TransferJob *job = getTransferJob(QStringLiteral("/user/orgs"), token); connect(job, &KIO::TransferJob::data, this, &Resource::slotOrgs); } void Resource::authenticate(const QString &name, const QString &password) { auto job = createHttpAuthJob(QLatin1String("Authorization: Basic ") + QString::fromUtf8(QByteArray(name.toUtf8() + ':' + password.toUtf8()).toBase64())); job->addMetaData("PropagateHttpHeader","true"); connect(job, &KIO::StoredTransferJob::result, this, &Resource::slotAuthenticate); job->start(); } void Resource::twoFactorAuthenticate(const QString &transferHeader, const QString &code) { auto job = createHttpAuthJob(transferHeader + QLatin1String("\nX-GitHub-OTP: ") + code); connect(job, &KIO::StoredTransferJob::result, this, &Resource::slotAuthenticate); job->start(); } void Resource::revokeAccess(const QString &id, const QString &name, const QString &password) { QUrl url = baseUrl; url.setPath(url.path() + "/authorizations/" + id); KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo); job->addMetaData(QStringLiteral("customHTTPHeader"), QLatin1String("Authorization: Basic ") + QString (name + ':' + password).toUtf8().toBase64()); /* And we don't care if it's successful ;) */ job->start(); } KIO::TransferJob * Resource::getTransferJob(const QString &path, const QString &token) const { QUrl url = baseUrl; url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + path); KIO::TransferJob *job = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); if (!token.isEmpty()) job->addMetaData(QStringLiteral("customHTTPHeader"), "Authorization: token " + token); return job; } void Resource::retrieveRepos(const QByteArray &data) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error == 0) { QVariantList map = doc.toVariant().toList(); m_model->clear(); foreach (const QVariant &it, map) { const QVariantMap &map = it.toMap(); Response res; res.name = map.value(QStringLiteral("name")).toString(); res.url = map.value(QStringLiteral("clone_url")).toUrl(); if (map.value(QStringLiteral("fork")).toBool()) res.kind = Fork; else if (map.value(QStringLiteral("private")).toBool()) res.kind = Private; else res.kind = Public; ProviderItem *item = new ProviderItem(res); m_model->appendRow(item); } } emit reposUpdated(); } void Resource::retrieveOrgs(const QByteArray &data) { QStringList res; QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error == 0) { QVariantList json = doc.toVariant().toList(); + res.reserve(json.size()); foreach (QVariant it, json) { QVariantMap map = it.toMap(); res << map.value(QStringLiteral("login")).toString(); } } emit orgsUpdated(res); } void Resource::slotAuthenticate(KJob *job) { const QString tokenName = job->property("requestedTokenName").toString(); Q_ASSERT(!tokenName.isEmpty()); if (job->error()) { emit authenticated("", "", tokenName); return; } const auto metaData = qobject_cast(job)->metaData(); if (metaData[QStringLiteral("responsecode")] == QStringLiteral("401")) { const auto& header = metaData[QStringLiteral("HTTP-Headers")]; if (header.contains(QStringLiteral("X-GitHub-OTP: required;"), Qt::CaseInsensitive)) { emit twoFactorAuthRequested(qobject_cast(job)->outgoingMetaData()[QStringLiteral("customHTTPHeader")]); return; } } QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(qobject_cast(job)->data(), &error); qCDebug(GHPROVIDER) << "Response:" << doc; if (error.error == 0) { QVariantMap map = doc.toVariant().toMap(); emit authenticated(map.value(QStringLiteral("id")).toByteArray(), map.value(QStringLiteral("token")).toByteArray(), tokenName); } else emit authenticated("", "", tokenName); } void Resource::slotRepos(KIO::Job *job, const QByteArray &data) { if (!job) { qCWarning(GHPROVIDER) << "NULL job returned!"; return; } if (job->error()) { qCWarning(GHPROVIDER) << "Job error: " << job->errorString(); return; } m_temp.append(data); if (data.isEmpty()) { retrieveRepos(m_temp); m_temp = ""; } } void Resource::slotOrgs(KIO::Job *job, const QByteArray &data) { QList res; if (!job) { qCWarning(GHPROVIDER) << "NULL job returned!"; emit orgsUpdated(res); return; } if (job->error()) { qCWarning(GHPROVIDER) << "Job error: " << job->errorString(); emit orgsUpdated(res); return; } m_orgTemp.append(data); if (data.isEmpty()) { retrieveOrgs(m_orgTemp); m_orgTemp = ""; } } } // End of namespace gh diff --git a/plugins/git/gitplugin.cpp b/plugins/git/gitplugin.cpp index 70b65509c0..65063ecf80 100644 --- a/plugins/git/gitplugin.cpp +++ b/plugins/git/gitplugin.cpp @@ -1,1554 +1,1561 @@ /*************************************************************************** * Copyright 2008 Evgeniy Ivanov * * Copyright 2009 Hugo Parente Lima * * Copyright 2010 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "gitplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gitclonejob.h" #include #include "stashmanagerdialog.h" #include #include #include #include #include #include #include "gitjob.h" #include "gitmessagehighlighter.h" #include "gitplugincheckinrepositoryjob.h" #include "gitnameemaildialog.h" #include "debug.h" using namespace KDevelop; QVariant runSynchronously(KDevelop::VcsJob* job) { QVariant ret; if(job->exec() && job->status()==KDevelop::VcsJob::JobSucceeded) { ret = job->fetchResults(); } delete job; return ret; } namespace { QDir dotGitDirectory(const QUrl& dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(finfo.filePath()): finfo.absoluteDir(); static const QString gitDir = QStringLiteral(".git"); while (!dir.exists(gitDir) && dir.cdUp()) {} // cdUp, until there is a sub-directory called .git if (dir.isRoot()) { qCWarning(PLUGIN_GIT) << "couldn't find the git root for" << dirPath; } return dir; } /** * Whenever a directory is provided, change it for all the files in it but not inner directories, * that way we make sure we won't get into recursion, */ static QList preventRecursion(const QList& urls) { QList ret; foreach(const QUrl& url, urls) { QDir d(url.toLocalFile()); if(d.exists()) { - QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); + const QStringList entries = d.entryList(QDir::Files | QDir::NoDotAndDotDot); + ret.reserve(ret.size() + entries.size()); foreach(const QString& entry, entries) { QUrl entryUrl = QUrl::fromLocalFile(d.absoluteFilePath(entry)); ret += entryUrl; } } else ret += url; } return ret; } QString toRevisionName(const KDevelop::VcsRevision& rev, const QString& currentRevision=QString()) { switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("^HEAD"); case VcsRevision::Base: return QString(); case VcsRevision::Working: return QString(); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); return currentRevision + "^1"; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: return rev.revisionValue().toString(); case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } QString revisionInterval(const KDevelop::VcsRevision& rev, const KDevelop::VcsRevision& limit) { QString ret; if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Start) //if we want it to the beginning just put the revisionInterval ret = toRevisionName(limit, QString()); else { QString dst = toRevisionName(limit); if(dst.isEmpty()) ret = dst; else { QString src = toRevisionName(rev, dst); if(src.isEmpty()) ret = src; else ret = src+".."+dst; } } return ret; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } QDir urlDir(const QList& urls) { return urlDir(urls.first()); } //TODO: could be improved } GitPlugin::GitPlugin( QObject *parent, const QVariantList & ) : DistributedVersionControlPlugin(parent, QStringLiteral("kdevgit")), m_oldVersion(false), m_usePrefix(true) { if (QStandardPaths::findExecutable(QStringLiteral("git")).isEmpty()) { setErrorDescription(i18n("Unable to find git executable. Is it installed on the system?")); return; } setObjectName(QStringLiteral("Git")); DVcsJob* versionJob = new DVcsJob(QDir::tempPath(), this, KDevelop::OutputJob::Silent); *versionJob << "git" << "--version"; connect(versionJob, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitVersionOutput); ICore::self()->runController()->registerJob(versionJob); m_watcher = new KDirWatch(this); connect(m_watcher, &KDirWatch::dirty, this, &GitPlugin::fileChanged); connect(m_watcher, &KDirWatch::created, this, &GitPlugin::fileChanged); } GitPlugin::~GitPlugin() {} bool emptyOutput(DVcsJob* job) { QScopedPointer _job(job); if(job->exec() && job->status()==VcsJob::JobSucceeded) return job->rawOutput().trimmed().isEmpty(); return false; } bool GitPlugin::hasStashes(const QDir& repository) { return !emptyOutput(gitStash(repository, QStringList(QStringLiteral("list")), KDevelop::OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& d) { return !emptyOutput(lsFiles(d, QStringList(QStringLiteral("-m")), OutputJob::Silent)); } bool GitPlugin::hasModifications(const QDir& repo, const QUrl& file) { - return !emptyOutput(lsFiles(repo, QStringList() << QStringLiteral("-m") << file.path(), OutputJob::Silent)); + return !emptyOutput(lsFiles(repo, QStringList{QStringLiteral("-m"), file.path()}, OutputJob::Silent)); } void GitPlugin::additionalMenuEntries(QMenu* menu, const QList& urls) { m_urls = urls; QDir dir=urlDir(urls); bool hasSt = hasStashes(dir); menu->addSeparator()->setText(i18n("Git Stashes")); menu->addAction(i18n("Stash Manager"), this, SLOT(ctxStashManager()))->setEnabled(hasSt); menu->addAction(i18n("Push Stash"), this, SLOT(ctxPushStash())); menu->addAction(i18n("Pop Stash"), this, SLOT(ctxPopStash()))->setEnabled(hasSt); } void GitPlugin::ctxPushStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxPopStash() { VcsJob* job = gitStash(urlDir(m_urls), QStringList(QStringLiteral("pop")), KDevelop::OutputJob::Verbose); ICore::self()->runController()->registerJob(job); } void GitPlugin::ctxStashManager() { QPointer d = new StashManagerDialog(urlDir(m_urls), this, nullptr); d->exec(); delete d; } DVcsJob* GitPlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity=OutputJob::Verbose) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } QString GitPlugin::name() const { return QStringLiteral("Git"); } QUrl GitPlugin::repositoryRoot(const QUrl& path) { return QUrl::fromLocalFile(dotGitDirectory(path).absolutePath()); } bool GitPlugin::isValidDirectory(const QUrl & dirPath) { QDir dir=dotGitDirectory(dirPath); QFile dotGitPotentialFile(dir.filePath(QStringLiteral(".git"))); // if .git is a file, we may be in a git worktree QFileInfo dotGitPotentialFileInfo(dotGitPotentialFile); if (!dotGitPotentialFileInfo.isDir() && dotGitPotentialFile.exists()) { QString gitWorktreeFileContent; if (dotGitPotentialFile.open(QFile::ReadOnly)) { // the content should be gitdir: /path/to/the/.git/worktree gitWorktreeFileContent = QString::fromUtf8(dotGitPotentialFile.readAll()); dotGitPotentialFile.close(); } else { return false; } const auto items = gitWorktreeFileContent.split(' '); if (items.size() == 2 && items.at(0) == QLatin1String("gitdir:")) { qCDebug(PLUGIN_GIT) << "we are in a git worktree" << items.at(1); return true; } } return dir.exists(QStringLiteral(".git/HEAD")); } bool GitPlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) { if (remoteLocation.isLocalFile()) { QFileInfo fileInfo(remoteLocation.toLocalFile()); if (fileInfo.isDir()) { QDir dir(fileInfo.filePath()); if (dir.exists(QStringLiteral(".git/HEAD"))) { return true; } // TODO: check also for bare repo } } else { const QString scheme = remoteLocation.scheme(); if (scheme == QLatin1String("git") || scheme == QLatin1String("git+ssh")) { return true; } // heuristic check, anything better we can do here without talking to server? if ((scheme == QLatin1String("http") || scheme == QLatin1String("https")) && remoteLocation.path().endsWith(QLatin1String(".git"))) { return true; } } return false; } bool GitPlugin::isVersionControlled(const QUrl &path) { QFileInfo fsObject(path.toLocalFile()); if (!fsObject.exists()) { return false; } if (fsObject.isDir()) { return isValidDirectory(path); } QString filename = fsObject.fileName(); QStringList otherFiles = getLsFiles(fsObject.dir(), QStringList(QStringLiteral("--")) << filename, KDevelop::OutputJob::Silent); return !otherFiles.empty(); } VcsJob* GitPlugin::init(const QUrl &directory) { DVcsJob* job = new DVcsJob(urlDir(directory), this); job->setType(VcsJob::Import); *job << "git" << "init"; return job; } VcsJob* GitPlugin::createWorkingCopy(const KDevelop::VcsLocation & source, const QUrl& dest, KDevelop::IBasicVersionControl::RecursionMode) { DVcsJob* job = new GitCloneJob(urlDir(dest), this); job->setType(VcsJob::Import); *job << "git" << "clone" << "--progress" << "--" << source.localUrl().url() << dest; return job; } VcsJob* GitPlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Add); *job << "git" << "add" << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } KDevelop::VcsJob* GitPlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty()) return errorsFound(i18n("Did not specify the list of files"), OutputJob::Verbose); DVcsJob* job = new GitJob(urlDir(localLocations), this, OutputJob::Silent); job->setType(VcsJob::Status); if(m_oldVersion) { *job << "git" << "ls-files" << "-t" << "-m" << "-c" << "-o" << "-d" << "-k" << "--directory"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput_old); } else { *job << "git" << "status" << "--porcelain"; job->setIgnoreError(true); connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitStatusOutput); } *job << "--" << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } VcsJob* GitPlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, IBasicVersionControl::RecursionMode recursion) { DVcsJob* job = new GitJob(dotGitDirectory(fileOrDirectory), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Diff); *job << "git" << "diff" << "--no-color" << "--no-ext-diff"; if (!usePrefix()) { // KDE's ReviewBoard now requires p1 patchfiles, so `git diff --no-prefix` to generate p0 patches // has become optional. *job << "--no-prefix"; } if (dstRevision.revisionType() == VcsRevision::Special && dstRevision.specialType() == VcsRevision::Working) { if (srcRevision.revisionType() == VcsRevision::Special && srcRevision.specialType() == VcsRevision::Base) { *job << "HEAD"; } else { *job << "--cached" << srcRevision.revisionValue().toString(); } } else { QString revstr = revisionInterval(srcRevision, dstRevision); if(!revstr.isEmpty()) *job << revstr; } *job << "--"; if (recursion == IBasicVersionControl::Recursive) { *job << fileOrDirectory; } else { *job << preventRecursion(QList() << fileOrDirectory); } connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitDiffOutput); return job; } VcsJob* GitPlugin::revert(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { if(localLocations.isEmpty() ) return errorsFound(i18n("Could not revert changes"), OutputJob::Verbose); QDir repo = urlDir(repositoryRoot(localLocations.first())); QString modified; for (const auto& file: localLocations) { if (hasModifications(repo, file)) { modified.append(file.toDisplayString(QUrl::PreferLocalFile) + "
    "); } } if (!modified.isEmpty()) { auto res = KMessageBox::questionYesNo(nullptr, i18n("The following files have uncommitted changes, " "which will be lost. Continue?") + "

    " + modified); if (res != KMessageBox::Yes) { return errorsFound(QString(), OutputJob::Silent); } } DVcsJob* job = new GitJob(dotGitDirectory(localLocations.front()), this); job->setType(VcsJob::Revert); *job << "git" << "checkout" << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } //TODO: git doesn't like empty messages, but "KDevelop didn't provide any message, it may be a bug" looks ugly... //If no files specified then commit already added files VcsJob* GitPlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode recursion) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); const QDir dir = dotGitDirectory(localLocations.front()); if (!ensureValidGitIdentity(dir)) { return errorsFound(i18n("Email or name for Git not specified")); } DVcsJob* job = new DVcsJob(dir, this); job->setType(VcsJob::Commit); QList files = (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); addNotVersionedFiles(dir, files); *job << "git" << "commit" << "-m" << message; *job << "--" << files; return job; } bool GitPlugin::ensureValidGitIdentity(const QDir& dir) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath()); const QString name = readConfigOption(url, QStringLiteral("user.name")); const QString email = readConfigOption(url, QStringLiteral("user.email")); if (!email.isEmpty() && !name.isEmpty()) { return true; // already okay } GitNameEmailDialog dialog; dialog.setName(name); dialog.setEmail(email); if (!dialog.exec()) { return false; } runSynchronously(setConfigOption(url, QStringLiteral("user.name"), dialog.name(), dialog.isGlobal())); runSynchronously(setConfigOption(url, QStringLiteral("user.email"), dialog.email(), dialog.isGlobal())); return true; } void GitPlugin::addNotVersionedFiles(const QDir& dir, const QList& files) { QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others"), KDevelop::OutputJob::Silent); QList toadd, otherFiles; + otherFiles.reserve(otherStr.size()); foreach(const QString& file, otherStr) { QUrl v = QUrl::fromLocalFile(dir.absoluteFilePath(file)); otherFiles += v; } //We add the files that are not versioned foreach(const QUrl& file, files) { if(otherFiles.contains(file) && QFileInfo(file.toLocalFile()).isFile()) toadd += file; } if(!toadd.isEmpty()) { VcsJob* job = add(toadd); job->exec(); // krazy:exclude=crashy } } bool isEmptyDirStructure(const QDir &dir) { foreach (const QFileInfo &i, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { if (i.isDir()) { if (!isEmptyDirStructure(QDir(i.filePath()))) return false; } else if (i.isFile()) { return false; } } return true; } VcsJob* GitPlugin::remove(const QList& files) { if (files.isEmpty()) return errorsFound(i18n("No files to remove")); QDir dotGitDir = dotGitDirectory(files.front()); QList files_(files); QMutableListIterator i(files_); while (i.hasNext()) { QUrl file = i.next(); QFileInfo fileInfo(file.toLocalFile()); - QStringList otherStr = getLsFiles(dotGitDir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << file.toLocalFile(), KDevelop::OutputJob::Silent); + const QStringList otherStr = getLsFiles(dotGitDir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), file.toLocalFile()}, KDevelop::OutputJob::Silent); if(!otherStr.isEmpty()) { //remove files not under version control QList otherFiles; + otherFiles.reserve(otherStr.size()); foreach(const QString &f, otherStr) { otherFiles << QUrl::fromLocalFile(dotGitDir.path()+'/'+f); } if (fileInfo.isFile()) { //if it's an unversioned file we are done, don't use git rm on it i.remove(); } auto trashJob = KIO::trash(otherFiles); trashJob->exec(); qCDebug(PLUGIN_GIT) << "other files" << otherFiles; } if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(file.toLocalFile()))) { //remove empty folders, git doesn't do that auto trashJob = KIO::trash(file); trashJob->exec(); qCDebug(PLUGIN_GIT) << "empty folder, removing" << file; //we already deleted it, don't use git rm on it i.remove(); } } } if (files_.isEmpty()) return nullptr; DVcsJob* job = new GitJob(dotGitDir, this); job->setType(VcsJob::Remove); // git refuses to delete files with local modifications // use --force to overcome this *job << "git" << "rm" << "-r" << "--force"; *job << "--" << files_; return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& src, const KDevelop::VcsRevision& dst) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString rev = revisionInterval(dst, src); if(!rev.isEmpty()) *job << rev; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } VcsJob* GitPlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, unsigned long int limit) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Log); *job << "git" << "log" << "--date=raw" << "--name-status" << "-M80%" << "--follow"; QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) *job << revStr; if(limit>0) *job << QStringLiteral("-%1").arg(limit); *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitLogOutput); return job; } KDevelop::VcsJob* GitPlugin::annotate(const QUrl &localLocation, const KDevelop::VcsRevision&) { DVcsJob* job = new GitJob(dotGitDirectory(localLocation), this, KDevelop::OutputJob::Silent); job->setType(VcsJob::Annotate); *job << "git" << "blame" << "--porcelain" << "-w"; *job << "--" << localLocation; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBlameOutput); return job; } void GitPlugin::parseGitBlameOutput(DVcsJob *job) { QVariantList results; VcsAnnotationLine* annotation = nullptr; const auto output = job->output(); const auto lines = output.splitRef('\n'); bool skipNext=false; QMap definedRevisions; for(QVector::const_iterator it=lines.constBegin(), itEnd=lines.constEnd(); it!=itEnd; ++it) { if(skipNext) { skipNext=false; results += qVariantFromValue(*annotation); continue; } if(it->isEmpty()) continue; QStringRef name = it->left(it->indexOf(' ')); QStringRef value = it->right(it->size()-name.size()-1); if(name==QLatin1String("author")) annotation->setAuthor(value.toString()); else if(name==QLatin1String("author-mail")) {} //TODO: do smth with the e-mail? else if(name==QLatin1String("author-tz")) {} //TODO: does it really matter? else if(name==QLatin1String("author-time")) annotation->setDate(QDateTime::fromTime_t(value.toUInt())); else if(name==QLatin1String("summary")) annotation->setCommitMessage(value.toString()); else if(name.startsWith(QStringLiteral("committer"))) {} //We will just store the authors else if(name==QLatin1String("previous")) {} //We don't need that either else if(name==QLatin1String("filename")) { skipNext=true; } else if(name==QLatin1String("boundary")) { definedRevisions.insert(QStringLiteral("boundary"), VcsAnnotationLine()); } else { const auto values = value.split(' '); VcsRevision rev; rev.setRevisionValue(name.left(8).toString(), KDevelop::VcsRevision::GlobalNumber); skipNext = definedRevisions.contains(name.toString()); if(!skipNext) definedRevisions.insert(name.toString(), VcsAnnotationLine()); annotation = &definedRevisions[name.toString()]; annotation->setLineNumber(values[1].toInt() - 1); annotation->setRevision(rev); } } job->setResults(results); } DVcsJob* GitPlugin::lsFiles(const QDir &repository, const QStringList &args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "ls-files" << args; return job; } DVcsJob* GitPlugin::gitStash(const QDir& repository, const QStringList& args, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(repository, this, verbosity); *job << "git" << "stash" << args; return job; } VcsJob* GitPlugin::tag(const QUrl& repository, const QString& commitMessage, const VcsRevision& rev, const QString& tagName) { DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "tag" << "-m" << commitMessage << tagName; if(rev.revisionValue().isValid()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::switchBranch(const QUrl &repository, const QString &branch) { QDir d=urlDir(repository); if(hasModifications(d) && KMessageBox::questionYesNo(nullptr, i18n("There are pending changes, do you want to stash them first?"))==KMessageBox::Yes) { QScopedPointer stash(gitStash(d, QStringList(), KDevelop::OutputJob::Verbose)); stash->exec(); } DVcsJob* job = new DVcsJob(d, this); *job << "git" << "checkout" << branch; return job; } VcsJob* GitPlugin::branch(const QUrl& repository, const KDevelop::VcsRevision& rev, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "branch" << "--" << branchName; if(!rev.prettyValue().isEmpty()) *job << rev.revisionValue().toString(); return job; } VcsJob* GitPlugin::deleteBranch(const QUrl& repository, const QString& branchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-D" << branchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::renameBranch(const QUrl& repository, const QString& oldBranchName, const QString& newBranchName) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); *job << "git" << "branch" << "-m" << newBranchName << oldBranchName; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } VcsJob* GitPlugin::mergeBranch(const QUrl& repository, const QString& branchName) { Q_ASSERT(!branchName.isEmpty()); DVcsJob* job = new DVcsJob(urlDir(repository), this); *job << "git" << "merge" << branchName; return job; } VcsJob* GitPlugin::currentBranch(const QUrl& repository) { DVcsJob* job = new DVcsJob(urlDir(repository), this, OutputJob::Silent); job->setIgnoreError(true); *job << "git" << "symbolic-ref" << "-q" << "--short" << "HEAD"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitCurrentBranch); return job; } void GitPlugin::parseGitCurrentBranch(DVcsJob* job) { QString out = job->output().trimmed(); job->setResults(out); } VcsJob* GitPlugin::branches(const QUrl &repository) { DVcsJob* job=new DVcsJob(urlDir(repository)); *job << "git" << "branch" << "-a"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitBranchOutput); return job; } void GitPlugin::parseGitBranchOutput(DVcsJob* job) { const auto output = job->output(); const auto branchListDirty = output.splitRef('\n', QString::SkipEmptyParts); QStringList branchList; foreach(const auto & branch, branchListDirty) { // Skip pointers to another branches (one example of this is "origin/HEAD -> origin/master"); // "git rev-list" chokes on these entries and we do not need duplicate branches altogether. if (branch.contains(QStringLiteral("->"))) continue; // Skip entries such as '(no branch)' if (branch.contains(QStringLiteral("(no branch)"))) continue; QStringRef name = branch; if (name.startsWith('*')) name = branch.right(branch.size()-2); branchList << name.trimmed().toString(); } job->setResults(branchList); } /* Few words about how this hardcore works: 1. get all commits (with --paretns) 2. select master (root) branch and get all unicial commits for branches (git-rev-list br2 ^master ^br3) 3. parse allCommits. While parsing set mask (columns state for every row) for BRANCH, INITIAL, CROSS, MERGE and INITIAL are also set in DVCScommit::setParents (depending on parents count) another setType(INITIAL) is used for "bottom/root/first" commits of branches 4. find and set merges, HEADS. It's an ittaration through all commits. - first we check if parent is from the same branch, if no then we go through all commits searching parent's index and set CROSS/HCROSS for rows (in 3 rows are set EMPTY after commit with parent from another tree met) - then we check branchesShas[i][0] to mark heads 4 can be a seporate function. TODO: All this porn require refactoring (rewriting is better)! It's a very dirty implementation. FIXME: 1. HEAD which is head has extra line to connect it with further commit 2. If you menrge branch2 to master, only new commits of branch2 will be visible (it's fine, but there will be extra merge rectangle in master. If there are no extra commits in branch2, but there are another branches, then the place for branch2 will be empty (instead of be used for branch3). 3. Commits that have additional commit-data (not only history merging, but changes to fix conflicts) are shown incorrectly */ QVector GitPlugin::allCommits(const QString& repo) { initBranchHash(repo); - QStringList args; - args << QStringLiteral("--all") << QStringLiteral("--pretty") << QStringLiteral("--parents"); + const QStringList args{ + QStringLiteral("--all"), + QStringLiteral("--pretty"), + QStringLiteral("--parents"), + }; QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); static QRegExp rx_com("commit \\w{40,40}"); QVector commitList; DVcsEvent item; //used to keep where we have empty/cross/branch entry //true if it's an active branch (then cross or branch) and false if not QVector additionalFlags(branchesShas.count()); additionalFlags.fill(false); //parse output for(int i = 0; i < commits.count(); ++i) { if (commits[i].contains(rx_com)) { qCDebug(PLUGIN_GIT) << "commit found in " << commits[i]; item.setCommit(commits[i].section(' ', 1, 1).trimmed()); // qCDebug(PLUGIN_GIT) << "commit is: " << commits[i].section(' ', 1); QStringList parents; QString parent = commits[i].section(' ', 2); int section = 2; while (!parent.isEmpty()) { /* qCDebug(PLUGIN_GIT) << "Parent is: " << parent;*/ parents.append(parent.trimmed()); section++; parent = commits[i].section(' ', section); } item.setParents(parents); //Avoid Merge string while (!commits[i].contains(QStringLiteral("Author: "))) ++i; item.setAuthor(commits[i].section(QStringLiteral("Author: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "author is: " << commits[i].section("Author: ", 1); item.setDate(commits[++i].section(QStringLiteral("Date: "), 1).trimmed()); // qCDebug(PLUGIN_GIT) << "date is: " << commits[i].section("Date: ", 1); QString log; i++; //next line! while (i < commits.count() && !commits[i].contains(rx_com)) log += commits[i++]; --i; //while took commit line item.setLog(log.trimmed()); // qCDebug(PLUGIN_GIT) << "log is: " << log; //mask is used in CommitViewDelegate to understand what we should draw for each branch QList mask; mask.reserve(branchesShas.count()); //set mask (properties for each graph column in row) for(int i = 0; i < branchesShas.count(); ++i) { qCDebug(PLUGIN_GIT)<<"commit: " << item.commit(); if (branchesShas[i].contains(item.commit())) { mask.append(item.type()); //we set type in setParents //check if parent from the same branch, if not then we have found a root of the branch //and will use empty column for all futher (from top to bottom) revisions //FIXME: we should set CROSS between parent and child (and do it when find merge point) additionalFlags[i] = false; foreach(const QString &sha, item.parents()) { if (branchesShas[i].contains(sha)) additionalFlags[i] = true; } if (additionalFlags[i] == false) item.setType(DVcsEvent::INITIAL); //hasn't parents from the same branch, used in drawing } else { if (additionalFlags[i] == false) mask.append(DVcsEvent::EMPTY); else mask.append(DVcsEvent::CROSS); } qCDebug(PLUGIN_GIT) << "mask " << i << "is " << mask[i]; } item.setProperties(mask); commitList.append(item); } } //find and set merges, HEADS, require refactoring! for (auto iter = commitList.begin(); iter != commitList.end(); ++iter) { QStringList parents = iter->parents(); //we need only only child branches if (parents.count() != 1) break; QString parent = parents[0]; const QString commit = iter->commit(); bool parent_checked = false; int heads_checked = 0; for(int i = 0; i < branchesShas.count(); ++i) { //check parent if (branchesShas[i].contains(commit)) { if (!branchesShas[i].contains(parent)) { //parent and child are not in same branch //since it is list, than parent has i+1 index //set CROSS and HCROSS for (auto f_iter = iter; f_iter != commitList.end(); ++f_iter) { if (parent == f_iter->commit()) { for(int j = 0; j < i; ++j) { if(branchesShas[j].contains(parent)) f_iter->setProperty(j, DVcsEvent::MERGE); else f_iter->setProperty(j, DVcsEvent::HCROSS); } f_iter->setType(DVcsEvent::MERGE); f_iter->setProperty(i, DVcsEvent::MERGE_RIGHT); qCDebug(PLUGIN_GIT) << parent << " is parent of " << commit; qCDebug(PLUGIN_GIT) << f_iter->commit() << " is merge"; parent_checked = true; break; } else f_iter->setProperty(i, DVcsEvent::CROSS); } } } //mark HEADs if (!branchesShas[i].empty() && commit == branchesShas[i][0]) { iter->setType(DVcsEvent::HEAD); iter->setProperty(i, DVcsEvent::HEAD); heads_checked++; qCDebug(PLUGIN_GIT) << "HEAD found"; } //some optimization if (heads_checked == branchesShas.count() && parent_checked) break; } } return commitList; } void GitPlugin::initBranchHash(const QString &repo) { const QUrl repoUrl = QUrl::fromLocalFile(repo); QStringList gitBranches = runSynchronously(branches(repoUrl)).toStringList(); qCDebug(PLUGIN_GIT) << "BRANCHES: " << gitBranches; //Now root branch is the current branch. In future it should be the longest branch //other commitLists are got with git-rev-lits branch ^br1 ^ br2 QString root = runSynchronously(currentBranch(repoUrl)).toString(); QScopedPointer job(gitRevList(repo, QStringList(root))); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); foreach(const QString &branch, gitBranches) { if (branch == root) continue; QStringList args(branch); foreach(const QString &branch_arg, gitBranches) { if (branch_arg != branch) //man gitRevList for '^' args<<'^' + branch_arg; } QScopedPointer job(gitRevList(repo, args)); bool ret = job->exec(); Q_ASSERT(ret && job->status()==VcsJob::JobSucceeded && "TODO: provide a fall back in case of failing"); Q_UNUSED(ret); QStringList commits = job->output().split('\n', QString::SkipEmptyParts); // qCDebug(PLUGIN_GIT) << "\n\n\n commits" << commits << "\n\n\n"; branchesShas.append(commits); } } //Actually we can just copy the output without parsing. So it's a kind of draft for future void GitPlugin::parseLogOutput(const DVcsJob* job, QVector& commits) const { // static QRegExp rx_sep( "[-=]+" ); // static QRegExp rx_date( "date:\\s+([^;]*);\\s+author:\\s+([^;]*).*" ); static QRegularExpression rx_com( QStringLiteral("commit \\w{1,40}") ); const auto output = job->output(); const auto lines = output.splitRef('\n', QString::SkipEmptyParts); DVcsEvent item; QString commitLog; for (int i=0; i commits; QString contents = job->output(); // check if git-log returned anything if (contents.isEmpty()) { job->setResults(commits); // empty list return; } // start parsing the output QTextStream s(&contents); VcsEvent item; QString message; bool pushCommit = false; while (!s.atEnd()) { QString line = s.readLine(); if (commitRegex.exactMatch(line)) { if (pushCommit) { item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); item.setItems(QList()); } else { pushCommit = true; } VcsRevision rev; rev.setRevisionValue(commitRegex.cap(1), KDevelop::VcsRevision::GlobalNumber); item.setRevision(rev); message.clear(); } else if (infoRegex.exactMatch(line)) { QString cap1 = infoRegex.cap(1); if (cap1 == QLatin1String("Author")) { item.setAuthor(infoRegex.cap(2).trimmed()); } else if (cap1 == QLatin1String("Date")) { item.setDate(QDateTime::fromTime_t(infoRegex.cap(2).trimmed().split(' ')[0].toUInt())); } } else if (modificationsRegex.exactMatch(line)) { VcsItemEvent::Actions a = actionsFromString(modificationsRegex.cap(1).at(0).toLatin1()); QString filenameA = modificationsRegex.cap(2); VcsItemEvent itemEvent; itemEvent.setActions(a); itemEvent.setRepositoryLocation(filenameA); if(a==VcsItemEvent::Replaced) { QString filenameB = modificationsRegex.cap(3); itemEvent.setRepositoryCopySourceLocation(filenameB); } item.addItem(itemEvent); } else if (line.startsWith(QLatin1String(" "))) { - message += line.remove(0, 4); - message += '\n'; + message += line.midRef(4) + QLatin1Char('\n'); } } item.setMessage(message.trimmed()); commits.append(QVariant::fromValue(item)); job->setResults(commits); } void GitPlugin::parseGitDiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); diff.setBaseDiff(repositoryRoot(QUrl::fromLocalFile(job->directory().absolutePath()))); diff.setDepth(usePrefix()? 1 : 0); job->setResults(qVariantFromValue(diff)); } static VcsStatusInfo::State lsfilesToState(char id) { switch(id) { case 'H': return VcsStatusInfo::ItemUpToDate; //Cached case 'S': return VcsStatusInfo::ItemUpToDate; //Skip work tree case 'M': return VcsStatusInfo::ItemHasConflicts; //unmerged case 'R': return VcsStatusInfo::ItemDeleted; //removed/deleted case 'C': return VcsStatusInfo::ItemModified; //modified/changed case 'K': return VcsStatusInfo::ItemDeleted; //to be killed case '?': return VcsStatusInfo::ItemUnknown; //other } Q_ASSERT(false); return VcsStatusInfo::ItemUnknown; } void GitPlugin::parseGitStatusOutput_old(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QDir dir = job->directory(); QMap allStatus; foreach(const QString& line, outputLines) { VcsStatusInfo::State status = lsfilesToState(line[0].toLatin1()); QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(line.right(line.size()-2))); allStatus[url] = status; } QVariantList statuses; + statuses.reserve(allStatus.size()); QMap< QUrl, VcsStatusInfo::State >::const_iterator it = allStatus.constBegin(), itEnd=allStatus.constEnd(); for(; it!=itEnd; ++it) { VcsStatusInfo status; status.setUrl(it.key()); status.setState(it.value()); statuses.append(qVariantFromValue(status)); } job->setResults(statuses); } void GitPlugin::parseGitStatusOutput(DVcsJob* job) { const auto output = job->output(); const auto outputLines = output.splitRef('\n', QString::SkipEmptyParts); QDir workingDir = job->directory(); QDir dotGit = dotGitDirectory(QUrl::fromLocalFile(workingDir.absolutePath())); QVariantList statuses; QList processedFiles; foreach(const QStringRef& line, outputLines) { //every line is 2 chars for the status, 1 space then the file desc QStringRef curr=line.right(line.size()-3); QStringRef state = line.left(2); int arrow = curr.indexOf(QStringLiteral(" -> ")); if(arrow>=0) { VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString().left(arrow)))); status.setState(VcsStatusInfo::ItemDeleted); statuses.append(qVariantFromValue(status)); processedFiles += status.url(); curr = curr.mid(arrow+4); } if(curr.startsWith('\"') && curr.endsWith('\"')) { //if the path is quoted, unquote curr = curr.mid(1, curr.size()-2); } VcsStatusInfo status; status.setUrl(QUrl::fromLocalFile(dotGit.absoluteFilePath(curr.toString()))); status.setState(messageToState(state)); processedFiles.append(status.url()); qCDebug(PLUGIN_GIT) << "Checking git status for " << line << curr << status.state(); statuses.append(qVariantFromValue(status)); } QStringList paths; QStringList oldcmd=job->dvcsCommand(); QStringList::const_iterator it=oldcmd.constBegin()+oldcmd.indexOf(QStringLiteral("--"))+1, itEnd=oldcmd.constEnd(); + paths.reserve(oldcmd.size()); for(; it!=itEnd; ++it) paths += *it; //here we add the already up to date files - QStringList files = getLsFiles(job->directory(), QStringList() << QStringLiteral("-c") << QStringLiteral("--") << paths, OutputJob::Silent); + QStringList files = getLsFiles(job->directory(), QStringList{QStringLiteral("-c"), QStringLiteral("--")} << paths, OutputJob::Silent); foreach(const QString& file, files) { QUrl fileUrl = QUrl::fromLocalFile(workingDir.absoluteFilePath(file)); if(!processedFiles.contains(fileUrl)) { VcsStatusInfo status; status.setUrl(fileUrl); status.setState(VcsStatusInfo::ItemUpToDate); statuses.append(qVariantFromValue(status)); } } job->setResults(statuses); } void GitPlugin::parseGitVersionOutput(DVcsJob* job) { const auto output = job->output().trimmed(); auto versionString = output.midRef(output.lastIndexOf(' ')).split('.'); - static const QList minimumVersion = QList() << 1 << 7; + static const QList minimumVersion = QList{1, 7}; qCDebug(PLUGIN_GIT) << "checking git version" << versionString << "against" << minimumVersion; m_oldVersion = false; if (versionString.size() < minimumVersion.size()) { m_oldVersion = true; qCWarning(PLUGIN_GIT) << "invalid git version string:" << job->output().trimmed(); return; } foreach(int num, minimumVersion) { QStringRef curr = versionString.takeFirst(); int valcurr = curr.toInt(); if (valcurr < num) { m_oldVersion = true; break; } if (valcurr > num) { m_oldVersion = false; break; } } qCDebug(PLUGIN_GIT) << "the current git version is old: " << m_oldVersion; } QStringList GitPlugin::getLsFiles(const QDir &directory, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(lsFiles(directory, args, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) return job->output().split('\n', QString::SkipEmptyParts); return QStringList(); } DVcsJob* GitPlugin::gitRevParse(const QString &repository, const QStringList &args, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(QDir(repository), this, verbosity); *job << "git" << "rev-parse" << args; return job; } DVcsJob* GitPlugin::gitRevList(const QString& directory, const QStringList& args) { DVcsJob* job = new DVcsJob(urlDir(QUrl::fromLocalFile(directory)), this, KDevelop::OutputJob::Silent); { *job << "git" << "rev-list" << args; return job; } } VcsStatusInfo::State GitPlugin::messageToState(const QStringRef& msg) { Q_ASSERT(msg.size()==1 || msg.size()==2); VcsStatusInfo::State ret = VcsStatusInfo::ItemUnknown; if(msg.contains('U') || msg == QLatin1String("AA") || msg == QLatin1String("DD")) ret = VcsStatusInfo::ItemHasConflicts; else switch(msg.at(0).toLatin1()) { case 'M': ret = VcsStatusInfo::ItemModified; break; case 'A': ret = VcsStatusInfo::ItemAdded; break; case 'R': ret = VcsStatusInfo::ItemModified; break; case 'C': ret = VcsStatusInfo::ItemHasConflicts; break; case ' ': ret = msg.at(1) == 'M' ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemDeleted; break; case 'D': ret = VcsStatusInfo::ItemDeleted; break; case '?': ret = VcsStatusInfo::ItemUnknown; break; default: qCDebug(PLUGIN_GIT) << "Git status not identified:" << msg; break; } return ret; } StandardJob::StandardJob(IPlugin* parent, KJob* job, OutputJob::OutputJobVerbosity verbosity) : VcsJob(parent, verbosity) , m_job(job) , m_plugin(parent) , m_status(JobNotStarted) {} void StandardJob::start() { connect(m_job, &KJob::result, this, &StandardJob::result); m_job->start(); m_status=JobRunning; } void StandardJob::result(KJob* job) { if (job->error() == 0) { m_status = JobSucceeded; setError(NoError); } else { m_status = JobFailed; setError(UserDefinedError); } emitResult(); } VcsJob* GitPlugin::copy(const QUrl& localLocationSrc, const QUrl& localLocationDstn) { //TODO: Probably we should "git add" after return new StandardJob(this, KIO::copy(localLocationSrc, localLocationDstn), KDevelop::OutputJob::Silent); } VcsJob* GitPlugin::move(const QUrl& source, const QUrl& destination) { QDir dir = urlDir(source); QFileInfo fileInfo(source.toLocalFile()); if (fileInfo.isDir()) { if (isEmptyDirStructure(QDir(source.toLocalFile()))) { //move empty folder, git doesn't do that qCDebug(PLUGIN_GIT) << "empty folder" << source; return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } - QStringList otherStr = getLsFiles(dir, QStringList() << QStringLiteral("--others") << QStringLiteral("--") << source.toLocalFile(), KDevelop::OutputJob::Silent); + const QStringList otherStr = getLsFiles(dir, QStringList{QStringLiteral("--others"), QStringLiteral("--"), source.toLocalFile()}, KDevelop::OutputJob::Silent); if(otherStr.isEmpty()) { DVcsJob* job = new DVcsJob(dir, this, KDevelop::OutputJob::Verbose); *job << "git" << "mv" << source.toLocalFile() << destination.toLocalFile(); return job; } else { return new StandardJob(this, KIO::move(source, destination), KDevelop::OutputJob::Silent); } } void GitPlugin::parseGitRepoLocationOutput(DVcsJob* job) { job->setResults(QVariant::fromValue(QUrl::fromLocalFile(job->output()))); } VcsJob* GitPlugin::repositoryLocation(const QUrl& localLocation) { DVcsJob* job = new DVcsJob(urlDir(localLocation), this); //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "config" << "remote.origin.url"; connect(job, &DVcsJob::readyForParsing, this, &GitPlugin::parseGitRepoLocationOutput); return job; } VcsJob* GitPlugin::pull(const KDevelop::VcsLocation& localOrRepoLocationSrc, const QUrl& localRepositoryLocation) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "pull"; if(!localOrRepoLocationSrc.localUrl().isEmpty()) *job << localOrRepoLocationSrc.localUrl().url(); return job; } VcsJob* GitPlugin::push(const QUrl& localRepositoryLocation, const KDevelop::VcsLocation& localOrRepoLocationDst) { DVcsJob* job = new DVcsJob(urlDir(localRepositoryLocation), this); job->setCommunicationMode(KProcess::MergedChannels); *job << "git" << "push"; if(!localOrRepoLocationDst.localUrl().isEmpty()) *job << localOrRepoLocationDst.localUrl().url(); return job; } VcsJob* GitPlugin::resolve(const QList& localLocations, IBasicVersionControl::RecursionMode recursion) { return add(localLocations, recursion); } VcsJob* GitPlugin::update(const QList& localLocations, const KDevelop::VcsRevision& rev, IBasicVersionControl::RecursionMode recursion) { if(rev.revisionType()==VcsRevision::Special && rev.revisionValue().value()==VcsRevision::Head) { return pull(VcsLocation(), localLocations.first()); } else { DVcsJob* job = new DVcsJob(urlDir(localLocations.first()), this); { //Probably we should check first if origin is the proper remote we have to use but as a first attempt it works *job << "git" << "checkout" << rev.revisionValue().toString() << "--"; *job << (recursion == IBasicVersionControl::Recursive ? localLocations : preventRecursion(localLocations)); return job; } } } void GitPlugin::setupCommitMessageEditor(const QUrl& localLocation, KTextEdit* editor) const { new GitMessageHighlighter(editor); QFile mergeMsgFile(dotGitDirectory(localLocation).filePath(QStringLiteral(".git/MERGE_MSG"))); // Some limit on the file size should be set since whole content is going to be read into // the memory. 1Mb seems to be good value since it's rather strange to have so huge commit // message. static const qint64 maxMergeMsgFileSize = 1024*1024; if (mergeMsgFile.size() > maxMergeMsgFileSize || !mergeMsgFile.open(QIODevice::ReadOnly)) return; QString mergeMsg = QString::fromLocal8Bit(mergeMsgFile.read(maxMergeMsgFileSize)); editor->setPlainText(mergeMsg); } class GitVcsLocationWidget : public KDevelop::StandardVcsLocationWidget { Q_OBJECT public: explicit GitVcsLocationWidget(QWidget* parent = nullptr) : StandardVcsLocationWidget(parent) {} bool isCorrect() const override { return !url().isEmpty(); } }; KDevelop::VcsLocationWidget* GitPlugin::vcsLocation(QWidget* parent) const { return new GitVcsLocationWidget(parent); } void GitPlugin::registerRepositoryForCurrentBranchChanges(const QUrl& repository) { QDir dir = dotGitDirectory(repository); QString headFile = dir.absoluteFilePath(QStringLiteral(".git/HEAD")); m_watcher->addFile(headFile); } void GitPlugin::fileChanged(const QString& file) { Q_ASSERT(file.endsWith(QStringLiteral("HEAD"))); //SMTH/.git/HEAD -> SMTH/ const QUrl fileUrl = Path(file).parent().parent().toUrl(); //We need to delay the emitted signal, otherwise the branch hasn't change yet //and the repository is not functional m_branchesChange.append(fileUrl); QTimer::singleShot(1000, this, &GitPlugin::delayedBranchChanged); } void GitPlugin::delayedBranchChanged() { emit repositoryBranchChanged(m_branchesChange.takeFirst()); } CheckInRepositoryJob* GitPlugin::isInRepository(KTextEditor::Document* document) { CheckInRepositoryJob* job = new GitPluginCheckInRepositoryJob(document, repositoryRoot(document->url()).path()); job->start(); return job; } DVcsJob* GitPlugin::setConfigOption(const QUrl& repository, const QString& key, const QString& value, bool global) { auto job = new DVcsJob(urlDir(repository), this); QStringList args; args << QStringLiteral("git") << QStringLiteral("config"); if(global) args << QStringLiteral("--global"); args << key << value; *job << args; return job; } QString GitPlugin::readConfigOption(const QUrl& repository, const QString& key) { QProcess exec; exec.setWorkingDirectory(urlDir(repository).absolutePath()); - exec.start(QStringLiteral("git"), QStringList() << QStringLiteral("config") << QStringLiteral("--get") << key); + exec.start(QStringLiteral("git"), QStringList{QStringLiteral("config"), QStringLiteral("--get"), key}); exec.waitForFinished(); return exec.readAllStandardOutput().trimmed(); } #include "gitplugin.moc" diff --git a/plugins/git/gitplugincheckinrepositoryjob.cpp b/plugins/git/gitplugincheckinrepositoryjob.cpp index 71fea79921..5ec13e012f 100644 --- a/plugins/git/gitplugincheckinrepositoryjob.cpp +++ b/plugins/git/gitplugincheckinrepositoryjob.cpp @@ -1,93 +1,93 @@ /*************************************************************************** * Copyright 2014 Sven Brauch * * * * 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 "gitplugincheckinrepositoryjob.h" #include "debug.h" #include #include #include GitPluginCheckInRepositoryJob::GitPluginCheckInRepositoryJob(KTextEditor::Document* document, const QString& rootDirectory) : CheckInRepositoryJob(document) , m_hashjob(nullptr) , m_findjob(nullptr) , m_rootDirectory(rootDirectory) {} void GitPluginCheckInRepositoryJob::start() { const QTextCodec* codec = QTextCodec::codecForName(document()->encoding().toLatin1()); const QDir workingDirectory(m_rootDirectory); if ( !workingDirectory.exists() ) { emit finished(false); return; } m_findjob = new QProcess(this); m_findjob->setWorkingDirectory(m_rootDirectory); m_hashjob = new QProcess(this); m_hashjob->setWorkingDirectory(m_rootDirectory); m_hashjob->setStandardOutputProcess(m_findjob); connect(m_findjob, static_cast(&QProcess::finished), this, &GitPluginCheckInRepositoryJob::repositoryQueryFinished); connect(m_hashjob, static_cast(&QProcess::error), this, &GitPluginCheckInRepositoryJob::processFailed); connect(m_findjob, static_cast(&QProcess::error), this, &GitPluginCheckInRepositoryJob::processFailed); - m_hashjob->start(QStringLiteral("git"), QStringList() << QStringLiteral("hash-object") << QStringLiteral("--stdin")); - m_findjob->start(QStringLiteral("git"), QStringList() << QStringLiteral("cat-file") << QStringLiteral("--batch-check")); + m_hashjob->start(QStringLiteral("git"), QStringList{QStringLiteral("hash-object"), QStringLiteral("--stdin")}); + m_findjob->start(QStringLiteral("git"), QStringList{QStringLiteral("cat-file"), QStringLiteral("--batch-check")}); for ( int i = 0; i < document()->lines(); i++ ) { m_hashjob->write(codec->fromUnicode(document()->line(i))); if ( i != document()->lines() - 1 ) { m_hashjob->write("\n"); } } m_hashjob->closeWriteChannel(); } GitPluginCheckInRepositoryJob::~GitPluginCheckInRepositoryJob() { if ( m_findjob && m_findjob->state() == QProcess::Running ) { m_findjob->kill(); } if ( m_hashjob && m_hashjob->state() == QProcess::Running ) { m_hashjob->kill(); } } void GitPluginCheckInRepositoryJob::processFailed(QProcess::ProcessError err) { qCDebug(PLUGIN_GIT) << "calling git failed with error:" << err; emit finished(false); } void GitPluginCheckInRepositoryJob::repositoryQueryFinished(int) { const QByteArray output = m_findjob->readAllStandardOutput(); bool requestSucceeded = output.contains(" blob "); emit finished(requestSucceeded); } diff --git a/plugins/git/stashmanagerdialog.cpp b/plugins/git/stashmanagerdialog.cpp index 2f6ca1e375..cf0cda9e21 100644 --- a/plugins/git/stashmanagerdialog.cpp +++ b/plugins/git/stashmanagerdialog.cpp @@ -1,153 +1,154 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "stashmanagerdialog.h" #include "ui_stashmanagerdialog.h" #include "gitplugin.h" #include "stashpatchsource.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; StashManagerDialog::StashManagerDialog(const QDir& stashed, GitPlugin* plugin, QWidget* parent) : QDialog(parent), m_plugin(plugin), m_dir(stashed) { setWindowTitle(i18n("Stash Manager")); m_mainWidget = new QWidget(this); m_ui = new Ui::StashManager; m_ui->setupUi(m_mainWidget); StashModel* m = new StashModel(stashed, plugin, this); m_ui->stashView->setModel(m); connect(m_ui->show, &QPushButton::clicked, this, &StashManagerDialog::showStash); connect(m_ui->apply, &QPushButton::clicked, this, &StashManagerDialog::applyClicked); connect(m_ui->branch, &QPushButton::clicked, this, &StashManagerDialog::branchClicked); connect(m_ui->pop, &QPushButton::clicked, this, &StashManagerDialog::popClicked); connect(m_ui->drop, &QPushButton::clicked, this, &StashManagerDialog::dropClicked); connect(m, &StashModel::rowsInserted, this, &StashManagerDialog::stashesFound); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &StashManagerDialog::reject); m_mainWidget->setEnabled(false); //we won't enable it until we have the model with data and selection } StashManagerDialog::~StashManagerDialog() { delete m_ui; } void StashManagerDialog::stashesFound() { QModelIndex firstIdx=m_ui->stashView->model()->index(0, 0); m_ui->stashView->setCurrentIndex(firstIdx); m_mainWidget->setEnabled(true); } QString StashManagerDialog::selection() const { QModelIndex idx = m_ui->stashView->currentIndex(); Q_ASSERT(idx.isValid()); return idx.data().toString(); } void StashManagerDialog::runStash(const QStringList& arguments) { VcsJob* job = m_plugin->gitStash(m_dir, arguments, OutputJob::Verbose); connect(job, &VcsJob::result, this, &StashManagerDialog::accept); m_mainWidget->setEnabled(false); ICore::self()->runController()->registerJob(job); } void StashManagerDialog::showStash() { IPatchReview * review = ICore::self()->pluginController()->extensionForPlugin(); IPatchSource::Ptr stashPatch(new StashPatchSource(selection(), m_plugin, m_dir)); review->startReview(stashPatch); accept(); } void StashManagerDialog::applyClicked() { - runStash(QStringList(QStringLiteral("apply")) << selection()); + runStash(QStringList{QStringLiteral("apply"), selection()}); } void StashManagerDialog::popClicked() { - runStash(QStringList(QStringLiteral("pop")) << selection()); + runStash(QStringList{QStringLiteral("pop"), selection()}); } void StashManagerDialog::dropClicked() { QString sel = selection(); int ret = KMessageBox::questionYesNo(this, i18n("Are you sure you want to drop the stash '%1'?", sel)); if(ret == KMessageBox::Yes) - runStash(QStringList(QStringLiteral("drop")) << sel); + runStash(QStringList{QStringLiteral("drop"), sel}); } void StashManagerDialog::branchClicked() { QString branchName = QInputDialog::getText(this, i18n("KDevelop - Git Stash"), i18n("Select a name for the new branch:")); if(!branchName.isEmpty()) - runStash(QStringList(QStringLiteral("branch")) << branchName << selection()); + runStash(QStringList{QStringLiteral("branch"), branchName, selection()}); } //////////////////StashModel StashModel::StashModel(const QDir& dir, GitPlugin* git, QObject* parent) : QStandardItemModel(parent) { VcsJob* job=git->gitStash(dir, QStringList(QStringLiteral("list")), OutputJob::Silent); connect(job, &VcsJob::finished, this, &StashModel::stashListReady); ICore::self()->runController()->registerJob(job); } void StashModel::stashListReady(KJob* _job) { DVcsJob* job = qobject_cast(_job); QList< QByteArray > output = job->rawOutput().split('\n'); foreach(const QByteArray& line, output) { QList< QByteArray > fields = line.split(':'); QList elements; + elements.reserve(fields.size()); foreach(const QByteArray& field, fields) elements += new QStandardItem(QString(field.trimmed())); appendRow(elements); } } diff --git a/plugins/git/stashpatchsource.cpp b/plugins/git/stashpatchsource.cpp index 96c2f3ab4e..b3f22631b7 100644 --- a/plugins/git/stashpatchsource.cpp +++ b/plugins/git/stashpatchsource.cpp @@ -1,88 +1,88 @@ /* * * Copyright (C) 2013 David E. Narvaez * * 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 "stashpatchsource.h" #include "gitplugin.h" #include "vcs/dvcs/dvcsjob.h" #include "interfaces/icore.h" #include "interfaces/iruncontroller.h" #include #include using namespace KDevelop; StashPatchSource::StashPatchSource(const QString& stashName, GitPlugin* plugin, const QDir & baseDir) : m_stashName(stashName), m_plugin(plugin), m_baseDir(baseDir) { QTemporaryFile tempFile; tempFile.setAutoRemove(false); tempFile.open(); m_patchFile = QUrl::fromLocalFile(tempFile.fileName()); - KDevelop::DVcsJob * job = m_plugin->gitStash(m_baseDir, QStringList() << QStringLiteral("show") << QStringLiteral("-u") << m_stashName, KDevelop::OutputJob::Silent); + KDevelop::DVcsJob * job = m_plugin->gitStash(m_baseDir, QStringList{QStringLiteral("show"), QStringLiteral("-u"), m_stashName}, KDevelop::OutputJob::Silent); connect(job, &DVcsJob::resultsReady, this, &StashPatchSource::updatePatchFile); KDevelop::ICore::self()->runController()->registerJob(job); } StashPatchSource::~StashPatchSource() { QFile::remove(m_patchFile.toLocalFile()); } QUrl StashPatchSource::baseDir() const { return QUrl::fromLocalFile(m_baseDir.absolutePath()); } QUrl StashPatchSource::file() const { return m_patchFile; } void StashPatchSource::update() { } bool StashPatchSource::isAlreadyApplied() const { return false; } QString StashPatchSource::name() const { return m_stashName; } void StashPatchSource::updatePatchFile(KDevelop::VcsJob* job) { KDevelop::DVcsJob* dvcsJob = qobject_cast(job); QFile f(m_patchFile.toLocalFile()); QTextStream txtStream(&f); f.open(QIODevice::WriteOnly); txtStream << dvcsJob->rawOutput(); f.close(); emit patchChanged(); } diff --git a/plugins/git/tests/test_git.cpp b/plugins/git/tests/test_git.cpp index e7c7ba13d8..935c659b3a 100644 --- a/plugins/git/tests/test_git.cpp +++ b/plugins/git/tests/test_git.cpp @@ -1,588 +1,588 @@ /*************************************************************************** * This file was partly taken from KDevelop's cvs plugin * * Copyright 2007 Robert Gruber * * * * Adapted for Git * * Copyright 2008 Evgeniy Ivanov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "test_git.h" #include #include #include #include #include #include #include #include "../gitplugin.h" #define VERIFYJOB(j) \ do { QVERIFY(j); QVERIFY(j->exec()); QVERIFY((j)->status() == KDevelop::VcsJob::JobSucceeded); } while(0) inline QString tempDir() { return QDir::tempPath(); } inline QString gitTest_BaseDir() { return tempDir() + "/kdevGit_testdir/"; } inline QString gitTest_BaseDir2() { return tempDir() + "/kdevGit_testdir2/"; } inline QString gitRepo() { return gitTest_BaseDir() + ".git"; } inline QString gitSrcDir() { return gitTest_BaseDir() + "src/"; } inline QString gitTest_FileName() { return QStringLiteral("testfile"); } inline QString gitTest_FileName2() { return QStringLiteral("foo"); } inline QString gitTest_FileName3() { return QStringLiteral("bar"); } using namespace KDevelop; bool writeFile(const QString &path, const QString& content, QIODevice::OpenModeFlag mode = QIODevice::WriteOnly) { QFile f(path); if (!f.open(mode)) { return false; } QTextStream input(&f); input << content; return true; } void GitInitTest::initTestCase() { AutoTestShell::init({QStringLiteral("kdevgit")}); TestCore::initialize(); m_plugin = new GitPlugin(TestCore::self()); } void GitInitTest::cleanupTestCase() { delete m_plugin; TestCore::shutdown(); } void GitInitTest::init() { // Now create the basic directory structure QDir tmpdir(tempDir()); tmpdir.mkdir(gitTest_BaseDir()); tmpdir.mkdir(gitSrcDir()); tmpdir.mkdir(gitTest_BaseDir2()); } void GitInitTest::cleanup() { removeTempDirs(); } void GitInitTest::repoInit() { qDebug() << "Trying to init repo"; // make job that creates the local repository VcsJob* j = m_plugin->init(QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //check if the CVSROOT directory in the new local repository exists now QVERIFY(QFileInfo::exists(gitRepo())); //check if isValidDirectory works QVERIFY(m_plugin->isValidDirectory(QUrl::fromLocalFile(gitTest_BaseDir()))); //and for non-git dir, I hope nobody has /tmp under git QVERIFY(!m_plugin->isValidDirectory(QUrl::fromLocalFile(tempDir()))); //we have nothing, so output should be empty DVcsJob * j2 = m_plugin->gitRevParse(gitRepo(), QStringList(QStringLiteral("--branches"))); QVERIFY(j2); QVERIFY(j2->exec()); QVERIFY(j2->output().isEmpty()); // Make sure to set the Git identity so unit tests don't depend on that auto j3 = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.email"), QStringLiteral("me@example.com")); VERIFYJOB(j3); auto j4 = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.name"), QStringLiteral("My Name")); VERIFYJOB(j4); } void GitInitTest::addFiles() { qDebug() << "Adding files to the repo"; //we start it after repoInit, so we still have empty git repo QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("HELLO WORLD"))); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName2(), QStringLiteral("No, bar()!"))); //test git-status exitCode (see DVcsJob::setExitCode). VcsJob* j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); // /tmp/kdevGit_testdir/ and testfile j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); VERIFYJOB(j); QVERIFY(writeFile(gitSrcDir() + gitTest_FileName3(), QStringLiteral("No, foo()! It's bar()!"))); //test git-status exitCode again j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //repository path without trailing slash and a file in a parent directory // /tmp/repo and /tmp/repo/src/bar j = m_plugin->add(QList() << QUrl::fromLocalFile(gitSrcDir() + gitTest_FileName3())); VERIFYJOB(j); //let's use absolute path, because it's used in ContextMenus j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName2())); VERIFYJOB(j); //Now let's create several files and try "git add file1 file2 file3" - QStringList files = QStringList() << QStringLiteral("file1") << QStringLiteral("file2") << QStringLiteral("la la"); + const QStringList files{QStringLiteral("file1"), QStringLiteral("file2"), QStringLiteral("la la")}; QList multipleFiles; foreach(const QString& file, files) { QVERIFY(writeFile(gitTest_BaseDir() + file, file)); multipleFiles << QUrl::fromLocalFile(gitTest_BaseDir() + file); } j = m_plugin->add(multipleFiles); VERIFYJOB(j); } void GitInitTest::commitFiles() { qDebug() << "Committing..."; //we start it after addFiles, so we just have to commit VcsJob* j = m_plugin->commit(QStringLiteral("Test commit"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //test git-status exitCode one more time. j = m_plugin->status(QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); //since we committed the file to the "pure" repository, .git/refs/heads/master should exist //TODO: maybe other method should be used QString headRefName(gitRepo() + "/refs/heads/master"); QVERIFY(QFileInfo::exists(headRefName)); //Test the results of the "git add" DVcsJob* jobLs = new DVcsJob(gitTest_BaseDir(), m_plugin); *jobLs << "git" << "ls-tree" << "--name-only" << "-r" << "HEAD"; if (jobLs->exec() && jobLs->status() == KDevelop::VcsJob::JobSucceeded) { QStringList files = jobLs->output().split('\n'); QVERIFY(files.contains(gitTest_FileName())); QVERIFY(files.contains(gitTest_FileName2())); QVERIFY(files.contains("src/" + gitTest_FileName3())); } QString firstCommit; QFile headRef(headRefName); if (headRef.open(QIODevice::ReadOnly)) { QTextStream output(&headRef); output >> firstCommit; } headRef.close(); QVERIFY(!firstCommit.isEmpty()); qDebug() << "Committing one more time"; //let's try to change the file and test "git commit -a" QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("Just another HELLO WORLD\n"))); //add changes j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("KDevelop's Test commit2"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); QString secondCommit; if (headRef.open(QIODevice::ReadOnly)) { QTextStream output(&headRef); output >> secondCommit; } headRef.close(); QVERIFY(!secondCommit.isEmpty()); QVERIFY(firstCommit != secondCommit); } void GitInitTest::testInit() { repoInit(); } static QString runCommand(const QString& cmd, const QStringList& args) { QProcess proc; proc.setWorkingDirectory(gitTest_BaseDir()); proc.start(cmd, args); proc.waitForFinished(); return proc.readAllStandardOutput().trimmed(); } void GitInitTest::testReadAndSetConfigOption() { repoInit(); { qDebug() << "read non-existing config option"; QString nameFromPlugin = m_plugin->readConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("notexisting.asdads")); QVERIFY(nameFromPlugin.isEmpty()); } { qDebug() << "write user.name = \"John Tester\""; auto job = m_plugin->setConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.name"), QStringLiteral("John Tester")); VERIFYJOB(job); const auto name = runCommand(QStringLiteral("git"), {"config", "--get", QStringLiteral("user.name")}); QCOMPARE(name, QStringLiteral("John Tester")); } { qDebug() << "read user.name"; const QString nameFromPlugin = m_plugin->readConfigOption(QUrl::fromLocalFile(gitTest_BaseDir()), QStringLiteral("user.name")); QCOMPARE(nameFromPlugin, QStringLiteral("John Tester")); const auto name = runCommand(QStringLiteral("git"), {"config", "--get", QStringLiteral("user.name")}); QCOMPARE(name, QStringLiteral("John Tester")); } } void GitInitTest::testAdd() { repoInit(); addFiles(); } void GitInitTest::testCommit() { repoInit(); addFiles(); commitFiles(); } void GitInitTest::testBranch(const QString& newBranch) { //Already tested, so I assume that it works const QUrl baseUrl = QUrl::fromLocalFile(gitTest_BaseDir()); QString oldBranch = runSynchronously(m_plugin->currentBranch(baseUrl)).toString(); VcsRevision rev; rev.setRevisionValue(oldBranch, KDevelop::VcsRevision::GlobalNumber); VcsJob* j = m_plugin->branch(baseUrl, rev, newBranch); VERIFYJOB(j); QVERIFY(runSynchronously(m_plugin->branches(baseUrl)).toStringList().contains(newBranch)); // switch branch j = m_plugin->switchBranch(baseUrl, newBranch); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), newBranch); // get into detached head state j = m_plugin->switchBranch(baseUrl, QStringLiteral("HEAD~1")); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), QString()); // switch back j = m_plugin->switchBranch(baseUrl, newBranch); VERIFYJOB(j); QCOMPARE(runSynchronously(m_plugin->currentBranch(baseUrl)).toString(), newBranch); j = m_plugin->deleteBranch(baseUrl, oldBranch); VERIFYJOB(j); QVERIFY(!runSynchronously(m_plugin->branches(baseUrl)).toStringList().contains(oldBranch)); } void GitInitTest::testMerge() { const QString branchNames[] = {QStringLiteral("aBranchToBeMergedIntoMaster"), QStringLiteral("AnotherBranch")}; const QString files[]={QStringLiteral("First File to Appear after merging"),QStringLiteral("Second File to Appear after merging"), QStringLiteral("Another_File.txt")}; const QString content=QStringLiteral("Testing merge."); repoInit(); addFiles(); commitFiles(); const QUrl baseUrl = QUrl::fromLocalFile(gitTest_BaseDir()); VcsJob* j = m_plugin->branches(baseUrl); VERIFYJOB(j); QString curBranch = runSynchronously(m_plugin->currentBranch(baseUrl)).toString(); QCOMPARE(curBranch, QStringLiteral("master")); VcsRevision rev; rev.setRevisionValue("master", KDevelop::VcsRevision::GlobalNumber); j = m_plugin->branch(baseUrl, rev, branchNames[0]); VERIFYJOB(j); qDebug() << "Adding files to the new branch"; //we start it after repoInit, so we still have empty git repo QVERIFY(writeFile(gitTest_BaseDir() + files[0], content)); QVERIFY(writeFile(gitTest_BaseDir() + files[1], content)); QList listOfAddedFiles{QUrl::fromLocalFile(gitTest_BaseDir() + files[0]), QUrl::fromLocalFile(gitTest_BaseDir() + files[1])}; j = m_plugin->add(listOfAddedFiles); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("Committing to the new branch"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); j = m_plugin->switchBranch(baseUrl, QStringLiteral("master")); VERIFYJOB(j); j = m_plugin->mergeBranch(baseUrl, branchNames[0]); VERIFYJOB(j); auto jobLs = new DVcsJob(gitTest_BaseDir(), m_plugin); *jobLs << "git" << "ls-tree" << "--name-only" << "-r" << "HEAD" ; if (jobLs->exec() && jobLs->status() == KDevelop::VcsJob::JobSucceeded) { QStringList files = jobLs->output().split('\n'); qDebug() << "Files in this Branch: " << files; QVERIFY(files.contains(files[0])); QVERIFY(files.contains(files[1])); } //Testing one more time. j = m_plugin->switchBranch(baseUrl, branchNames[0]); VERIFYJOB(j); rev.setRevisionValue(branchNames[0], KDevelop::VcsRevision::GlobalNumber); j = m_plugin->branch(baseUrl, rev, branchNames[1]); VERIFYJOB(j); QVERIFY(writeFile(gitTest_BaseDir() + files[2], content)); j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + files[2])); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("Committing to AnotherBranch"), QList() << baseUrl); VERIFYJOB(j); j = m_plugin->switchBranch(baseUrl, branchNames[0]); VERIFYJOB(j); j = m_plugin->mergeBranch(baseUrl, branchNames[1]); VERIFYJOB(j); qDebug() << j->errorString() ; jobLs = new DVcsJob(gitTest_BaseDir(), m_plugin); *jobLs << "git" << "ls-tree" << "--name-only" << "-r" << "HEAD" ; if (jobLs->exec() && jobLs->status() == KDevelop::VcsJob::JobSucceeded) { QStringList files = jobLs->output().split('\n'); QVERIFY(files.contains(files[2])); qDebug() << "Files in this Branch: " << files; } j = m_plugin->switchBranch(baseUrl, QStringLiteral("master")); VERIFYJOB(j); j = m_plugin->mergeBranch(baseUrl, branchNames[1]); VERIFYJOB(j); qDebug() << j->errorString() ; jobLs = new DVcsJob(gitTest_BaseDir(), m_plugin); *jobLs << "git" << "ls-tree" << "--name-only" << "-r" << "HEAD" ; if (jobLs->exec() && jobLs->status() == KDevelop::VcsJob::JobSucceeded) { QStringList files = jobLs->output().split('\n'); QVERIFY(files.contains(files[2])); qDebug() << "Files in this Branch: " << files; } } void GitInitTest::testBranching() { repoInit(); addFiles(); commitFiles(); const QUrl baseUrl = QUrl::fromLocalFile(gitTest_BaseDir()); VcsJob* j = m_plugin->branches(baseUrl); VERIFYJOB(j); QString curBranch = runSynchronously(m_plugin->currentBranch(baseUrl)).toString(); QCOMPARE(curBranch, QStringLiteral("master")); testBranch(QStringLiteral("new")); testBranch(QStringLiteral("averylongbranchnamejusttotestlongnames")); testBranch(QStringLiteral("KDE/4.10")); } void GitInitTest::revHistory() { repoInit(); addFiles(); commitFiles(); QVector commits = m_plugin->allCommits(gitTest_BaseDir()); QVERIFY(!commits.isEmpty()); QStringList logMessages; for (int i = 0; i < commits.count(); ++i) logMessages << commits[i].log(); QCOMPARE(commits.count(), 2); QCOMPARE(logMessages[0], QStringLiteral("KDevelop's Test commit2")); //0 is later than 1! QCOMPARE(logMessages[1], QStringLiteral("Test commit")); QVERIFY(commits[1].parents().isEmpty()); //0 is later than 1! QVERIFY(!commits[0].parents().isEmpty()); //initial commit is on the top QVERIFY(commits[1].commit().contains(QRegExp("^\\w{,40}$"))); QVERIFY(commits[0].commit().contains(QRegExp("^\\w{,40}$"))); QVERIFY(commits[0].parents()[0].contains(QRegExp("^\\w{,40}$"))); } void GitInitTest::testAnnotation() { repoInit(); addFiles(); commitFiles(); // called after commitFiles QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->commit(QStringLiteral("KDevelop's Test commit3"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); j = m_plugin->annotate(QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName()), VcsRevision::createSpecialRevision(VcsRevision::Head)); VERIFYJOB(j); QList results = j->fetchResults().toList(); QCOMPARE(results.size(), 2); QVERIFY(results.at(0).canConvert()); VcsAnnotationLine annotation = results.at(0).value(); QCOMPARE(annotation.lineNumber(), 0); QCOMPARE(annotation.commitMessage(), QStringLiteral("KDevelop's Test commit2")); QVERIFY(results.at(1).canConvert()); annotation = results.at(1).value(); QCOMPARE(annotation.lineNumber(), 1); QCOMPARE(annotation.commitMessage(), QStringLiteral("KDevelop's Test commit3")); } void GitInitTest::testRemoveEmptyFolder() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("emptydir")); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"emptydir/")); if (j) VERIFYJOB(j); QVERIFY(!d.exists(QStringLiteral("emptydir"))); } void GitInitTest::testRemoveEmptyFolderInFolder() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("dir")); QDir d2(gitTest_BaseDir()+"dir"); d2.mkdir(QStringLiteral("emptydir")); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"dir/")); if (j) VERIFYJOB(j); QVERIFY(!d.exists(QStringLiteral("dir"))); } void GitInitTest::testRemoveUnindexedFile() { repoInit(); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + gitTest_FileName())); if (j) VERIFYJOB(j); QVERIFY(!QFile::exists(gitTest_BaseDir() + gitTest_FileName())); } void GitInitTest::testRemoveFolderContainingUnversionedFiles() { repoInit(); QDir d(gitTest_BaseDir()); d.mkdir(QStringLiteral("dir")); QVERIFY(writeFile(gitTest_BaseDir() + "dir/foo", QStringLiteral("An appended line"), QIODevice::Append)); VcsJob* j = m_plugin->add(QList() << QUrl::fromLocalFile(gitTest_BaseDir()+"dir")); VERIFYJOB(j); j = m_plugin->commit(QStringLiteral("initial commit"), QList() << QUrl::fromLocalFile(gitTest_BaseDir())); VERIFYJOB(j); QVERIFY(writeFile(gitTest_BaseDir() + "dir/bar", QStringLiteral("An appended line"), QIODevice::Append)); j = m_plugin->remove(QList() << QUrl::fromLocalFile(gitTest_BaseDir() + "dir")); if (j) VERIFYJOB(j); QVERIFY(!QFile::exists(gitTest_BaseDir() + "dir")); } void GitInitTest::removeTempDirs() { for (const auto& dirPath : {gitTest_BaseDir(), gitTest_BaseDir2()}) { QDir dir(dirPath); if (dir.exists() && !dir.removeRecursively()) { qDebug() << "QDir::removeRecursively(" << dirPath << ") returned false"; } } } void GitInitTest::testDiff() { repoInit(); addFiles(); commitFiles(); QVERIFY(writeFile(gitTest_BaseDir() + gitTest_FileName(), QStringLiteral("something else"))); VcsRevision srcrev = VcsRevision::createSpecialRevision(VcsRevision::Base); VcsRevision dstrev = VcsRevision::createSpecialRevision(VcsRevision::Working); VcsJob* j = m_plugin->diff(QUrl::fromLocalFile(gitTest_BaseDir()), srcrev, dstrev, IBasicVersionControl::Recursive); VERIFYJOB(j); KDevelop::VcsDiff d = j->fetchResults().value(); QVERIFY(d.baseDiff().isLocalFile()); QString path = d.baseDiff().toLocalFile(); QVERIFY(QDir().exists(path+"/.git")); } QTEST_MAIN(GitInitTest) // #include "gittest.moc" diff --git a/plugins/grepview/grepdialog.cpp b/plugins/grepview/grepdialog.cpp index 44ba553bc8..e7e95ce7d1 100644 --- a/plugins/grepview/grepdialog.cpp +++ b/plugins/grepview/grepdialog.cpp @@ -1,556 +1,578 @@ /*************************************************************************** * Copyright 1999-2001 Bernd Gehrmann and the KDevelop Team * * bernd@kdevelop.org * * Copyright 2007 Dukju Ahn * * Copyright 2010 Julien Desgats * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "grepdialog.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 "grepviewplugin.h" #include "grepoutputview.h" #include "grepfindthread.h" #include "greputil.h" using namespace KDevelop; namespace { inline QString allOpenFilesString() { return i18n("All Open Files"); } inline QString allOpenProjectsString() { return i18n("All Open Projects"); } -inline QStringList template_desc() { return QStringList() - << QStringLiteral("verbatim") - << QStringLiteral("word") - << QStringLiteral("assignment") - << QStringLiteral("->MEMBER(") - << QStringLiteral("class::MEMBER(") - << QStringLiteral("OBJECT->member("); +inline QStringList template_desc() +{ + return QStringList{ + QStringLiteral("verbatim"), + QStringLiteral("word"), + QStringLiteral("assignment"), + QStringLiteral("->MEMBER("), + QStringLiteral("class::MEMBER("), + QStringLiteral("OBJECT->member("), + }; } -inline QStringList template_str() { return QStringList() - << QStringLiteral("%s") - << QStringLiteral("\\b%s\\b") - << QStringLiteral("\\b%s\\b\\s*=[^=]") - << QStringLiteral("\\->\\s*\\b%s\\b\\s*\\(") - << QStringLiteral("([a-z0-9_$]+)\\s*::\\s*\\b%s\\b\\s*\\(") - << QStringLiteral("\\b%s\\b\\s*\\->\\s*([a-z0-9_$]+)\\s*\\("); +inline QStringList template_str() +{ + return QStringList{ + QStringLiteral("%s"), + QStringLiteral("\\b%s\\b"), + QStringLiteral("\\b%s\\b\\s*=[^=]"), + QStringLiteral("\\->\\s*\\b%s\\b\\s*\\("), + QStringLiteral("([a-z0-9_$]+)\\s*::\\s*\\b%s\\b\\s*\\("), + QStringLiteral("\\b%s\\b\\s*\\->\\s*([a-z0-9_$]+)\\s*\\("), + }; } -inline QStringList repl_template() { return QStringList() - << QStringLiteral("%s") - << QStringLiteral("%s") - << QStringLiteral("%s = ") - << QStringLiteral("->%s(") - << QStringLiteral("\\1::%s(") - << QStringLiteral("%s->\\1("); +inline QStringList repl_template() +{ + return QStringList{ + QStringLiteral("%s"), + QStringLiteral("%s"), + QStringLiteral("%s = "), + QStringLiteral("->%s("), + QStringLiteral("\\1::%s("), + QStringLiteral("%s->\\1("), + }; } -inline QStringList filepatterns() { return QStringList() - << QStringLiteral("*.h,*.hxx,*.hpp,*.hh,*.h++,*.H,*.tlh,*.cpp,*.cc,*.C,*.c++,*.cxx,*.ocl,*.inl,*.idl,*.c,*.m,*.mm,*.M,*.y,*.ypp,*.yxx,*.y++,*.l") - << QStringLiteral("*.cpp,*.cc,*.C,*.c++,*.cxx,*.ocl,*.inl,*.c,*.m,*.mm,*.M") - << QStringLiteral("*.h,*.hxx,*.hpp,*.hh,*.h++,*.H,*.tlh,*.idl") - << QStringLiteral("*.adb") - << QStringLiteral("*.cs") - << QStringLiteral("*.f") - << QStringLiteral("*.html,*.htm") - << QStringLiteral("*.hs") - << QStringLiteral("*.java") - << QStringLiteral("*.js") - << QStringLiteral("*.php,*.php3,*.php4") - << QStringLiteral("*.pl") - << QStringLiteral("*.pp,*.pas") - << QStringLiteral("*.py") - << QStringLiteral("*.js,*.css,*.yml,*.rb,*.rhtml,*.html.erb,*.rjs,*.js.rjs,*.rxml,*.xml.builder") - << QStringLiteral("CMakeLists.txt,*.cmake") - << QStringLiteral("*"); +inline QStringList filepatterns() +{ + return QStringList{ + QStringLiteral("*.h,*.hxx,*.hpp,*.hh,*.h++,*.H,*.tlh,*.cpp,*.cc,*.C,*.c++,*.cxx,*.ocl,*.inl,*.idl,*.c,*.m,*.mm,*.M,*.y,*.ypp,*.yxx,*.y++,*.l"), + QStringLiteral("*.cpp,*.cc,*.C,*.c++,*.cxx,*.ocl,*.inl,*.c,*.m,*.mm,*.M"), + QStringLiteral("*.h,*.hxx,*.hpp,*.hh,*.h++,*.H,*.tlh,*.idl"), + QStringLiteral("*.adb"), + QStringLiteral("*.cs"), + QStringLiteral("*.f"), + QStringLiteral("*.html,*.htm"), + QStringLiteral("*.hs"), + QStringLiteral("*.java"), + QStringLiteral("*.js"), + QStringLiteral("*.php,*.php3,*.php4"), + QStringLiteral("*.pl"), + QStringLiteral("*.pp,*.pas"), + QStringLiteral("*.py"), + QStringLiteral("*.js,*.css,*.yml,*.rb,*.rhtml,*.html.erb,*.rjs,*.js.rjs,*.rxml,*.xml.builder"), + QStringLiteral("CMakeLists.txt,*.cmake"), + QStringLiteral("*"), + }; } -inline QStringList excludepatterns() { return QStringList() - << QStringLiteral("/CVS/,/SCCS/,/.svn/,/_darcs/,/build/,/.git/") - << QString(); +inline QStringList excludepatterns() +{ + return QStringList{ + QStringLiteral("/CVS/,/SCCS/,/.svn/,/_darcs/,/build/,/.git/"), + QString(), + }; } ///Separator used to separate search paths. inline QString pathsSeparator() { return (QStringLiteral(";")); } ///Returns the chosen directories or files (only the top directories, not subfiles) QList getDirectoryChoice(const QString& text) { QList ret; if (text == allOpenFilesString()) { - foreach(IDocument* doc, ICore::self()->documentController()->openDocuments()) + const auto openDocuments = ICore::self()->documentController()->openDocuments(); + ret.reserve(openDocuments.size()); + for (auto* doc : openDocuments) { ret << doc->url(); + } } else if (text == allOpenProjectsString()) { - foreach(IProject* project, ICore::self()->projectController()->projects()) + const auto projects = ICore::self()->projectController()->projects(); + ret.reserve(projects.size()); + for (auto* project : projects) { ret << project->path().toUrl(); + } } else { QStringList semicolonSeparatedFileList = text.split(pathsSeparator()); if (!semicolonSeparatedFileList.isEmpty() && QFileInfo::exists(semicolonSeparatedFileList[0])) { // We use QFileInfo to make sure this is really a semicolon-separated file list, not a file containing // a semicolon in the name. + ret.reserve(semicolonSeparatedFileList.size()); foreach(const QString& file, semicolonSeparatedFileList) ret << QUrl::fromLocalFile(file).adjusted(QUrl::StripTrailingSlash); } else { ret << QUrl::fromUserInput(text).adjusted(QUrl::StripTrailingSlash); } } return ret; } ///Check if all directories are part of a project bool directoriesInProject(const QString& dir) { foreach (const QUrl& url, getDirectoryChoice(dir)) { IProject *proj = ICore::self()->projectController()->findProjectForUrl(url); if (!proj || !proj->path().toUrl().isLocalFile()) { return false; } } return true; } ///Max number of items in paths combo box. const int pathsMaxCount = 25; } GrepDialog::GrepDialog(GrepViewPlugin *plugin, QWidget *parent, bool show) : QDialog(parent), Ui::GrepWidget(), m_plugin(plugin), m_show(show) { setAttribute(Qt::WA_DeleteOnClose); // if we don't intend on showing the dialog, we can skip all UI setup if (!m_show) { return; } setWindowTitle( i18n("Find/Replace in Files") ); setupUi(this); adjustSize(); auto searchButton = buttonBox->button(QDialogButtonBox::Ok); Q_ASSERT(searchButton); searchButton->setText(i18nc("@action:button", "Search...")); searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); connect(searchButton, &QPushButton::clicked, this, &GrepDialog::startSearch); connect(buttonBox, &QDialogButtonBox::rejected, this, &GrepDialog::reject); KConfigGroup cg = ICore::self()->activeSession()->config()->group( "GrepDialog" ); patternCombo->addItems( cg.readEntry("LastSearchItems", QStringList()) ); patternCombo->setInsertPolicy(QComboBox::InsertAtTop); templateTypeCombo->addItems(template_desc()); templateTypeCombo->setCurrentIndex( cg.readEntry("LastUsedTemplateIndex", 0) ); templateEdit->addItems( cg.readEntry("LastUsedTemplateString", template_str()) ); templateEdit->setEditable(true); templateEdit->setCompletionMode(KCompletion::CompletionPopup); KCompletion* comp = templateEdit->completionObject(); connect(templateEdit, static_cast(&KComboBox::returnPressed), comp, static_cast(&KCompletion::addItem)); for(int i=0; icount(); i++) comp->addItem(templateEdit->itemText(i)); replacementTemplateEdit->addItems( cg.readEntry("LastUsedReplacementTemplateString", repl_template()) ); replacementTemplateEdit->setEditable(true); replacementTemplateEdit->setCompletionMode(KCompletion::CompletionPopup); comp = replacementTemplateEdit->completionObject(); connect(replacementTemplateEdit, static_cast(&KComboBox::returnPressed), comp, static_cast(&KCompletion::addItem)); for(int i=0; icount(); i++) comp->addItem(replacementTemplateEdit->itemText(i)); regexCheck->setChecked(cg.readEntry("regexp", false )); caseSensitiveCheck->setChecked(cg.readEntry("case_sens", true)); searchPaths->setCompletionObject(new KUrlCompletion()); searchPaths->setAutoDeleteCompletionObject(true); QList projects = m_plugin->core()->projectController()->projects(); searchPaths->addItems(cg.readEntry("SearchPaths", QStringList(!projects.isEmpty() ? allOpenProjectsString() : QDir::homePath() ) )); searchPaths->setInsertPolicy(QComboBox::InsertAtTop); syncButton->setIcon(QIcon::fromTheme(QStringLiteral("dirsync"))); syncButton->setMenu(createSyncButtonMenu()); depthSpin->setValue(cg.readEntry("depth", -1)); limitToProjectCheck->setChecked(cg.readEntry("search_project_files", true)); filesCombo->addItems(cg.readEntry("file_patterns", filepatterns())); excludeCombo->addItems(cg.readEntry("exclude_patterns", excludepatterns()) ); connect(templateTypeCombo, static_cast(&KComboBox::activated), this, &GrepDialog::templateTypeComboActivated); connect(patternCombo, &QComboBox::editTextChanged, this, &GrepDialog::patternComboEditTextChanged); patternComboEditTextChanged( patternCombo->currentText() ); patternCombo->setFocus(); connect(searchPaths, static_cast(&KComboBox::activated), this, &GrepDialog::setSearchLocations); directorySelector->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); connect(directorySelector, &QPushButton::clicked, this, &GrepDialog::selectDirectoryDialog ); } void GrepDialog::selectDirectoryDialog() { const QString dirName = QFileDialog::getExistingDirectory( this, i18nc("@title:window", "Select directory to search in"), searchPaths->lineEdit()->text()); if (!dirName.isEmpty()) { setSearchLocations(dirName); } } void GrepDialog::addUrlToMenu(QMenu* menu, const QUrl& url) { QAction* action = menu->addAction(m_plugin->core()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain)); action->setData(QVariant(url.toString(QUrl::PreferLocalFile))); connect(action, &QAction::triggered, this, &GrepDialog::synchronizeDirActionTriggered); } void GrepDialog::addStringToMenu(QMenu* menu, const QString& string) { QAction* action = menu->addAction(string); action->setData(QVariant(string)); connect(action, &QAction::triggered, this, &GrepDialog::synchronizeDirActionTriggered); } void GrepDialog::synchronizeDirActionTriggered(bool) { QAction* action = qobject_cast(sender()); Q_ASSERT(action); setSearchLocations(action->data().toString()); } QMenu* GrepDialog::createSyncButtonMenu() { QMenu* ret = new QMenu(this); QSet hadUrls; IDocument *doc = m_plugin->core()->documentController()->activeDocument(); if ( doc ) { Path url = Path(doc->url()).parent(); // always add the current file's parent directory hadUrls.insert(url); addUrlToMenu(ret, url.toUrl()); url = url.parent(); while(m_plugin->core()->projectController()->findProjectForUrl(url.toUrl())) { if(hadUrls.contains(url)) break; hadUrls.insert(url); addUrlToMenu(ret, url.toUrl()); url = url.parent(); } } QVector otherProjectUrls; foreach(IProject* project, m_plugin->core()->projectController()->projects()) { if (!hadUrls.contains(project->path())) { otherProjectUrls.append(project->path().toUrl()); } } // sort the remaining project URLs alphabetically std::sort(otherProjectUrls.begin(), otherProjectUrls.end()); foreach(const QUrl& url, otherProjectUrls) { addUrlToMenu(ret, url); } ret->addSeparator(); addStringToMenu(ret, allOpenFilesString()); addStringToMenu(ret, allOpenProjectsString()); return ret; } GrepDialog::~GrepDialog() { } void GrepDialog::setVisible(bool visible) { QDialog::setVisible(visible && m_show); } void GrepDialog::closeEvent(QCloseEvent* closeEvent) { Q_UNUSED(closeEvent); if (!m_show) { return; } KConfigGroup cg = ICore::self()->activeSession()->config()->group( "GrepDialog" ); // memorize the last patterns and paths cg.writeEntry("LastSearchItems", qCombo2StringList(patternCombo)); cg.writeEntry("regexp", regexCheck->isChecked()); cg.writeEntry("depth", depthSpin->value()); cg.writeEntry("search_project_files", limitToProjectCheck->isChecked()); cg.writeEntry("case_sens", caseSensitiveCheck->isChecked()); cg.writeEntry("exclude_patterns", qCombo2StringList(excludeCombo)); cg.writeEntry("file_patterns", qCombo2StringList(filesCombo)); cg.writeEntry("LastUsedTemplateIndex", templateTypeCombo->currentIndex()); cg.writeEntry("LastUsedTemplateString", qCombo2StringList(templateEdit)); cg.writeEntry("LastUsedReplacementTemplateString", qCombo2StringList(replacementTemplateEdit)); cg.writeEntry("SearchPaths", qCombo2StringList(searchPaths)); cg.sync(); } void GrepDialog::templateTypeComboActivated(int index) { templateEdit->setCurrentItem( template_str().at(index), true ); replacementTemplateEdit->setCurrentItem( repl_template().at(index), true ); } void GrepDialog::setSettings(const GrepJobSettings& settings) { patternCombo->setEditText(settings.pattern); patternComboEditTextChanged(settings.pattern); m_settings.pattern = settings.pattern; limitToProjectCheck->setEnabled(settings.projectFilesOnly); limitToProjectLabel->setEnabled(settings.projectFilesOnly); m_settings.projectFilesOnly = settings.projectFilesOnly; // Note: everything else is set by a user } GrepJobSettings GrepDialog::settings() const { return m_settings; } void GrepDialog::historySearch(QVector &settingsHistory) { // clear the current settings history and pass it to a job list m_historyJobSettings.clear(); m_historyJobSettings.swap(settingsHistory); // check if anything is do be done and if all projects are loaded if (!m_historyJobSettings.empty() && !checkProjectsOpened()) { connect(KDevelop::ICore::self()->projectController(), &KDevelop::IProjectController::projectOpened, this, &GrepDialog::checkProjectsOpened); } } void GrepDialog::setSearchLocations(const QString& dir) { if (!dir.isEmpty()) { if (m_show) { if (QDir::isAbsolutePath(dir)) { static_cast(searchPaths->completionObject())->setDir( QUrl::fromLocalFile(dir) ); } if (searchPaths->contains(dir)) { searchPaths->removeItem(searchPaths->findText(dir)); } searchPaths->insertItem(0, dir); searchPaths->setCurrentItem(dir); if (searchPaths->count() > pathsMaxCount) { searchPaths->removeItem(searchPaths->count() - 1); } } else { m_settings.searchPaths = dir; } } m_settings.projectFilesOnly = directoriesInProject(dir); } void GrepDialog::patternComboEditTextChanged( const QString& text) { buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); } bool GrepDialog::checkProjectsOpened() { // only proceed if all projects have been opened if (KDevelop::ICore::self()->activeSession()->config()->group("General Options").readEntry("Open Projects", QList()).count() != KDevelop::ICore::self()->projectController()->projects().count()) return false; foreach (IProject* p, KDevelop::ICore::self()->projectController()->projects()) { if (!p->isReady()) return false; } // do the grep jobs one by one connect(m_plugin, &GrepViewPlugin::grepJobFinished, this, &GrepDialog::nextHistory); QTimer::singleShot(0, this, [=]() {nextHistory(true);}); return true; } void GrepDialog::nextHistory(bool next) { if (next && !m_historyJobSettings.empty()) { m_settings = m_historyJobSettings.takeFirst(); startSearch(); } else { close(); } } bool GrepDialog::isPartOfChoice(const QUrl& url) const { foreach(const QUrl& choice, getDirectoryChoice(m_settings.searchPaths)) { if(choice.isParentOf(url) || choice == url) return true; } return false; } void GrepDialog::startSearch() { // if m_show is false, all settings are fixed in m_settings if (m_show) updateSettings(); const QStringList include = GrepFindFilesThread::parseInclude(m_settings.files); const QStringList exclude = GrepFindFilesThread::parseExclude(m_settings.exclude); // search for unsaved documents QList unsavedFiles; foreach(IDocument* doc, ICore::self()->documentController()->openDocuments()) { QUrl docUrl = doc->url(); if (doc->state() != IDocument::Clean && isPartOfChoice(docUrl) && QDir::match(include, docUrl.fileName()) && !QDir::match(exclude, docUrl.toLocalFile()) ) { unsavedFiles << doc; } } if(!ICore::self()->documentController()->saveSomeDocuments(unsavedFiles)) { close(); return; } const QString descriptionOrUrl(m_settings.searchPaths); QList choice = getDirectoryChoice(descriptionOrUrl); QString description = descriptionOrUrl; // Shorten the description if(descriptionOrUrl != allOpenFilesString() && descriptionOrUrl != allOpenProjectsString()) { auto prettyFileName = [](const QUrl& url) { return ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain); }; if (choice.size() > 1) { description = i18np("%2, and %1 more item", "%2, and %1 more items", choice.size() - 1, prettyFileName(choice[0])); } else if (!choice.isEmpty()) { description = prettyFileName(choice[0]); } } GrepOutputView *toolView = (GrepOutputView*)ICore::self()->uiController()->findToolView( i18n("Find/Replace in Files"), m_plugin->toolViewFactory(), m_settings.fromHistory ? IUiController::Create : IUiController::CreateAndRaise); if (m_settings.fromHistory) { // when restored from history, only display the parameters toolView->renewModel(m_settings, i18n("Search \"%1\" in %2", m_settings.pattern, description)); emit m_plugin->grepJobFinished(true); } else { GrepOutputModel* outputModel = toolView->renewModel(m_settings, i18n("Search \"%1\" in %2 (at time %3)", m_settings.pattern, description, QTime::currentTime().toString(QStringLiteral("hh:mm")))); GrepJob* job = m_plugin->newGrepJob(); connect(job, &GrepJob::showErrorMessage, toolView, &GrepOutputView::showErrorMessage); //the GrepOutputModel gets the 'showMessage' signal to store it and forward //it to toolView connect(job, &GrepJob::showMessage, outputModel, &GrepOutputModel::showMessageSlot); connect(outputModel, &GrepOutputModel::showMessage, toolView, &GrepOutputView::showMessage); connect(toolView, &GrepOutputView::outputViewIsClosed, job, [=]() {job->kill();}); job->setOutputModel(outputModel); job->setDirectoryChoice(choice); job->setSettings(m_settings); ICore::self()->runController()->registerJob(job); } m_plugin->rememberSearchDirectory(descriptionOrUrl); // if m_show is false, the dialog is closed somewhere else if (m_show) close(); } void GrepDialog::updateSettings() { if (limitToProjectCheck->isEnabled()) m_settings.projectFilesOnly = limitToProjectCheck->isChecked(); m_settings.caseSensitive = caseSensitiveCheck->isChecked(); m_settings.regexp = regexCheck->isChecked(); m_settings.depth = depthSpin->value(); m_settings.pattern = patternCombo->currentText(); m_settings.searchTemplate = templateEdit->currentText().isEmpty() ? QStringLiteral("%s") : templateEdit->currentText(); m_settings.replacementTemplate = replacementTemplateEdit->currentText(); m_settings.files = filesCombo->currentText(); m_settings.exclude = excludeCombo->currentText(); m_settings.searchPaths = searchPaths->currentText(); } diff --git a/plugins/grepview/grepfindthread.cpp b/plugins/grepview/grepfindthread.cpp index 46e8567b0a..85402b5a33 100644 --- a/plugins/grepview/grepfindthread.cpp +++ b/plugins/grepview/grepfindthread.cpp @@ -1,176 +1,179 @@ #include "grepfindthread.h" #include "debug.h" #include #include #include #include #include #include #include using KDevelop::IndexedString; /** * @return Return true in case @p url is in @p dir within a maximum depth of @p maxDepth */ static bool isInDirectory(const QUrl& url, const QUrl& dir, int maxDepth) { QUrl folderUrl = url.adjusted(QUrl::RemoveFilename); int currentLevel = maxDepth; while(currentLevel > 0) { folderUrl = folderUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); if ( folderUrl == dir.adjusted(QUrl::StripTrailingSlash) ) { return true; } currentLevel--; } return false; } // the abort parameter must be volatile so that it // is evaluated every time - optimization might prevent that static QList thread_getProjectFiles(const QUrl& dir, int depth, const QStringList& include, const QStringList& exlude, volatile bool &abort) { ///@todo This is not thread-safe! KDevelop::IProject *project = KDevelop::ICore::self()->projectController()->findProjectForUrl( dir ); QList res; if(!project) return res; const QSet fileSet = project->fileSet(); foreach( const IndexedString &item, fileSet ) { if(abort) break; QUrl url = item.toUrl(); if( url != dir ) { if ( depth == 0 ) { if ( url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) != dir.adjusted(QUrl::StripTrailingSlash) ) { continue; } } else if ( !dir.isParentOf(url) ) { continue; } else if ( depth > 0 ) { // To ensure the current file is within the defined depth limit, navigate up the tree for as many levels // as the depth value, trying to find "dir", which is the project folder. If after all the loops there // is no match, it means the current file is deeper down the project tree than the limit depth, and so // it must be skipped. if(!isInDirectory(url, dir, depth)) continue; } } if( QDir::match(include, url.fileName()) && !QDir::match(exlude, url.toLocalFile()) ) res << url; } return res; } static QList thread_findFiles(const QDir& dir, int depth, const QStringList& include, const QStringList& exclude, volatile bool &abort) { QFileInfoList infos = dir.entryInfoList(include, QDir::NoDotAndDotDot|QDir::Files|QDir::Readable); if(!QFileInfo(dir.path()).isDir()) infos << QFileInfo(dir.path()); QList dirFiles; foreach(const QFileInfo &currFile, infos) { QString currName = currFile.canonicalFilePath(); if(!QDir::match(exclude, currName)) dirFiles << QUrl::fromLocalFile(currName); } if(depth != 0) { static const QDir::Filters dirFilter = QDir::NoDotAndDotDot|QDir::AllDirs|QDir::Readable|QDir::NoSymLinks; foreach(const QFileInfo &currDir, dir.entryInfoList(QStringList(), dirFilter)) { if(abort) break; QString canonical = currDir.canonicalFilePath(); if (!canonical.startsWith(dir.canonicalPath())) continue; if ( depth > 0 ) { depth--; } dirFiles << thread_findFiles(canonical, depth, include, exclude, abort); } } return dirFiles; } GrepFindFilesThread::GrepFindFilesThread(QObject* parent, const QList& startDirs, int depth, const QString& pats, const QString& excl, bool onlyProject) : QThread(parent) , m_startDirs(startDirs) , m_patString(pats) , m_exclString(excl) , m_depth(depth) , m_project(onlyProject) , m_tryAbort(false) { setTerminationEnabled(false); } void GrepFindFilesThread::tryAbort() { m_tryAbort = true; } bool GrepFindFilesThread::triesToAbort() const { return m_tryAbort; } void GrepFindFilesThread::run() { QStringList include = GrepFindFilesThread::parseInclude(m_patString); QStringList exclude = GrepFindFilesThread::parseExclude(m_exclString); qCDebug(PLUGIN_GREPVIEW) << "running with start dir" << m_startDirs; foreach(const QUrl& directory, m_startDirs) { if(m_project) m_files += thread_getProjectFiles(directory, m_depth, include, exclude, m_tryAbort); else { m_files += thread_findFiles(directory.toLocalFile(), m_depth, include, exclude, m_tryAbort); } } } QList GrepFindFilesThread::files() const { auto tmpList = QList::fromSet(m_files.toSet()); std::sort(tmpList.begin(), tmpList.end()); return tmpList; } QStringList GrepFindFilesThread::parseExclude(const QString& excl) { QStringList exclude; // Split around commas or spaces - foreach(const QString &sub, excl.split(QRegExp(",|\\s"), QString::SkipEmptyParts)) + const auto excludesList = excl.split(QRegExp(",|\\s"), QString::SkipEmptyParts); + exclude.reserve(excludesList.size()); + for (const auto& sub : excludesList) { exclude << QStringLiteral("*%1*").arg(sub); + } return exclude; } QStringList GrepFindFilesThread::parseInclude(const QString& inc) { // Split around commas or spaces return inc.split(QRegExp(",|\\s"), QString::SkipEmptyParts); } diff --git a/plugins/grepview/greputil.cpp b/plugins/grepview/greputil.cpp index c034f00f88..07154eadae 100644 --- a/plugins/grepview/greputil.cpp +++ b/plugins/grepview/greputil.cpp @@ -1,65 +1,65 @@ /*************************************************************************** * Copyright 2010 Julien Desgats * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "greputil.h" #include #include #include static int const MAX_LAST_SEARCH_ITEMS_COUNT = 15; QString substitudePattern(const QString& pattern, const QString& searchString) { QString subst = searchString; QString result; bool expectEscape = false; foreach(const QChar ch, pattern) { if(expectEscape) { expectEscape = false; if(ch == '%') result.append('%'); else if(ch == 's') result.append(subst); else - result.append('%').append(ch); + result.append(QLatin1Char('%') + ch); } else if(ch == '%') expectEscape = true; else result.append(ch); } return result; } QStringList qCombo2StringList( QComboBox* combo, bool allowEmpty ) { QStringList list; if (!combo) { return list; } QString currentText = combo->currentText(); int skippedItem = combo->currentIndex(); if (!currentText.isEmpty() || allowEmpty) { list << currentText; } if (skippedItem != -1 && currentText != combo->itemText(skippedItem)) { skippedItem = -1; } for (int i = 0; i < std::min(MAX_LAST_SEARCH_ITEMS_COUNT, combo->count()); ++i) { if (i != skippedItem && !combo->itemText(i).isEmpty()) { list << combo->itemText(i); } } return list; } diff --git a/plugins/heaptrack/visualizer.cpp b/plugins/heaptrack/visualizer.cpp index b75724b2dc..1ae339f944 100644 --- a/plugins/heaptrack/visualizer.cpp +++ b/plugins/heaptrack/visualizer.cpp @@ -1,72 +1,72 @@ /* This file is part of KDevelop Copyright 2017 Anton Anikin 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "visualizer.h" #include "debug.h" #include "globalsettings.h" #include "utils.h" #include #include #include #include namespace Heaptrack { Visualizer::Visualizer(const QString& resultsFile, QObject* parent) : QProcess(parent) , m_resultsFile(resultsFile) { #if QT_VERSION < 0x050600 connect(this, static_cast(&QProcess::error), #else connect(this, &QProcess::errorOccurred, #endif this, [this](QProcess::ProcessError error) { QString errorMessage; if (error == QProcess::FailedToStart) { - errorMessage += i18n("Failed to start visualizer from \"%1\".", program()); - errorMessage += QStringLiteral("\n\n"); - errorMessage += i18n("Check your settings and install the visualizer if necessary."); + errorMessage = i18n("Failed to start visualizer from \"%1\".", program()) + + QStringLiteral("\n\n") + + i18n("Check your settings and install the visualizer if necessary."); } else { - errorMessage += i18n("Error during visualizer execution:"); - errorMessage += QStringLiteral("\n\n"); - errorMessage += errorString(); + errorMessage = i18n("Error during visualizer execution:") + + QStringLiteral("\n\n") + + errorString(); } KMessageBox::error(activeMainWindow(), errorMessage, i18n("Heaptrack Error")); }); connect(this, static_cast(&QProcess::finished), this, [this]() { deleteLater(); }); setProgram(KDevelop::Path(GlobalSettings::heaptrackGuiExecutable()).toLocalFile()); setArguments({ resultsFile }); } Visualizer::~Visualizer() { QFile::remove(m_resultsFile); } } diff --git a/plugins/lldb/debugsession.cpp b/plugins/lldb/debugsession.cpp index 5797840e9a..693becc9b2 100644 --- a/plugins/lldb/debugsession.cpp +++ b/plugins/lldb/debugsession.cpp @@ -1,494 +1,495 @@ /* * LLDB Debugger Support * Copyright 2016 Aetf * * 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 "debugsession.h" #include "controllers/variable.h" #include "dbgglobal.h" #include "debuggerplugin.h" #include "debuglog.h" #include "lldbcommand.h" #include "mi/micommand.h" #include "stty.h" #include "stringhelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevMI::LLDB; using namespace KDevMI::MI; using namespace KDevMI; using namespace KDevelop; struct ExecRunHandler : public MICommandHandler { explicit ExecRunHandler(DebugSession *session, int maxRetry = 5) : m_session(session) , m_remainRetry(maxRetry) , m_activeCommands(1) { } void handle(const ResultRecord& r) override { --m_activeCommands; if (r.reason == QLatin1String("error")) { if (r.hasField(QStringLiteral("msg")) && r[QStringLiteral("msg")].literal().contains(QLatin1String("Invalid process during debug session"))) { // for some unknown reason, lldb-mi sometimes fails to start process if (m_remainRetry && m_session) { qCDebug(DEBUGGERLLDB) << "Retry starting"; --m_remainRetry; // resend the command again. ++m_activeCommands; m_session->addCommand(ExecRun, QString(), this, // use *this as handler, so we can track error times CmdMaybeStartsRunning | CmdHandlesError); return; } } qCDebug(DEBUGGERLLDB) << "Failed to start inferior:" << "exceeded retry times or session become invalid"; m_session->stopDebugger(); } if (m_activeCommands == 0) delete this; } bool handlesError() override { return true; } bool autoDelete() override { return false; } QPointer m_session; int m_remainRetry; int m_activeCommands; }; DebugSession::DebugSession(LldbDebuggerPlugin *plugin) : MIDebugSession(plugin) , m_formatterPath() { m_breakpointController = new BreakpointController(this); m_variableController = new VariableController(this); m_frameStackModel = new LldbFrameStackModel(this); if (m_plugin) m_plugin->setupToolViews(); connect(this, &DebugSession::stateChanged, this, &DebugSession::handleSessionStateChange); } DebugSession::~DebugSession() { if (m_plugin) m_plugin->unloadToolViews(); } BreakpointController *DebugSession::breakpointController() const { return m_breakpointController; } VariableController *DebugSession::variableController() const { return m_variableController; } LldbFrameStackModel *DebugSession::frameStackModel() const { return m_frameStackModel; } LldbDebugger *DebugSession::createDebugger() const { return new LldbDebugger; } MICommand *DebugSession::createCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) const { return new LldbCommand(type, arguments, flags); } MICommand *DebugSession::createUserCommand(const QString& cmd) const { if (m_hasCorrectCLIOutput) return MIDebugSession::createUserCommand(cmd); auto msg = i18n("Attempting to execute user command on unsupported LLDB version"); emit debuggerInternalOutput(msg); qCDebug(DEBUGGERLLDB) << "Attempting user command on unsupported LLDB version"; return nullptr; } void DebugSession::setFormatterPath(const QString &path) { m_formatterPath = path; } void DebugSession::initializeDebugger() { //addCommand(MI::EnableTimings, "yes"); // Check version addCommand(new CliCommand(MI::NonMI, QStringLiteral("version"), this, &DebugSession::handleVersion)); // load data formatter auto formatterPath = m_formatterPath; if (!QFileInfo(formatterPath).isFile()) { formatterPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevlldb/formatters/all.py")); } if (!formatterPath.isEmpty()) { addCommand(MI::NonMI, "command script import " + KShell::quoteArg(formatterPath)); } // Treat char array as string addCommand(MI::GdbSet, QStringLiteral("print char-array-as-string on")); // set a larger term width. // TODO: set term-width to exact max column count in console view addCommand(MI::NonMI, QStringLiteral("settings set term-width 1024")); qCDebug(DEBUGGERLLDB) << "Initialized LLDB"; } void DebugSession::configInferior(ILaunchConfiguration *cfg, IExecutePlugin *iexec, const QString &executable) { // Read Configuration values KConfigGroup grp = cfg->config(); // Create target as early as possible, so we can do target specific configuration later QString filesymbols = Utils::quote(executable); bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); if (remoteDebugging) { auto connStr = grp.readEntry(Config::LldbRemoteServerEntry, QString()); auto remoteDir = grp.readEntry(Config::LldbRemotePathEntry, QString()); auto remoteExe = QDir(remoteDir).filePath(QFileInfo(executable).fileName()); filesymbols += " -r " + Utils::quote(remoteExe); addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); addCommand(MI::TargetSelect, "remote " + connStr, this, &DebugSession::handleTargetSelect, CmdHandlesError); // ensure executable is on remote end addCommand(MI::NonMI, QStringLiteral("platform mkdir -v 755 %0").arg(Utils::quote(remoteDir))); addCommand(MI::NonMI, QStringLiteral("platform put-file %0 %1") .arg(Utils::quote(executable), Utils::quote(remoteExe))); } else { addCommand(MI::FileExecAndSymbols, filesymbols, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); } raiseEvent(connected_to_program); // Set the environment variables has effect only after target created const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); QString envProfileName = iexec->environmentProfileName(cfg); if (envProfileName.isEmpty()) { envProfileName = environmentProfiles.defaultProfileName(); } const auto &envVariables = environmentProfiles.variables(envProfileName); if (!envVariables.isEmpty()) { QStringList vars; + vars.reserve(envVariables.size()); for (auto it = envVariables.constBegin(), ite = envVariables.constEnd(); it != ite; ++it) { vars.append(QStringLiteral("%0=%1").arg(it.key(), Utils::quote(it.value()))); } // actually using lldb command 'settings set target.env-vars' which accepts multiple values addCommand(GdbSet, "environment " + vars.join(QLatin1Char(' '))); } // Break on start: can't use "-exec-run --start" because in lldb-mi // the inferior stops without any notification bool breakOnStart = grp.readEntry(KDevMI::Config::BreakOnStartEntry, false); if (breakOnStart) { BreakpointModel* m = ICore::self()->debugController()->breakpointModel(); bool found = false; foreach (Breakpoint *b, m->breakpoints()) { if (b->location() == QLatin1String("main")) { found = true; break; } } if (!found) { m->addCodeBreakpoint(QStringLiteral("main")); } } // Needed so that breakpoint widget has a chance to insert breakpoints. // FIXME: a bit hacky, as we're really not ready for new commands. setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_ready); qCDebug(DEBUGGERLLDB) << "Per inferior configuration done"; } bool DebugSession::execInferior(ILaunchConfiguration *cfg, IExecutePlugin *, const QString &) { qCDebug(DEBUGGERLLDB) << "Executing inferior"; KConfigGroup grp = cfg->config(); // Start inferior bool remoteDebugging = grp.readEntry(Config::LldbRemoteDebuggingEntry, false); QUrl configLldbScript = grp.readEntry(Config::LldbConfigScriptEntry, QUrl()); addCommand(new SentinelCommand([this, remoteDebugging, configLldbScript]() { // setup inferior I/O redirection if (!remoteDebugging) { // FIXME: a hacky way to emulate tty setting on linux. Not sure if this provides all needed // functionalities of a pty. Should make this conditional on other platforms. // no need to quote, settings set takes 'raw' input addCommand(MI::NonMI, QStringLiteral("settings set target.input-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.output-path %0").arg(m_tty->getSlave())); addCommand(MI::NonMI, QStringLiteral("settings set target.error-path %0").arg(m_tty->getSlave())); } else { // what is the expected behavior for using external terminal when doing remote debugging? } // send breakpoints already in our breakpoint model to lldb auto bc = breakpointController(); bc->initSendBreakpoints(); qCDebug(DEBUGGERLLDB) << "Turn on delete duplicate mode"; // turn on delete duplicate breakpoints model, so that breakpoints created by user command in // the script and returned as a =breakpoint-created notification won't get duplicated with the // one already in our model. // we will turn this model off once we first reach a paused state, and from that time on, // the user can create duplicated breakpoints using normal command. bc->setDeleteDuplicateBreakpoints(true); // run custom config script right before we starting the inferior, // so the user has the freedom to change everything. if (configLldbScript.isValid()) { addCommand(MI::NonMI, "command source -s 0 " + KShell::quoteArg(configLldbScript.toLocalFile())); } addCommand(MI::ExecRun, QString(), new ExecRunHandler(this), CmdMaybeStartsRunning | CmdHandlesError); }, CmdMaybeStartsRunning)); return true; } bool DebugSession::loadCoreFile(ILaunchConfiguration *, const QString &debugee, const QString &corefile) { addCommand(MI::FileExecAndSymbols, debugee, this, &DebugSession::handleFileExecAndSymbols, CmdHandlesError); raiseEvent(connected_to_program); addCommand(new CliCommand(NonMI, "target create -c " + Utils::quote(corefile), this, &DebugSession::handleCoreFile, CmdHandlesError)); return true; } void DebugSession::interruptDebugger() { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; addCommand(ExecInterrupt, QString(), CmdInterrupt); return; } void DebugSession::ensureDebuggerListening() { // lldb always uses async mode and prompt is always available. // no need to interrupt setDebuggerStateOff(s_dbgNotListening); // NOTE: there is actually a bug in lldb-mi that, when receiving SIGINT, // it would print '^done', which doesn't coresponds to any previous command. // This confuses our command queue. } void DebugSession::handleFileExecAndSymbols(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not start debugger:
    ")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleTargetSelect(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error(qApp->activeWindow(), i18n("Error connecting to remote target:
    ")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } void DebugSession::handleCoreFile(const QStringList &s) { qCDebug(DEBUGGERLLDB) << s; for (const auto &line : s) { if (line.startsWith(QLatin1String("error:"))) { KMessageBox::error( qApp->activeWindow(), i18n("Failed to load core file" "

    Debugger reported the following error:" "

    %1", s.join('\n')), i18n("Startup error")); stopDebugger(); return; } } // There's no "thread-group-started" notification from lldb-mi, so pretend we have received one. // see MIDebugSession::processNotification(const MI::AsyncRecord & async) setDebuggerStateOff(s_appNotStarted | s_programExited); setDebuggerStateOn(s_programExited | s_core); } void DebugSession::handleVersion(const QStringList& s) { m_hasCorrectCLIOutput = !s.isEmpty(); if (!m_hasCorrectCLIOutput) { // No output from 'version' command. It's likely that // the lldb used is not patched for the CLI output if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } auto ans = KMessageBox::warningYesNo( qApp->activeWindow(), i18n("Your lldb-mi version is unsupported, as it lacks an essential patch.
    " "See https://llvm.org/bugs/show_bug.cgi?id=28026 for more information.
    " "Debugger console will be disabled to prevent crash.
    " "Do you want to continue?"), i18n("LLDB Version Unsupported"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("unsupported-lldb-debugger")); if (ans == KMessageBox::ButtonCode::No) { programFinished(QStringLiteral("Stopped because of unsupported LLDB version")); stopDebugger(); } return; } qCDebug(DEBUGGERLLDB) << s.first(); // minimal version is 3.8.1 #ifdef Q_OS_OSX QRegularExpression rx("^lldb-(\\d+).(\\d+).(\\d+)\\b", QRegularExpression::MultilineOption); // lldb 3.8.1 reports version 350.99.0 on OS X const int min_ver[] = {350, 99, 0}; #else QRegularExpression rx(QStringLiteral("^lldb version (\\d+).(\\d+).(\\d+)\\b"), QRegularExpression::MultilineOption); const int min_ver[] = {3, 8, 1}; #endif auto match = rx.match(s.first()); int version[] = {0, 0, 0}; if (match.hasMatch()) { for (int i = 0; i != 3; ++i) { version[i] = match.captured(i+1).toInt(); } } bool ok = true; for (int i = 0; i < 3; ++i) { if (version[i] < min_ver[i]) { ok = false; break; } else if (version[i] > min_ver[i]) { ok = true; break; } } if (!ok) { if (!qobject_cast(qApp)) { //for unittest qFatal("You need a graphical application."); } KMessageBox::error( qApp->activeWindow(), i18n("You need lldb-mi from LLDB 3.8.1 or higher.
    " "You are using: %1", s.first()), i18n("LLDB Error")); stopDebugger(); } } void DebugSession::updateAllVariables() { // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist // for variables that have a python synthetic provider. Remove this after this is fixed // in the upstream. // re-fetch all toplevel variables, as -var-update doesn't work with data formatter // we have to pick out top level variables first, as refetching will delete child // variables. QList toplevels; for (auto it = m_allVariables.begin(), ite = m_allVariables.end(); it != ite; ++it) { LldbVariable *var = qobject_cast(it.value()); if (var->topLevel()) { toplevels << var; } } for (auto var : toplevels) { var->refetch(); } } void DebugSession::handleSessionStateChange(IDebugSession::DebuggerState state) { if (state == IDebugSession::PausedState) { // session is paused, the user can input any commands now. // Turn off delete duplicate breakpoints mode, as the user // may intentionaly want to do this. qCDebug(DEBUGGERLLDB) << "Turn off delete duplicate mode"; breakpointController()->setDeleteDuplicateBreakpoints(false); } } diff --git a/plugins/makebuilder/makejob.cpp b/plugins/makebuilder/makejob.cpp index 901311ee0d..82d01a6fa8 100644 --- a/plugins/makebuilder/makejob.cpp +++ b/plugins/makebuilder/makejob.cpp @@ -1,313 +1,313 @@ /* This file is part of KDevelop Copyright 2004 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2007 Dukju Ahn Copyright 2008 Hamish Rodda Copyright 2012 Ivan Shapovalov This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "makejob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "makebuilder.h" #include "makebuilderpreferences.h" #include "debug.h" using namespace KDevelop; class MakeJobCompilerFilterStrategy : public CompilerFilterStrategy { public: using CompilerFilterStrategy::CompilerFilterStrategy; IFilterStrategy::Progress progressInLine(const QString& line) override; }; IFilterStrategy::Progress MakeJobCompilerFilterStrategy::progressInLine(const QString& line) { // example string: [ 97%] Built target clang-parser static const QRegularExpression re(QStringLiteral("^\\[([\\d ][\\d ]\\d)%\\] (.*)")); QRegularExpressionMatch match = re.match(line); if (match.hasMatch()) { bool ok; const int percent = match.capturedRef(1).toInt(&ok); if (ok) { // this is output from make, likely const QString action = match.captured(2); return {action, percent}; } } return {}; } MakeJob::MakeJob(QObject* parent, KDevelop::ProjectBaseItem* item, CommandType c, const QStringList& overrideTargets, const MakeVariables& variables ) : OutputExecuteJob(parent) , m_idx(item->index()) , m_command(c) , m_overrideTargets(overrideTargets) , m_variables(variables) { auto bsm = item->project()->buildSystemManager(); auto buildDir = bsm->buildDirectory(item); Q_ASSERT(item && item->model() && m_idx.isValid() && this->item() == item); setCapabilities( Killable ); setFilteringStrategy(new MakeJobCompilerFilterStrategy(buildDir.toUrl())); setProperties( NeedWorkingDirectory | PortableMessages | DisplayStderr | IsBuilderHint ); QString title; if( !m_overrideTargets.isEmpty() ) title = i18n("Make (%1): %2", item->text(), m_overrideTargets.join(QLatin1Char(' '))); else title = i18n("Make (%1)", item->text()); setJobName( title ); setToolTitle( i18n("Make") ); } MakeJob::~MakeJob() { } void MakeJob::start() { ProjectBaseItem* it = item(); qCDebug(KDEV_MAKEBUILDER) << "Building with make" << m_command << m_overrideTargets.join(QLatin1Char(' ')); if (!it) { setError(ItemNoLongerValidError); setErrorText(i18n("Build item no longer available")); return emitResult(); } if( it->type() == KDevelop::ProjectBaseItem::File ) { setError(IncorrectItemError); setErrorText(i18n("Internal error: cannot build a file item")); return emitResult(); } setStandardToolView(IOutputView::BuildView); setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll); OutputExecuteJob::start(); } KDevelop::ProjectBaseItem * MakeJob::item() const { return ICore::self()->projectController()->projectModel()->itemFromIndex(m_idx); } MakeJob::CommandType MakeJob::commandType() const { return m_command; } QStringList MakeJob::customTargets() const { return m_overrideTargets; } QUrl MakeJob::workingDirectory() const { ProjectBaseItem* it = item(); if(!it) return QUrl(); KDevelop::IBuildSystemManager *bldMan = it->project()->buildSystemManager(); if( bldMan ) return bldMan->buildDirectory( it ).toUrl(); // the correct build dir else { // Just build in-source, where the build directory equals the one with particular target/source. for( ProjectBaseItem* item = it; item; item = item->parent() ) { switch( item->type() ) { case KDevelop::ProjectBaseItem::Folder: case KDevelop::ProjectBaseItem::BuildFolder: return static_cast(item)->path().toUrl(); break; case KDevelop::ProjectBaseItem::Target: case KDevelop::ProjectBaseItem::File: break; } } return QUrl(); } } QStringList MakeJob::privilegedExecutionCommand() const { ProjectBaseItem* it = item(); if(!it) return QStringList(); KSharedConfigPtr configPtr = it->project()->projectConfiguration(); KConfigGroup builderGroup( configPtr, "MakeBuilder" ); bool runAsRoot = builderGroup.readEntry( "Install As Root", false ); if ( runAsRoot && m_command == InstallCommand ) { QString suCommand = builderGroup.readEntry( "Su Command", QString() ); bool suCommandIsDigit; QStringList suCommandWithArg; int suCommandNum = suCommand.toInt(&suCommandIsDigit); /* * "if(suCommandIsDigit)" block exists only because of backwards compatibility * reasons, In earlier versions of KDevelop, suCommand's type was * int, if a user upgrades to current version from an older version, * suCommandIsDigit will become "true" and we will set suCommand according * to the stored config entry. */ if(suCommandIsDigit) { switch(suCommandNum) { case 1: { suCommand = QStringLiteral("kdesudo"); break; } case 2: { suCommand = QStringLiteral("sudo"); break; } default: suCommand = QStringLiteral("kdesu"); } builderGroup.writeEntry("Su Command", suCommand); //so that suCommandIsDigit becomes false next time //project is opened. } suCommandWithArg = KShell::splitArgs(suCommand); if( suCommandWithArg.isEmpty() ) { - suCommandWithArg << QStringLiteral("kdesu") << QStringLiteral("-t"); + suCommandWithArg = QStringList{QStringLiteral("kdesu"), QStringLiteral("-t")}; } return suCommandWithArg; } return QStringList(); } QStringList MakeJob::commandLine() const { ProjectBaseItem* it = item(); if(!it) return QStringList(); QStringList cmdline; KSharedConfigPtr configPtr = it->project()->projectConfiguration(); KConfigGroup builderGroup( configPtr, "MakeBuilder" ); // TODO: migrate to more generic key term "Make Executable" QString makeBin = builderGroup.readEntry("Make Binary", MakeBuilderPreferences::standardMakeExecutable()); cmdline << makeBin; if( ! builderGroup.readEntry("Abort on First Error", true)) { cmdline << (isNMake(makeBin) ? "/K" : "-k"); } // note: nmake does not support the -j flag if (!isNMake(makeBin)) { if (builderGroup.readEntry("Override Number Of Jobs", false)) { int jobCount = builderGroup.readEntry("Number Of Jobs", 1); if (jobCount > 0) { cmdline << QStringLiteral("-j%1").arg(jobCount); } } else { // use the ideal thread count by default cmdline << QStringLiteral("-j%1").arg(QThread::idealThreadCount()); } } if( builderGroup.readEntry("Display Only", false) ) { cmdline << (isNMake(makeBin) ? "/N" : "-n"); } QString extraOptions = builderGroup.readEntry("Additional Options", QString()); if( ! extraOptions.isEmpty() ) { foreach(const QString& option, KShell::splitArgs( extraOptions ) ) cmdline << option; } for (MakeVariables::const_iterator it = m_variables.constBegin(); it != m_variables.constEnd(); ++it) { cmdline += QStringLiteral("%1=%2").arg(it->first).arg(it->second); } if( m_overrideTargets.isEmpty() ) { QString target; switch (it->type()) { case KDevelop::ProjectBaseItem::Target: case KDevelop::ProjectBaseItem::ExecutableTarget: case KDevelop::ProjectBaseItem::LibraryTarget: Q_ASSERT(it->target()); cmdline << it->target()->text(); break; case KDevelop::ProjectBaseItem::BuildFolder: target = builderGroup.readEntry("Default Target", QString()); if( !target.isEmpty() ) cmdline << target; break; default: break; } }else { cmdline += m_overrideTargets; } return cmdline; } QString MakeJob::environmentProfile() const { ProjectBaseItem* it = item(); if(!it) return QString(); KSharedConfigPtr configPtr = it->project()->projectConfiguration(); KConfigGroup builderGroup( configPtr, "MakeBuilder" ); return builderGroup.readEntry( "Default Make Environment Profile", QString() ); } bool MakeJob::isNMake(const QString& makeBin) { return !QFileInfo(makeBin).baseName().compare(QStringLiteral("nmake"), Qt::CaseInsensitive); } diff --git a/plugins/ninjabuilder/ninjabuilder.cpp b/plugins/ninjabuilder/ninjabuilder.cpp index 3604eac1ae..99fddbb39c 100644 --- a/plugins/ninjabuilder/ninjabuilder.cpp +++ b/plugins/ninjabuilder/ninjabuilder.cpp @@ -1,207 +1,209 @@ /* This file is part of KDevelop Copyright 2012 Aleix Pol Gonzalez Copyright 2017 Kevin Funk 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 "ninjabuilder.h" #include "ninjajob.h" #include "ninjabuilderpreferences.h" #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(NinjaBuilderFactory, "kdevninja.json", registerPlugin(); ) NinjaBuilder::NinjaBuilder(QObject* parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevninja"), parent) { if (NinjaJob::ninjaExecutable().isEmpty()) { setErrorDescription(i18n("Unable to find ninja executable. Is it installed on the system?")); } } static QStringList targetsInFolder(KDevelop::ProjectFolderItem* item) { QStringList ret; - foreach (KDevelop::ProjectTargetItem* target, item->targetList()) { + const auto targets = item->targetList(); + ret.reserve(targets.size()); + for (auto* target : targets) { ret += target->text(); } return ret; } /** * Returns the first non-empty list of targets in folder @p item * or any of its ancestors if possible */ static QStringList closestTargetsForFolder(KDevelop::ProjectFolderItem* item) { KDevelop::ProjectFolderItem* current = item; while (current) { const QStringList targets = targetsInFolder(current); if (!targets.isEmpty()) { return targets; } current = (current->parent() ? current->parent()->folder() : nullptr); } return QStringList(); } static QStringList argumentsForItem(KDevelop::ProjectBaseItem* item) { if (!item->parent() && QFile::exists(item->project()->buildSystemManager()->buildDirectory(item->project()->projectItem()).toLocalFile())) { return QStringList(); } switch (item->type()) { case KDevelop::ProjectBaseItem::File: return QStringList(item->path().toLocalFile() + '^'); case KDevelop::ProjectBaseItem::Target: case KDevelop::ProjectBaseItem::ExecutableTarget: case KDevelop::ProjectBaseItem::LibraryTarget: return QStringList(item->target()->text()); case KDevelop::ProjectBaseItem::Folder: case KDevelop::ProjectBaseItem::BuildFolder: return closestTargetsForFolder(item->folder()); } return QStringList(); } NinjaJob* NinjaBuilder::runNinja(KDevelop::ProjectBaseItem* item, NinjaJob::CommandType commandType, const QStringList& args, const QByteArray& signal) { ///Running the same builder twice may result in serious problems, ///so kill jobs already running on the same project foreach (NinjaJob* ninjaJob, m_activeNinjaJobs.data()) { if (item && ninjaJob->item() && ninjaJob->item()->project() == item->project() && ninjaJob->commandType() == commandType) { qCDebug(NINJABUILDER) << "killing running ninja job, due to new started build on same project:" << ninjaJob; ninjaJob->kill(KJob::EmitResult); } } // Build arguments using data from KCM QStringList jobArguments; KSharedConfigPtr config = item->project()->projectConfiguration(); KConfigGroup group = config->group("NinjaBuilder"); if (!group.readEntry("Abort on First Error", true)) { jobArguments << QStringLiteral("-k"); } if (group.readEntry("Override Number Of Jobs", false)) { int jobCount = group.readEntry("Number Of Jobs", 1); if (jobCount > 0) { jobArguments << QStringLiteral("-j%1").arg(jobCount); } } int errorCount = group.readEntry("Number Of Errors", 1); if (errorCount > 1) { jobArguments << QStringLiteral("-k%1").arg(errorCount); } if (group.readEntry("Display Only", false)) { jobArguments << QStringLiteral("-n"); } QString extraOptions = group.readEntry("Additional Options", QString()); if (!extraOptions.isEmpty()) { foreach (const QString& option, KShell::splitArgs(extraOptions)) { jobArguments << option; } } jobArguments << args; NinjaJob* job = new NinjaJob(item, commandType, jobArguments, signal, this); m_activeNinjaJobs.append(job); return job; } KJob* NinjaBuilder::build(KDevelop::ProjectBaseItem* item) { return runNinja(item, NinjaJob::BuildCommand, argumentsForItem(item), "built"); } KJob* NinjaBuilder::clean(KDevelop::ProjectBaseItem* item) { return runNinja(item, NinjaJob::CleanCommand, QStringList(QStringLiteral("-t")) << QStringLiteral("clean"), "cleaned"); } KJob* NinjaBuilder::install(KDevelop::ProjectBaseItem* item) { NinjaJob* installJob = runNinja(item, NinjaJob::InstallCommand, QStringList(QStringLiteral("install")), "installed"); installJob->setIsInstalling(true); KSharedConfigPtr configPtr = item->project()->projectConfiguration(); KConfigGroup builderGroup(configPtr, "NinjaBuilder"); bool installAsRoot = builderGroup.readEntry("Install As Root", false); if (installAsRoot) { KDevelop::BuilderJob* job = new KDevelop::BuilderJob; job->addCustomJob(KDevelop::BuilderJob::Build, build(item), item); job->addCustomJob(KDevelop::BuilderJob::Install, installJob, item); job->updateJobName(); return job; } else { return installJob; } } int NinjaBuilder::perProjectConfigPages() const { return 1; } KDevelop::ConfigPage* NinjaBuilder::perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) { if (number == 0) { return new NinjaBuilderPreferences(this, options, parent); } return nullptr; } class ErrorJob : public KJob { public: ErrorJob(QObject* parent, const QString& error) : KJob(parent) , m_error(error) {} void start() override { setError(!m_error.isEmpty()); setErrorText(m_error); emitResult(); } private: QString m_error; }; KJob* NinjaBuilder::install(KDevelop::ProjectBaseItem* dom, const QUrl& installPath) { return installPath.isEmpty() ? install(dom) : new ErrorJob(nullptr, i18n("Cannot specify prefix in %1, on ninja", installPath.toDisplayString())); } #include "ninjabuilder.moc" diff --git a/plugins/ninjabuilder/ninjajob.cpp b/plugins/ninjabuilder/ninjajob.cpp index ceedc42f21..eaf8c9fdd5 100644 --- a/plugins/ninjabuilder/ninjajob.cpp +++ b/plugins/ninjabuilder/ninjajob.cpp @@ -1,239 +1,239 @@ /* This file is part of KDevelop Copyright 2012 Aleix Pol Gonzalez Copyright 2017 Kevin Funk 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 "ninjajob.h" #include "ninjabuilder.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; class NinjaJobCompilerFilterStrategy : public CompilerFilterStrategy { public: using CompilerFilterStrategy::CompilerFilterStrategy; IFilterStrategy::Progress progressInLine(const QString& line) override; }; IFilterStrategy::Progress NinjaJobCompilerFilterStrategy::progressInLine(const QString& line) { // example string: [87/88] Building CXX object projectbuilders/ninjabuilder/CMakeFiles/kdevninja.dir/ninjajob.cpp.o static const QRegularExpression re(QStringLiteral("^\\[([0-9]+)\\/([0-9]+)\\] (.*)")); QRegularExpressionMatch match = re.match(line); if (match.hasMatch()) { const int current = match.capturedRef(1).toInt(); const int total = match.capturedRef(2).toInt(); if (current && total) { // this is output from ninja const QString action = match.captured(3); const int percent = qRound(( float )current / total * 100); return { action, percent }; } } return {}; } NinjaJob::NinjaJob(KDevelop::ProjectBaseItem* item, CommandType commandType, const QStringList& arguments, const QByteArray& signal, NinjaBuilder* parent) : OutputExecuteJob(parent) , m_isInstalling(false) , m_idx(item->index()) , m_commandType(commandType) , m_signal(signal) , m_plugin(parent) { auto bsm = item->project()->buildSystemManager(); auto buildDir = bsm->buildDirectory(item); setToolTitle(i18n("Ninja")); setCapabilities(Killable); setStandardToolView(KDevelop::IOutputView::BuildView); setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll); setFilteringStrategy(new NinjaJobCompilerFilterStrategy(buildDir.toUrl())); setProperties(NeedWorkingDirectory | PortableMessages | DisplayStderr | IsBuilderHint | PostProcessOutput); // hardcode the ninja output format so we can parse it reliably addEnvironmentOverride(QStringLiteral("NINJA_STATUS"), QStringLiteral("[%s/%t] ")); *this << ninjaExecutable(); *this << arguments; QStringList targets; foreach (const QString& arg, arguments) { if (!arg.startsWith('-')) { targets << arg; } } QString title; if (!targets.isEmpty()) { title = i18n("Ninja (%1): %2", item->text(), targets.join(QLatin1Char(' '))); } else { title = i18n("Ninja (%1)", item->text()); } setJobName(title); connect(this, &NinjaJob::finished, this, &NinjaJob::emitProjectBuilderSignal); } NinjaJob::~NinjaJob() { // prevent crash when emitting KJob::finished from ~KJob // (=> at this point NinjaJob is already destructed...) disconnect(this, &NinjaJob::finished, this, &NinjaJob::emitProjectBuilderSignal); } void NinjaJob::setIsInstalling(bool isInstalling) { m_isInstalling = isInstalling; } QString NinjaJob::ninjaExecutable() { QString path = QStandardPaths::findExecutable(QStringLiteral("ninja-build")); if (path.isEmpty()) { path = QStandardPaths::findExecutable(QStringLiteral("ninja")); } return path; } QUrl NinjaJob::workingDirectory() const { KDevelop::ProjectBaseItem* it = item(); if (!it) { return QUrl(); } KDevelop::IBuildSystemManager* bsm = it->project()->buildSystemManager(); KDevelop::Path workingDir = bsm->buildDirectory(it); while (!QFile::exists(workingDir.toLocalFile() + "build.ninja")) { KDevelop::Path upWorkingDir = workingDir.parent(); if (!upWorkingDir.isValid() || upWorkingDir == workingDir) { return bsm->buildDirectory(it->project()->projectItem()).toUrl(); } workingDir = upWorkingDir; } return workingDir.toUrl(); } QStringList NinjaJob::privilegedExecutionCommand() const { KDevelop::ProjectBaseItem* it = item(); if (!it) { return QStringList(); } KSharedConfigPtr configPtr = it->project()->projectConfiguration(); KConfigGroup builderGroup(configPtr, "NinjaBuilder"); bool runAsRoot = builderGroup.readEntry("Install As Root", false); if (runAsRoot && m_isInstalling) { int suCommand = builderGroup.readEntry("Su Command", 0); QStringList arguments; QString suCommandName; switch (suCommand) { case 1: - return QStringList() << QStringLiteral("kdesudo") << QStringLiteral("-t"); + return QStringList{QStringLiteral("kdesudo"), QStringLiteral("-t")}; case 2: - return QStringList() << QStringLiteral("sudo"); + return QStringList{QStringLiteral("sudo")}; default: - return QStringList() << QStringLiteral("kdesu") << QStringLiteral("-t"); + return QStringList{QStringLiteral("kdesu"), QStringLiteral("-t")}; } } return QStringList(); } void NinjaJob::emitProjectBuilderSignal(KJob* job) { if (!m_plugin) { return; } KDevelop::ProjectBaseItem* it = item(); if (!it) { return; } if (job->error() == 0) { Q_ASSERT(!m_signal.isEmpty()); QMetaObject::invokeMethod(m_plugin, m_signal, Q_ARG(KDevelop::ProjectBaseItem*, it)); } else { QMetaObject::invokeMethod(m_plugin, "failed", Q_ARG(KDevelop::ProjectBaseItem*, it)); } } void NinjaJob::postProcessStderr(const QStringList& lines) { appendLines(lines); } void NinjaJob::postProcessStdout(const QStringList& lines) { appendLines(lines); } void NinjaJob::appendLines(const QStringList& lines) { if (lines.isEmpty()) { return; } QStringList ret(lines); bool prev = false; for (QStringList::iterator it = ret.end(); it != ret.begin(); ) { --it; bool curr = it->startsWith('['); if ((prev && curr) || it->endsWith(QLatin1String("] "))) { it = ret.erase(it); } prev = curr; } model()->appendLines(ret); } KDevelop::ProjectBaseItem* NinjaJob::item() const { return KDevelop::ICore::self()->projectController()->projectModel()->itemFromIndex(m_idx); } NinjaJob::CommandType NinjaJob::commandType() const { return m_commandType; } diff --git a/plugins/okteta/oktetawidget.cpp b/plugins/okteta/oktetawidget.cpp index d32fca66fa..0ad78d1e00 100644 --- a/plugins/okteta/oktetawidget.cpp +++ b/plugins/okteta/oktetawidget.cpp @@ -1,156 +1,158 @@ /* This file is part of the KDevelop Okteta module, part of the KDE project. Copyright 2010 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "oktetawidget.h" // plugin #include "oktetadocument.h" #include "oktetaplugin.h" // Okteta Kasten #include //#include //#include #include #include #include #include #include #include #include #include #include #include #include #include // Kasten #include // #include // #include #include #include #include #include #include #include #include #include // KDevelop #include // KDE #include #include #include #include // Qt #include namespace KDevelop { OktetaWidget::OktetaWidget( QWidget* parent, Kasten::ByteArrayView* byteArrayView, OktetaPlugin* plugin ) : QWidget( parent ), KXMLGUIClient(), mByteArrayView( byteArrayView ) { setComponentName( QStringLiteral("kdevokteta") , QStringLiteral("KDevelop Okteta")); setXMLFile( QStringLiteral("kdevokteta.rc") ); setupActions(plugin); QVBoxLayout* layout = new QVBoxLayout( this ); layout->setMargin( 0 ); QWidget* widget = mByteArrayView->widget(); layout->addWidget( widget ); setFocusProxy( widget ); } void OktetaWidget::setupActions(OktetaPlugin* plugin) { - mControllers.append( new Kasten::VersionController(this) ); - mControllers.append( new Kasten::ReadOnlyController(this) ); + Kasten::ByteArrayViewProfileManager* viewProfileManager = plugin->viewProfileManager(); + mControllers = { + new Kasten::VersionController(this), + new Kasten::ReadOnlyController(this), // TODO: save_as // mControllers.append( new ExportController(mProgram->viewManager(),mProgram->documentManager(),this) ); - mControllers.append( new Kasten::ZoomController(this) ); - mControllers.append( new Kasten::SelectController(this) ); - mControllers.append( new Kasten::ClipboardController(this) ); + new Kasten::ZoomController(this), + new Kasten::SelectController(this), + new Kasten::ClipboardController(this), // if( modus != BrowserViewModus ) // mControllers.append( new Kasten::InsertController(mProgram->viewManager(),mProgram->documentManager(),this) ); // mControllers.append( new Kasten::CopyAsController(mProgram->viewManager(),mProgram->documentManager(),this) ); - mControllers.append( new Kasten::OverwriteModeController(this) ); - mControllers.append( new Kasten::SearchController(this,this) ); - mControllers.append( new Kasten::ReplaceController(this,this) ); + new Kasten::OverwriteModeController(this), + new Kasten::SearchController(this,this), + new Kasten::ReplaceController(this,this), // mControllers.append( new Kasten::GotoOffsetController(mGroupedViews,this) ); // mControllers.append( new Kasten::SelectRangeController(mGroupedViews,this) ); - mControllers.append( new Kasten::BookmarksController(this) ); - mControllers.append( new Kasten::PrintController( this ) ); - mControllers.append( new Kasten::ViewConfigController(this) ); - mControllers.append( new Kasten::ViewModeController(this) ); - Kasten::ByteArrayViewProfileManager* viewProfileManager = plugin->viewProfileManager(); - mControllers.append( new Kasten::ViewProfileController(viewProfileManager, mByteArrayView->widget(), this) ); - mControllers.append( new Kasten::ViewProfilesManageController(this, viewProfileManager, mByteArrayView->widget()) ); + new Kasten::BookmarksController(this), + new Kasten::PrintController(this), + new Kasten::ViewConfigController(this), + new Kasten::ViewModeController(this), + new Kasten::ViewProfileController(viewProfileManager, mByteArrayView->widget(), this), + new Kasten::ViewProfilesManageController(this, viewProfileManager, mByteArrayView->widget()), + }; // update the text of the viewprofiles_manage action, to make clear this is just for byte arrays QAction* viewprofilesManageAction = actionCollection()->action(QStringLiteral("settings_viewprofiles_manage")); viewprofilesManageAction->setText( i18nc("@action:inmenu", "Manage Byte Array View Profiles...") ); // Kasten::StatusBar* bottomBar = static_cast( statusBar() ); // mControllers.append( new ViewStatusController(bottomBar) ); // mControllers.append( new ReadOnlyBarController(bottomBar) ); // mControllers.append( new ZoomBarController(bottomBar) ); foreach( Kasten::AbstractXmlGuiController* controller, mControllers ) controller->setTargetModel( mByteArrayView ); #if 0 QDesignerFormWindowManagerInterface* manager = mDocument->form()->core()->formWindowManager(); KActionCollection* ac = actionCollection(); KStandardAction::save( this, SLOT(save()), ac); ac->addAction( "adjust_size", manager->actionAdjustSize() ); ac->addAction( "break_layout", manager->actionBreakLayout() ); ac->addAction( "designer_cut", manager->actionCut() ); ac->addAction( "designer_copy", manager->actionCopy() ); ac->addAction( "designer_paste", manager->actionPaste() ); ac->addAction( "designer_delete", manager->actionDelete() ); ac->addAction( "layout_grid", manager->actionGridLayout() ); ac->addAction( "layout_horiz", manager->actionHorizontalLayout() ); ac->addAction( "layout_vertical", manager->actionVerticalLayout() ); ac->addAction( "layout_split_horiz", manager->actionSplitHorizontal() ); ac->addAction( "layout_split_vert", manager->actionSplitVertical() ); ac->addAction( "designer_undo", manager->actionUndo() ); ac->addAction( "designer_redo", manager->actionRedo() ); ac->addAction( "designer_select_all", manager->actionSelectAll() ); #endif } #if 0 void OktetaWidget::save() { mDocument->save(); } #endif OktetaWidget::~OktetaWidget() { qDeleteAll( mControllers ); } } diff --git a/plugins/patchreview/patchhighlighter.cpp b/plugins/patchreview/patchhighlighter.cpp index d24c4bba79..65b817dae9 100644 --- a/plugins/patchreview/patchhighlighter.cpp +++ b/plugins/patchreview/patchhighlighter.cpp @@ -1,692 +1,691 @@ /*************************************************************************** Copyright 2006 David Nolden ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "patchhighlighter.h" #include #include #include "patchreview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QPointer currentTooltip; KTextEditor::MovingRange* currentTooltipMark; QSize sizeHintForHtml( const QString& html, QSize maxSize ) { QTextDocument doc; doc.setHtml( html ); QSize ret; if( doc.idealWidth() > maxSize.width() ) { doc.setPageSize( QSize( maxSize.width(), 30 ) ); ret.setWidth( maxSize.width() ); }else{ ret.setWidth( doc.idealWidth() ); } ret.setHeight( doc.size().height() ); if( ret.height() > maxSize.height() ) ret.setHeight( maxSize.height() ); return ret; } } const unsigned int PatchHighlighter::m_allmarks = KTextEditor::MarkInterface::markType22 | KTextEditor::MarkInterface::markType23 | KTextEditor::MarkInterface::markType24 | KTextEditor::MarkInterface::markType25 | KTextEditor::MarkInterface::markType26 | KTextEditor::MarkInterface::markType27; void PatchHighlighter::showToolTipForMark(const QPoint& pos, KTextEditor::MovingRange* markRange) { if( currentTooltipMark == markRange && currentTooltip ) return; delete currentTooltip; //Got the difference Diff2::Difference* diff = m_ranges[markRange]; QString html; #if 0 if( diff->hasConflict() ) html += i18n( "Conflict
    " ); #endif Diff2::DifferenceStringList lines; html += QLatin1String(""); if( diff->applied() ) { if( !m_plugin->patch()->isAlreadyApplied() ) html += i18n( "Applied.
    " ); if( isInsertion( diff ) ) { html += i18n( "Insertion
    " ); } else { if( isRemoval( diff ) ) html += i18n( "Removal
    " ); html += i18n( "Previous:
    " ); lines = diff->sourceLines(); } } else { if( m_plugin->patch()->isAlreadyApplied() ) html += i18n( "Reverted.
    " ); if( isRemoval( diff ) ) { html += i18n( "Removal
    " ); } else { if( isInsertion( diff ) ) html += i18n( "Insertion
    " ); html += i18n( "Alternative:
    " ); lines = diff->destinationLines(); } } html += QLatin1String("
    "); for( int a = 0; a < lines.size(); ++a ) { Diff2::DifferenceString* line = lines[a]; uint currentPos = 0; QString string = line->string(); Diff2::MarkerList markers = line->markerList(); for( int b = 0; b < markers.size(); ++b ) { QString spanText = string.mid( currentPos, markers[b]->offset() - currentPos ).toHtmlEscaped(); if( markers[b]->type() == Diff2::Marker::End && ( currentPos != 0 || markers[b]->offset() != static_cast( string.size() ) ) ) { html += "" + spanText + ""; }else{ html += spanText; } currentPos = markers[b]->offset(); } - html += string.mid( currentPos, string.length()-currentPos ).toHtmlEscaped(); - html += QLatin1String("
    "); + html += string.mid(currentPos, string.length()-currentPos).toHtmlEscaped() + QLatin1String("
    "); } auto browser = new QTextBrowser; browser->setPalette( QApplication::palette() ); browser->setHtml( html ); int maxHeight = 500; browser->setMinimumSize( sizeHintForHtml( html, QSize( ( ICore::self()->uiController()->activeMainWindow()->width()*2 )/3, maxHeight ) ) ); browser->setMaximumSize( browser->minimumSize() + QSize( 10, 10 ) ); if( browser->minimumHeight() != maxHeight ) browser->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); QVBoxLayout* layout = new QVBoxLayout; layout->setMargin( 0 ); layout->addWidget( browser ); KDevelop::ActiveToolTip* tooltip = new KDevelop::ActiveToolTip( ICore::self()->uiController()->activeMainWindow(), pos + QPoint( 5, -browser->sizeHint().height() - 30 ) ); tooltip->setLayout( layout ); tooltip->resize( tooltip->sizeHint() + QSize( 10, 10 ) ); tooltip->move( pos - QPoint( 0, 20 + tooltip->height() ) ); tooltip->setHandleRect( QRect( pos - QPoint( 15, 15 ), pos + QPoint( 15, 15 ) ) ); currentTooltip = tooltip; currentTooltipMark = markRange; ActiveToolTip::showToolTip( tooltip ); } void PatchHighlighter::markClicked( KTextEditor::Document* doc, const KTextEditor::Mark& mark, bool& handled ) { if( handled || !(mark.type & m_allmarks) ) return; auto range_diff = rangeForMark(mark); m_applying = true; if (range_diff.first) { handled = true; KTextEditor::MovingRange *&range = range_diff.first; Diff2::Difference *&diff = range_diff.second; QString currentText = doc->text( range->toRange() ); removeLineMarker( range ); QString sourceText; QString targetText; for( int a = 0; a < diff->sourceLineCount(); ++a ) { sourceText += diff->sourceLineAt( a )->string(); if( !sourceText.endsWith( '\n' ) ) sourceText += '\n'; } for( int a = 0; a < diff->destinationLineCount(); ++a ) { targetText += diff->destinationLineAt( a )->string(); if( !targetText.endsWith( '\n' ) ) targetText += '\n'; } bool applied = diff->applied(); QString &replace(applied ? targetText : sourceText); QString &replaceWith(applied ? sourceText : targetText); if( currentText.simplified() != replace.simplified() ) { KMessageBox::error( ICore::self()->uiController()->activeMainWindow(), i18n( "Could not apply the change: Text should be \"%1\", but is \"%2\".", replace, currentText ) ); m_applying = false; return; } diff->apply(!applied); KTextEditor::Cursor start = range->start().toCursor(); range->document()->replaceText( range->toRange(), replaceWith ); uint replaceWithLines = replaceWith.count( '\n' ); KTextEditor::Range newRange( start, KTextEditor::Cursor(start.line() + replaceWithLines, start.column()) ); range->setRange( newRange ); addLineMarker( range, diff ); { // After applying the change, show the tooltip again, mainly to update an old tooltip delete currentTooltip; currentTooltip = nullptr; bool h = false; markToolTipRequested( doc, mark, QCursor::pos(), h ); } } m_applying = false; } QPair PatchHighlighter::rangeForMark( const KTextEditor::Mark &mark ) { if (!m_applying) { for( QMap::const_iterator it = m_ranges.constBegin(); it != m_ranges.constEnd(); ++it ) { if (it.value() && it.key()->start().line() <= mark.line && mark.line <= it.key()->end().line()) { return qMakePair(it.key(), it.value()); } } } return qMakePair(nullptr, nullptr); } void PatchHighlighter::markToolTipRequested( KTextEditor::Document*, const KTextEditor::Mark& mark, QPoint pos, bool& handled ) { if( handled ) return; if( mark.type & m_allmarks ) { //There is a mark in this line. Show the old text. auto range = rangeForMark(mark); if( range.first ) { showToolTipForMark( pos, range.first ); handled = true; } } } bool PatchHighlighter::isInsertion( Diff2::Difference* diff ) { return diff->sourceLineCount() == 0; } bool PatchHighlighter::isRemoval( Diff2::Difference* diff ) { return diff->destinationLineCount() == 0; } void PatchHighlighter::performContentChange( KTextEditor::Document* doc, const QStringList& oldLines, const QStringList& newLines, int editLineNumber ) { QPair, QList > diffChange = m_model->linesChanged( oldLines, newLines, editLineNumber ); QList inserted = diffChange.first; QList removed = diffChange.second; foreach(Diff2::Difference* d, removed) { foreach(Diff2::DifferenceString* s, d->sourceLines()) qCDebug(PLUGIN_PATCHREVIEW) << "removed source" << s->string(); foreach(Diff2::DifferenceString* s, d->destinationLines()) qCDebug(PLUGIN_PATCHREVIEW) << "removed destination" << s->string(); } foreach(Diff2::Difference* d, inserted) { foreach(Diff2::DifferenceString* s, d->sourceLines()) qCDebug(PLUGIN_PATCHREVIEW) << "inserted source" << s->string(); foreach(Diff2::DifferenceString* s, d->destinationLines()) qCDebug(PLUGIN_PATCHREVIEW) << "inserted destination" << s->string(); } // Remove all ranges that are in the same line (the line markers) for (auto it = m_ranges.begin(); it != m_ranges.end();) { if (removed.contains(it.value())) { KTextEditor::MovingRange* r = it.key(); removeLineMarker(r); // is altering m_ranges it = m_ranges.erase(it); delete r; } else { ++it; } } qDeleteAll(removed); KTextEditor::MovingInterface* moving = dynamic_cast( doc ); if ( !moving ) return; foreach( Diff2::Difference* diff, inserted ) { int lineStart = diff->destinationLineNumber(); if ( lineStart > 0 ) { --lineStart; } int lineEnd = diff->destinationLineEnd(); if ( lineEnd > 0 ) { --lineEnd; } KTextEditor::Range newRange( lineStart, 0, lineEnd, 0 ); KTextEditor::MovingRange * r = moving->newMovingRange( newRange ); m_ranges[r] = diff; addLineMarker( r, diff ); } } void PatchHighlighter::textRemoved( KTextEditor::Document* doc, const KTextEditor::Range& range, const QString& oldText ) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "removal range" << range; qCDebug(PLUGIN_PATCHREVIEW) << "removed text" << oldText; KTextEditor::Cursor cursor = range.start(); int startLine = cursor.line(); QStringList removedLines; QStringList remainingLines; if (startLine > 0) { QString above = doc->line(--startLine); removedLines << above; remainingLines << above; } QString changed = doc->line(cursor.line()) + '\n'; removedLines << changed.mid(0, cursor.column()) + oldText + changed.mid(cursor.column()); remainingLines << changed; if (doc->documentRange().end().line() > cursor.line()) { QString below = doc->line(cursor.line() + 1); removedLines << below; remainingLines << below; } performContentChange(doc, removedLines, remainingLines, startLine + 1); } void PatchHighlighter::newlineRemoved(KTextEditor::Document* doc, int line) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "remove newline" << line; KTextEditor::Cursor cursor = m_doc->cursorPosition(); int startLine = line - 1; QStringList removedLines; QStringList remainingLines; if (startLine > 0) { QString above = doc->line(--startLine); removedLines << above; remainingLines << above; } QString changed = doc->line(line - 1); if (cursor.line() == line - 1) { removedLines << changed.mid(0, cursor.column()); removedLines << changed.mid(cursor.column()); } else { removedLines << changed; removedLines << QString(); } remainingLines << changed; if (doc->documentRange().end().line() >= line) { QString below = doc->line(line); removedLines << below; remainingLines << below; } performContentChange(doc, removedLines, remainingLines, startLine + 1); } void PatchHighlighter::documentReloaded(KTextEditor::Document* doc) { qCDebug(PLUGIN_PATCHREVIEW) << "re-doing"; //The document was loaded / reloaded if ( !m_model->differences() ) return; KTextEditor::MovingInterface* moving = dynamic_cast( doc ); if ( !moving ) return; KTextEditor::MarkInterface* markIface = dynamic_cast( doc ); if( !markIface ) return; clear(); KColorScheme scheme( QPalette::Active ); QImage tintedInsertion = QIcon::fromTheme( QStringLiteral("insert-text") ).pixmap( 16, 16 ).toImage(); KIconEffect::colorize( tintedInsertion, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); QImage tintedRemoval = QIcon::fromTheme( QStringLiteral("edit-delete") ).pixmap( 16, 16 ).toImage(); KIconEffect::colorize( tintedRemoval, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); QImage tintedChange = QIcon::fromTheme( QStringLiteral("text-field") ).pixmap( 16, 16 ).toImage(); KIconEffect::colorize( tintedChange, scheme.foreground( KColorScheme::NegativeText ).color(), 1.0 ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType22, i18n( "Insertion" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType22, QPixmap::fromImage( tintedInsertion ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType23, i18n( "Removal" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType23, QPixmap::fromImage( tintedRemoval ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType24, i18n( "Change" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType24, QPixmap::fromImage( tintedChange ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType25, i18n( "Insertion" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType25, QIcon::fromTheme( QStringLiteral("insert-text") ).pixmap( 16, 16 ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType26, i18n( "Removal" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType26, QIcon::fromTheme( QStringLiteral("edit-delete") ).pixmap( 16, 16 ) ); markIface->setMarkDescription( KTextEditor::MarkInterface::markType27, i18n( "Change" ) ); markIface->setMarkPixmap( KTextEditor::MarkInterface::markType27, QIcon::fromTheme( QStringLiteral("text-field") ).pixmap( 16, 16 ) ); for ( Diff2::DifferenceList::const_iterator it = m_model->differences()->constBegin(); it != m_model->differences()->constEnd(); ++it ) { Diff2::Difference* diff = *it; int line, lineCount; Diff2::DifferenceStringList lines; if( diff->applied() ) { line = diff->destinationLineNumber(); lineCount = diff->destinationLineCount(); lines = diff->destinationLines(); } else { line = diff->sourceLineNumber(); lineCount = diff->sourceLineCount(); lines = diff->sourceLines(); } if ( line > 0 ) line -= 1; KTextEditor::Cursor c( line, 0 ); KTextEditor::Cursor endC( line + lineCount, 0 ); if ( doc->lines() <= c.line() ) c.setLine( doc->lines() - 1 ); if ( doc->lines() <= endC.line() ) endC.setLine( doc->lines() ); if ( endC.isValid() && c.isValid() ) { KTextEditor::MovingRange * r = moving->newMovingRange( KTextEditor::Range( c, endC ) ); m_ranges[r] = diff; addLineMarker( r, diff ); } } } void PatchHighlighter::textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text) { if ( m_applying ) { // Do not interfere with patch application return; } int startLine = cursor.line(); int endColumn = cursor.column() + text.length(); qCDebug(PLUGIN_PATCHREVIEW) << "insertion range" << KTextEditor::Range(cursor, KTextEditor::Cursor(startLine, endColumn)); qCDebug(PLUGIN_PATCHREVIEW) << "inserted text" << text; QStringList removedLines; QStringList insertedLines; if (startLine > 0) { QString above = doc->line(--startLine) + '\n'; removedLines << above; insertedLines << above; } QString changed = doc->line(cursor.line()) + '\n'; removedLines << changed.mid(0, cursor.column()) + changed.mid(endColumn); insertedLines << changed; if (doc->documentRange().end().line() > cursor.line()) { QString below = doc->line(cursor.line() + 1) + '\n'; removedLines << below; insertedLines << below; } performContentChange(doc, removedLines, insertedLines, startLine + 1); } void PatchHighlighter::newlineInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor) { if ( m_applying ) { // Do not interfere with patch application return; } qCDebug(PLUGIN_PATCHREVIEW) << "newline range" << KTextEditor::Range(cursor, KTextEditor::Cursor(cursor.line() + 1, 0)); int startLine = cursor.line(); QStringList removedLines; QStringList insertedLines; if (startLine > 0) { QString above = doc->line(--startLine) + '\n'; removedLines << above; insertedLines << above; } insertedLines << QString('\n'); if (doc->documentRange().end().line() > cursor.line()) { QString below = doc->line(cursor.line() + 1) + '\n'; removedLines << below; insertedLines << below; } performContentChange(doc, removedLines, insertedLines, startLine + 1); } PatchHighlighter::PatchHighlighter( Diff2::DiffModel* model, IDocument* kdoc, PatchReviewPlugin* plugin, bool updatePatchFromEdits ) : m_doc( kdoc ), m_plugin( plugin ), m_model( model ), m_applying( false ) { KTextEditor::Document* doc = kdoc->textDocument(); // connect( kdoc, SIGNAL(destroyed(QObject*)), this, SLOT(documentDestroyed()) ); if (updatePatchFromEdits) { connect(doc, &KTextEditor::Document::textInserted, this, &PatchHighlighter::textInserted); connect(doc, &KTextEditor::Document::lineWrapped, this, &PatchHighlighter::newlineInserted); connect(doc, &KTextEditor::Document::textRemoved, this, &PatchHighlighter::textRemoved); connect(doc, &KTextEditor::Document::lineUnwrapped, this, &PatchHighlighter::newlineRemoved); } connect(doc, &KTextEditor::Document::reloaded, this, &PatchHighlighter::documentReloaded); connect(doc, &KTextEditor::Document::destroyed, this, &PatchHighlighter::documentDestroyed); if ( doc->lines() == 0 ) return; if (qobject_cast(doc)) { //can't use new signal/slot syntax here, MarkInterface is not a QObject connect(doc, SIGNAL(markToolTipRequested(KTextEditor::Document*,KTextEditor::Mark,QPoint,bool&)), this, SLOT(markToolTipRequested(KTextEditor::Document*,KTextEditor::Mark,QPoint,bool&))); connect(doc, SIGNAL(markClicked(KTextEditor::Document*,KTextEditor::Mark,bool&)), this, SLOT(markClicked(KTextEditor::Document*,KTextEditor::Mark,bool&))); } if (qobject_cast(doc)) { //can't use new signal/slot syntax here, MovingInterface is not a QObject connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*))); connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*))); } documentReloaded(doc); } void PatchHighlighter::removeLineMarker( KTextEditor::MovingRange* range ) { KTextEditor::MovingInterface* moving = dynamic_cast( range->document() ); if ( !moving ) return; KTextEditor::MarkInterface* markIface = dynamic_cast( range->document() ); if( !markIface ) return; for (int line = range->start().line(); line <= range->end().line(); ++line) { markIface->removeMark(line, m_allmarks); } // Remove all ranges that are in the same line (the line markers) for (auto it = m_ranges.begin(); it != m_ranges.end();) { if (it.key() != range && range->overlaps(it.key()->toRange())) { delete it.key(); it = m_ranges.erase(it); } else { ++it; } } } void PatchHighlighter::addLineMarker( KTextEditor::MovingRange* range, Diff2::Difference* diff ) { KTextEditor::MovingInterface* moving = dynamic_cast( range->document() ); if ( !moving ) return; KTextEditor::MarkInterface* markIface = dynamic_cast( range->document() ); if( !markIface ) return; KTextEditor::Attribute::Ptr t( new KTextEditor::Attribute() ); bool isOriginalState = diff->applied() == m_plugin->patch()->isAlreadyApplied(); if( isOriginalState ) { t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 0, 255, 255 ), 20 ) ) ); }else{ t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 255, 0, 255 ), 20 ) ) ); } range->setAttribute( t ); range->setZDepth( -500 ); KTextEditor::MarkInterface::MarkTypes mark; if( isOriginalState ) { mark = KTextEditor::MarkInterface::markType27; if( isInsertion( diff ) ) mark = KTextEditor::MarkInterface::markType25; if( isRemoval( diff ) ) mark = KTextEditor::MarkInterface::markType26; }else{ mark = KTextEditor::MarkInterface::markType24; if( isInsertion( diff ) ) mark = KTextEditor::MarkInterface::markType22; if( isRemoval( diff ) ) mark = KTextEditor::MarkInterface::markType23; } markIface->addMark( range->start().line(), mark ); Diff2::DifferenceStringList lines; if( diff->applied() ) lines = diff->destinationLines(); else lines = diff->sourceLines(); for( int a = 0; a < lines.size(); ++a ) { Diff2::DifferenceString* line = lines[a]; int currentPos = 0; QString string = line->string(); Diff2::MarkerList markers = line->markerList(); for( int b = 0; b < markers.size(); ++b ) { if( markers[b]->type() == Diff2::Marker::End ) { if( currentPos != 0 || markers[b]->offset() != static_cast( string.size() ) ) { KTextEditor::MovingRange * r2 = moving->newMovingRange( KTextEditor::Range( KTextEditor::Cursor( a + range->start().line(), currentPos ), KTextEditor::Cursor( a + range->start().line(), markers[b]->offset() ) ) ); m_ranges[r2] = nullptr; KTextEditor::Attribute::Ptr t( new KTextEditor::Attribute() ); t->setProperty( QTextFormat::BackgroundBrush, QBrush( ColorCache::self()->blendBackground( QColor( 255, 0, 0 ), 70 ) ) ); r2->setAttribute( t ); r2->setZDepth( -600 ); } } currentPos = markers[b]->offset(); } } } void PatchHighlighter::clear() { if( m_ranges.empty() ) return; KTextEditor::MovingInterface* moving = dynamic_cast( m_doc->textDocument() ); if ( !moving ) return; KTextEditor::MarkInterface* markIface = dynamic_cast( m_doc->textDocument() ); if( !markIface ) return; foreach( int line, markIface->marks().keys() ) { markIface->removeMark( line, m_allmarks ); } // Diff is taking care of its own objects (except removed ones) qDeleteAll( m_ranges.keys() ); m_ranges.clear(); } PatchHighlighter::~PatchHighlighter() { clear(); } IDocument* PatchHighlighter::doc() { return m_doc; } void PatchHighlighter::documentDestroyed() { qCDebug(PLUGIN_PATCHREVIEW) << "document destroyed"; m_ranges.clear(); } void PatchHighlighter::aboutToDeleteMovingInterfaceContent( KTextEditor::Document* ) { qCDebug(PLUGIN_PATCHREVIEW) << "about to delete"; clear(); } QList< KTextEditor::MovingRange* > PatchHighlighter::ranges() const { return m_ranges.keys(); } diff --git a/plugins/patchreview/patchreviewtoolview.cpp b/plugins/patchreview/patchreviewtoolview.cpp index 9c5d8fe897..ba403add23 100644 --- a/plugins/patchreview/patchreviewtoolview.cpp +++ b/plugins/patchreview/patchreviewtoolview.cpp @@ -1,595 +1,596 @@ /*************************************************************************** Copyright 2006-2009 David Nolden ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "patchreviewtoolview.h" #include "localpatchsource.h" #include "patchreview.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_PURPOSE #include #include #endif using namespace KDevelop; class PatchFilesModel : public VcsFileChangesModel { Q_OBJECT public: PatchFilesModel( QObject *parent, bool allowSelection ) : VcsFileChangesModel( parent, allowSelection ) { }; enum ItemRoles { HunksNumberRole = LastItemRole+1 }; public Q_SLOTS: void updateState( const KDevelop::VcsStatusInfo &status, unsigned hunksNum ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( item, hunksNum ); item->setData( QVariant( hunksNum ), HunksNumberRole ); } void updateState( const KDevelop::VcsStatusInfo &status ) { int row = VcsFileChangesModel::updateState( invisibleRootItem(), status ); if ( row == -1 ) return; QStandardItem *item = invisibleRootItem()->child( row, 0 ); setFileInfo( invisibleRootItem()->child( row, 0 ), item->data( HunksNumberRole ).toUInt() ); } private: void setFileInfo( QStandardItem *item, unsigned int hunksNum ) { const auto url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); const QString path = ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain); const QString newText = i18ncp( "%1: number of changed hunks, %2: file name", "%2 (1 hunk)", "%2 (%1 hunks)", hunksNum, path); item->setText( newText ); } }; PatchReviewToolView::PatchReviewToolView( QWidget* parent, PatchReviewPlugin* plugin ) : QWidget( parent ), m_resetCheckedUrls( true ), m_plugin( plugin ) { setWindowIcon(QIcon::fromTheme(QStringLiteral("text-x-patch"), windowIcon())); connect( m_plugin->finishReviewAction(), &QAction::triggered, this, &PatchReviewToolView::finishReview ); connect( plugin, &PatchReviewPlugin::patchChanged, this, &PatchReviewToolView::patchChanged ); connect( plugin, &PatchReviewPlugin::startingNewReview, this, &PatchReviewToolView::startingNewReview ); connect( ICore::self()->documentController(), &IDocumentController::documentActivated, this, &PatchReviewToolView::documentActivated ); Sublime::MainWindow* w = dynamic_cast( ICore::self()->uiController()->activeMainWindow() ); connect(w, &Sublime::MainWindow::areaChanged, m_plugin, &PatchReviewPlugin::areaChanged); showEditDialog(); patchChanged(); } void PatchReviewToolView::resizeEvent(QResizeEvent* ev) { bool vertical = (width() < height()); m_editPatch.buttonsLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.contentLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight); m_editPatch.buttonsSpacer->changeSize(vertical ? 0 : 40, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); QWidget::resizeEvent(ev); if(m_customWidget) { m_editPatch.contentLayout->removeWidget( m_customWidget ); m_editPatch.contentLayout->insertWidget(0, m_customWidget ); } } void PatchReviewToolView::startingNewReview() { m_resetCheckedUrls = true; } void PatchReviewToolView::patchChanged() { fillEditFromPatch(); kompareModelChanged(); #ifdef WITH_PURPOSE IPatchSource::Ptr p = m_plugin->patch(); if (p) { m_exportMenu->model()->setInputData(QJsonObject { { QStringLiteral("urls"), QJsonArray { p->file().toString() } }, { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } }, { QStringLiteral("localBaseDir"), { p->baseDir().toString() } } }); } #endif } PatchReviewToolView::~PatchReviewToolView() { } LocalPatchSource* PatchReviewToolView::GetLocalPatchSource() { IPatchSource::Ptr ips = m_plugin->patch(); if ( !ips ) return nullptr; return dynamic_cast( ips.data() ); } void PatchReviewToolView::fillEditFromPatch() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) return; m_editPatch.cancelReview->setVisible( ipatch->canCancel() ); m_fileModel->setIsCheckbable( m_plugin->patch()->canSelectFiles() ); if( m_customWidget ) { qCDebug(PLUGIN_PATCHREVIEW) << "removing custom widget"; m_customWidget->hide(); m_editPatch.contentLayout->removeWidget( m_customWidget ); } m_customWidget = ipatch->customWidget(); if( m_customWidget ) { m_editPatch.contentLayout->insertWidget( 0, m_customWidget ); m_customWidget->show(); qCDebug(PLUGIN_PATCHREVIEW) << "got custom widget"; } bool showTests = false; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { auto project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project && !ICore::self()->testController()->testSuitesForProject(project).isEmpty()) { showTests = true; break; } } m_editPatch.testsButton->setVisible(showTests); m_editPatch.testProgressBar->hide(); } void PatchReviewToolView::slotAppliedChanged( int newState ) { if ( LocalPatchSource* lpatch = GetLocalPatchSource() ) { lpatch->setAlreadyApplied( newState == Qt::Checked ); m_plugin->notifyPatchChanged(); } } void PatchReviewToolView::showEditDialog() { m_editPatch.setupUi( this ); bool allowSelection = m_plugin->patch() && m_plugin->patch()->canSelectFiles(); m_fileModel = new PatchFilesModel( this, allowSelection ); m_fileSortProxyModel = new VcsFileChangesSortProxyModel(this); m_fileSortProxyModel->setSourceModel(m_fileModel); m_fileSortProxyModel->sort(1); m_fileSortProxyModel->setDynamicSortFilter(true); m_editPatch.filesList->setModel( m_fileSortProxyModel ); m_editPatch.filesList->header()->hide(); m_editPatch.filesList->setRootIsDecorated( false ); m_editPatch.filesList->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_editPatch.filesList, &QTreeView::customContextMenuRequested, this, &PatchReviewToolView::customContextMenuRequested); connect(m_fileModel, &PatchFilesModel::itemChanged, this, &PatchReviewToolView::fileItemChanged); m_editPatch.finishReview->setDefaultAction(m_plugin->finishReviewAction()); #ifdef WITH_PURPOSE m_exportMenu = new Purpose::Menu(this); connect(m_exportMenu, &Purpose::Menu::finished, this, [](const QJsonObject &output, int error, const QString &message) { if (error==0) { KMessageBox::information(nullptr, i18n("You can find the new request at:
    %1
    ", output["url"].toString()), QString(), QString(), KMessageBox::AllowLink); } else { QMessageBox::warning(nullptr, i18n("Error exporting"), i18n("Couldn't export the patch.\n%1", message)); } }); m_exportMenu->model()->setPluginType(QStringLiteral("Export")); m_editPatch.exportReview->setMenu( m_exportMenu ); #else m_editPatch.exportReview->setEnabled(false); #endif connect( m_editPatch.previousHunk, &QToolButton::clicked, this, &PatchReviewToolView::prevHunk ); connect( m_editPatch.nextHunk, &QToolButton::clicked, this, &PatchReviewToolView::nextHunk ); connect( m_editPatch.previousFile, &QToolButton::clicked, this, &PatchReviewToolView::prevFile ); connect( m_editPatch.nextFile, &QToolButton::clicked, this, &PatchReviewToolView::nextFile ); connect( m_editPatch.filesList, &QTreeView::activated , this, &PatchReviewToolView::fileDoubleClicked ); connect( m_editPatch.cancelReview, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::cancelReview ); //connect( m_editPatch.cancelButton, SIGNAL(pressed()), this, SLOT(slotEditCancel()) ); //connect( this, SIGNAL(finished(int)), this, SLOT(slotEditDialogFinished(int)) ); connect( m_editPatch.updateButton, &QPushButton::clicked, m_plugin, &PatchReviewPlugin::forceUpdate ); connect( m_editPatch.testsButton, &QPushButton::clicked, this, &PatchReviewToolView::runTests ); m_selectAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-all")), i18n("Select All"), this ); connect( m_selectAllAction, &QAction::triggered, this, &PatchReviewToolView::selectAll ); m_deselectAllAction = new QAction( i18n("Deselect All"), this ); connect( m_deselectAllAction, &QAction::triggered, this, &PatchReviewToolView::deselectAll ); } void PatchReviewToolView::customContextMenuRequested(const QPoint& pos) { QList urls; QModelIndexList selectionIdxs = m_editPatch.filesList->selectionModel()->selectedIndexes(); + urls.reserve(selectionIdxs.size()); foreach(const QModelIndex& idx, selectionIdxs) { urls += idx.data(KDevelop::VcsFileChangesModel::UrlRole).toUrl(); } QPointer menu = new QMenu(m_editPatch.filesList); QList extensions; if(!urls.isEmpty()) { KDevelop::FileContext context(urls); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); } QList vcsActions; foreach( const ContextMenuExtension& ext, extensions ) { vcsActions += ext.actions(ContextMenuExtension::VcsGroup); } menu->addAction(m_selectAllAction); menu->addAction(m_deselectAllAction); menu->addActions(vcsActions); menu->exec(m_editPatch.filesList->viewport()->mapToGlobal(pos)); delete menu; } void PatchReviewToolView::nextHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( true, current->textDocument()->url() ); } void PatchReviewToolView::prevHunk() { IDocument* current = ICore::self()->documentController()->activeDocument(); if(current && current->textDocument()) m_plugin->seekHunk( false, current->textDocument()->url() ); } void PatchReviewToolView::seekFile(bool forwards) { if(!m_plugin->patch()) return; QList checkedUrls = m_fileModel->checkedUrls(); QList allUrls = m_fileModel->urls(); IDocument* current = ICore::self()->documentController()->activeDocument(); if(!current || checkedUrls.empty()) return; qCDebug(PLUGIN_PATCHREVIEW) << "seeking direction" << forwards; int currentIndex = allUrls.indexOf(current->url()); QUrl newUrl; if((forwards && current->url() == checkedUrls.back()) || (!forwards && current->url() == checkedUrls[0])) { newUrl = m_plugin->patch()->file(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping to patch"; } else if(current->url() == m_plugin->patch()->file() || currentIndex == -1) { if(forwards) newUrl = checkedUrls[0]; else newUrl = checkedUrls.back(); qCDebug(PLUGIN_PATCHREVIEW) << "jumping from patch"; } else { QSet checkedUrlsSet( checkedUrls.toSet() ); for(int offset = 1; offset < allUrls.size(); ++offset) { int pos; if(forwards) { pos = (currentIndex + offset) % allUrls.size(); }else{ pos = currentIndex - offset; if(pos < 0) pos += allUrls.size(); } if(checkedUrlsSet.contains(allUrls[pos])) { newUrl = allUrls[pos]; break; } } } if(newUrl.isValid()) { open( newUrl, true ); }else{ qCDebug(PLUGIN_PATCHREVIEW) << "found no valid target url"; } } void PatchReviewToolView::open( const QUrl& url, bool activate ) const { qCDebug(PLUGIN_PATCHREVIEW) << "activating url" << url; // If the document is already open in this area, just re-activate it if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) { foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views()) { if(view->document() == dynamic_cast(doc)) { if (activate) { // use openDocument() for the activation so that the document is added to File/Open Recent. ICore::self()->documentController()->openDocument(doc->url(), KTextEditor::Range::invalid()); } return; } } } QStandardItem* item = m_fileModel->itemForUrl( url ); IDocument* buddyDoc = nullptr; if (m_plugin->patch() && item) { for (int preRow = item->row() - 1; preRow >= 0; --preRow) { QStandardItem* preItem = m_fileModel->item(preRow); if (!m_fileModel->isCheckable() || preItem->checkState() == Qt::Checked) { // found valid predecessor, take it as buddy buddyDoc = ICore::self()->documentController()->documentForUrl(preItem->index().data(VcsFileChangesModel::UrlRole).toUrl()); if (buddyDoc) { break; } } } if (!buddyDoc) { buddyDoc = ICore::self()->documentController()->documentForUrl(m_plugin->patch()->file()); } } // we simplify and assume that documents to be opened without activating them also need not be // added to the Files/Open Recent menu. IDocument* newDoc = ICore::self()->documentController()->openDocument(url, KTextEditor::Range::invalid(), activate ? IDocumentController::DefaultMode : IDocumentController::DoNotActivate|IDocumentController::DoNotAddToRecentOpen, QString(), buddyDoc); KTextEditor::View* view = nullptr; if(newDoc) view = newDoc->activeTextView(); if(view && view->cursorPosition().line() == 0) m_plugin->seekHunk( true, url ); } void PatchReviewToolView::fileItemChanged( QStandardItem* item ) { if (item->column() != 0 || !m_plugin->patch()) return; QUrl url = item->index().data(VcsFileChangesModel::UrlRole).toUrl(); if (url.isEmpty()) return; KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if(m_fileModel->isCheckable() && item->checkState() != Qt::Checked) { // The file was deselected, so eventually close it if(doc && doc->state() == IDocument::Clean) { foreach(Sublime::View* view, ICore::self()->uiController()->activeArea()->views()) { if(view->document() == dynamic_cast(doc)) { ICore::self()->uiController()->activeArea()->closeView(view); return; } } } } else if (!doc) { // Maybe the file was unchecked before, or it was just loaded. open( url, false ); } } void PatchReviewToolView::nextFile() { seekFile(true); } void PatchReviewToolView::prevFile() { seekFile(false); } void PatchReviewToolView::deselectAll() { m_fileModel->setAllChecked(false); } void PatchReviewToolView::selectAll() { m_fileModel->setAllChecked(true); } void PatchReviewToolView::finishReview() { QList selectedUrls = m_fileModel->checkedUrls(); qCDebug(PLUGIN_PATCHREVIEW) << "finishing review with" << selectedUrls; m_plugin->finishReview( selectedUrls ); } void PatchReviewToolView::fileDoubleClicked( const QModelIndex& idx ) { const QUrl file = idx.data(VcsFileChangesModel::UrlRole).toUrl(); open( file, true ); } void PatchReviewToolView::kompareModelChanged() { QList oldCheckedUrls = m_fileModel->checkedUrls(); m_fileModel->clear(); if ( !m_plugin->modelList() ) return; QMap additionalUrls = m_plugin->patch()->additionalSelectableFiles(); const Diff2::DiffModelList* models = m_plugin->modelList()->models(); if( models ) { Diff2::DiffModelList::const_iterator it = models->constBegin(); for(; it != models->constEnd(); ++it ) { Diff2::DifferenceList * diffs = ( *it )->differences(); int cnt = 0; if ( diffs ) cnt = diffs->count(); const QUrl file = m_plugin->urlForFileModel( *it ); if( file.isLocalFile() && !QFileInfo( file.toLocalFile() ).isReadable() ) continue; VcsStatusInfo status; status.setUrl( file ); status.setState( cnt>0 ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemUpToDate ); m_fileModel->updateState( status, cnt ); } } for( QMap::const_iterator it = additionalUrls.constBegin(); it != additionalUrls.constEnd(); ++it ) { VcsStatusInfo status; status.setUrl( it.key() ); status.setState( it.value() ); m_fileModel->updateState( status ); } if(!m_resetCheckedUrls) m_fileModel->setCheckedUrls(oldCheckedUrls); else m_resetCheckedUrls = false; m_editPatch.filesList->resizeColumnToContents( 0 ); // Eventually select the active document documentActivated( ICore::self()->documentController()->activeDocument() ); } void PatchReviewToolView::documentActivated( IDocument* doc ) { if( !doc ) return; if ( !m_plugin->modelList() ) return; const auto matches = m_fileSortProxyModel->match( m_fileSortProxyModel->index(0, 0), VcsFileChangesModel::UrlRole, doc->url(), 1, Qt::MatchExactly); m_editPatch.filesList->setCurrentIndex(matches.value(0)); } void PatchReviewToolView::runTests() { IPatchSource::Ptr ipatch = m_plugin->patch(); if ( !ipatch ) { return; } IProject* project = nullptr; QMap files = ipatch->additionalSelectableFiles(); QMap::const_iterator it = files.constBegin(); for (; it != files.constEnd(); ++it) { project = ICore::self()->projectController()->findProjectForUrl(it.key()); if (project) { break; } } if (!project) { return; } m_editPatch.testProgressBar->setFormat(i18n("Running tests: %p%")); m_editPatch.testProgressBar->setValue(0); m_editPatch.testProgressBar->show(); ProjectTestJob* job = new ProjectTestJob(project, this); connect(job, &ProjectTestJob::finished, this, &PatchReviewToolView::testJobResult); connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(testJobPercent(KJob*,ulong))); ICore::self()->runController()->registerJob(job); } void PatchReviewToolView::testJobPercent(KJob* job, ulong percent) { Q_UNUSED(job); m_editPatch.testProgressBar->setValue(percent); } void PatchReviewToolView::testJobResult(KJob* job) { ProjectTestJob* testJob = qobject_cast(job); if (!testJob) { return; } ProjectTestResult result = testJob->testResult(); QString format; if (result.passed > 0 && result.failed == 0 && result.error == 0) { format = i18np("Test passed", "All %1 tests passed", result.passed); } else { format = i18n("Test results: %1 passed, %2 failed, %3 errors", result.passed, result.failed, result.error); } m_editPatch.testProgressBar->setFormat(format); // Needed because some test jobs may raise their own output views ICore::self()->uiController()->raiseToolView(this); } #include "patchreviewtoolview.moc" diff --git a/plugins/perforce/perforceplugin.cpp b/plugins/perforce/perforceplugin.cpp index 99fe31e052..83dd593915 100644 --- a/plugins/perforce/perforceplugin.cpp +++ b/plugins/perforce/perforceplugin.cpp @@ -1,691 +1,689 @@ /*************************************************************************** * Copyright 2010 Morten Danielsen Volden * * * * 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, see . * ***************************************************************************/ #include "perforceplugin.h" #include "ui/perforceimportmetadatawidget.h" #include "debug.h" #include "qtcompat_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { QString toRevisionName(const KDevelop::VcsRevision& rev, const QString& currentRevision=QString()) { bool ok; int previous = currentRevision.toInt(&ok); previous--; QString tmp; switch(rev.revisionType()) { case VcsRevision::Special: switch(rev.revisionValue().value()) { case VcsRevision::Head: return QStringLiteral("#head"); case VcsRevision::Base: return QStringLiteral("#have"); case VcsRevision::Working: return QStringLiteral("#have"); case VcsRevision::Previous: Q_ASSERT(!currentRevision.isEmpty()); tmp.setNum(previous); - tmp.prepend("#"); + tmp.prepend(QLatin1Char('#')); return tmp; case VcsRevision::Start: return QString(); case VcsRevision::UserSpecialType: //Not used Q_ASSERT(false && "i don't know how to do that"); } break; case VcsRevision::GlobalNumber: - tmp.append("#"); - tmp.append(rev.revisionValue().toString()); + tmp.append(QLatin1Char('#') + rev.revisionValue().toString()); return tmp; case VcsRevision::Date: case VcsRevision::FileNumber: case VcsRevision::Invalid: case VcsRevision::UserSpecialType: Q_ASSERT(false); } return QString(); } VcsItemEvent::Actions actionsFromString(QString const& changeDescription) { if(changeDescription == QLatin1String("add")) return VcsItemEvent::Added; if(changeDescription == QLatin1String("delete")) return VcsItemEvent::Deleted; return VcsItemEvent::Modified; } QDir urlDir(const QUrl& url) { QFileInfo f(url.toLocalFile()); if(f.isDir()) return QDir(url.toLocalFile()); else return f.absoluteDir(); } } PerforcePlugin::PerforcePlugin(QObject* parent, const QVariantList&): KDevelop::IPlugin(QStringLiteral("kdevperforce"), parent) , m_common(new KDevelop::VcsPluginHelper(this, this)) , m_perforceConfigName(QStringLiteral("p4config.txt")) , m_perforceExecutable(QStringLiteral("p4")) , m_edit_action(nullptr) { QProcessEnvironment currentEviron(QProcessEnvironment::systemEnvironment()); QString tmp(currentEviron.value(QStringLiteral("P4CONFIG"))); if (tmp.isEmpty()) { // We require the P4CONFIG variable to be set because the perforce command line client will need it setErrorDescription(i18n("The variable P4CONFIG is not set. Is perforce installed on the system?")); return; } else { m_perforceConfigName = tmp; } qCDebug(PLUGIN_PERFORCE) << "The value of P4CONFIG is : " << tmp; } PerforcePlugin::~PerforcePlugin() { } QString PerforcePlugin::name() const { return i18n("Perforce"); } KDevelop::VcsImportMetadataWidget* PerforcePlugin::createImportMetadataWidget(QWidget* parent) { return new PerforceImportMetadataWidget(parent); } bool PerforcePlugin::isValidRemoteRepositoryUrl(const QUrl& remoteLocation) { Q_UNUSED(remoteLocation); // TODO return false; } bool PerforcePlugin::isValidDirectory(const QUrl & dirPath) { const QFileInfo finfo(dirPath.toLocalFile()); QDir dir = finfo.isDir() ? QDir(dirPath.toLocalFile()) : finfo.absoluteDir(); do { if (dir.exists(m_perforceConfigName)) { return true; } } while (dir.cdUp()); return false; } bool PerforcePlugin::isVersionControlled(const QUrl& localLocation) { QFileInfo fsObject(localLocation.toLocalFile()); if (fsObject.isDir()) { return isValidDirectory(localLocation); } return parseP4fstat(fsObject, KDevelop::OutputJob::Silent); } DVcsJob* PerforcePlugin::p4fstatJob(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { DVcsJob* job = new DVcsJob(curFile.absolutePath(), this, verbosity); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); return job; } bool PerforcePlugin::parseP4fstat(const QFileInfo& curFile, OutputJob::OutputJobVerbosity verbosity) { QScopedPointer job(p4fstatJob(curFile, verbosity)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { qCDebug(PLUGIN_PERFORCE) << "Perforce returned: " << job->output(); if (!job->output().isEmpty()) return true; } return false; } QString PerforcePlugin::getRepositoryName(const QFileInfo& curFile) { static const QString DEPOT_FILE_STR(QStringLiteral("... depotFile ")); QString ret; QScopedPointer job(p4fstatJob(curFile, KDevelop::OutputJob::Silent)); if (job->exec() && job->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); foreach(const QString & line, outputLines) { int idx(line.indexOf(DEPOT_FILE_STR)); if (idx != -1) { ret = line.right(line.size() - DEPOT_FILE_STR.size()); return ret; } } } } return ret; } KDevelop::VcsJob* PerforcePlugin::repositoryLocation(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::add(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "add" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::remove(const QList& /*localLocations*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::copy(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDstn*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::move(const QUrl& /*localLocationSrc*/, const QUrl& /*localLocationDst*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::status(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "fstat" << curFile.fileName(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4StatusOutput); return job; } KDevelop::VcsJob* PerforcePlugin::revert(const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.count() != 1) { KMessageBox::error(nullptr, i18n("Please select only one item for this operation")); return nullptr; } QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "revert" << curFile.fileName(); return job; } KDevelop::VcsJob* PerforcePlugin::update(const QList& localLocations, const KDevelop::VcsRevision& /*rev*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); //*job << m_perforceExecutable << "-p" << "127.0.0.1:1666" << "info"; - Let's keep this for now it's very handy for debugging QString fileOrDirectory; if (curFile.isDir()) fileOrDirectory = curFile.absolutePath() + "/..."; else fileOrDirectory = curFile.fileName(); *job << m_perforceExecutable << "sync" << fileOrDirectory; return job; } KDevelop::VcsJob* PerforcePlugin::commit(const QString& message, const QList& localLocations, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { if (localLocations.empty() || message.isEmpty()) return errorsFound(i18n("No files or message specified")); QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "submit" << "-d" << message << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::diff(const QUrl& fileOrDirectory, const KDevelop::VcsRevision& srcRevision, const KDevelop::VcsRevision& dstRevision, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { QFileInfo curFile(fileOrDirectory.toLocalFile()); QString depotSrcFileName = getRepositoryName(curFile); QString depotDstFileName = depotSrcFileName; depotSrcFileName.append(toRevisionName(srcRevision, dstRevision.prettyValue())); // dstRevision acutally contains the number that we want to take previous of DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); switch (dstRevision.revisionType()) { case VcsRevision::FileNumber: case VcsRevision::GlobalNumber: - depotDstFileName.append("#"); - depotDstFileName.append(dstRevision.prettyValue()); + depotDstFileName.append(QLatin1Char('#') + dstRevision.prettyValue()); *job << m_perforceExecutable << "diff2" << "-u" << depotSrcFileName << depotDstFileName; break; case VcsRevision::Special: switch (dstRevision.revisionValue().value()) { case VcsRevision::Working: *job << m_perforceExecutable << "diff" << "-du" << depotSrcFileName; break; case VcsRevision::Start: case VcsRevision::UserSpecialType: default: break; } default: break; } connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4DiffOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& rev, long unsigned int limit) { static QString lastSeenChangeList; QFileInfo curFile(localLocation.toLocalFile()); QString localLocationAndRevStr = localLocation.toLocalFile(); DVcsJob* job = new DVcsJob(urlDir(localLocation), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit"; if(limit > 0) *job << QStringLiteral("-m %1").arg(limit); if (curFile.isDir()) { localLocationAndRevStr.append("/..."); } QString revStr = toRevisionName(rev, QString()); if(!revStr.isEmpty()) { // This is not too nice, but perforce argument for restricting output from filelog does not Work :-( // So putting this in so we do not end up in infinite loop calling log, if(revStr == lastSeenChangeList) { localLocationAndRevStr.append("#none"); lastSeenChangeList.clear(); } else { localLocationAndRevStr.append(revStr); lastSeenChangeList = revStr; } } *job << localLocationAndRevStr; qCDebug(PLUGIN_PERFORCE) << "Issuing the following command to p4: " << job->dvcsCommand(); connect(job, &DVcsJob::readyForParsing, this, &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::log(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/, const KDevelop::VcsRevision& /*limit*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "filelog" << "-lit" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4LogOutput); return job; } KDevelop::VcsJob* PerforcePlugin::annotate(const QUrl& localLocation, const KDevelop::VcsRevision& /*rev*/) { QFileInfo curFile(localLocation.toLocalFile()); if (curFile.isDir()) { KMessageBox::error(nullptr, i18n("Please select a file for this operation")); return errorsFound(i18n("Directory not supported for this operation")); } DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "annotate" << "-qi" << localLocation; connect(job, &DVcsJob::readyForParsing, this , &PerforcePlugin::parseP4AnnotateOutput); return job; } KDevelop::VcsJob* PerforcePlugin::resolve(const QList& /*localLocations*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::createWorkingCopy(const KDevelop::VcsLocation& /*sourceRepository*/, const QUrl& /*destinationDirectory*/, KDevelop::IBasicVersionControl::RecursionMode /*recursion*/) { return nullptr; } KDevelop::VcsLocationWidget* PerforcePlugin::vcsLocation(QWidget* parent) const { return new StandardVcsLocationWidget(parent); } KDevelop::VcsJob* PerforcePlugin::edit(const QList& localLocations) { QFileInfo curFile(localLocations.front().toLocalFile()); DVcsJob* job = new DVcsJob(curFile.dir(), this, KDevelop::OutputJob::Verbose); setEnvironmentForJob(job, curFile); *job << m_perforceExecutable << "edit" << localLocations; return job; } KDevelop::VcsJob* PerforcePlugin::edit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::unedit(const QUrl& /*localLocation*/) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::localRevision(const QUrl& /*localLocation*/, KDevelop::VcsRevision::RevisionType) { return nullptr; } KDevelop::VcsJob* PerforcePlugin::import(const QString& /*commitMessage*/, const QUrl& /*sourceDirectory*/, const KDevelop::VcsLocation& /*destinationRepository*/) { return nullptr; } KDevelop::ContextMenuExtension PerforcePlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { m_common->setupFromContext(context); const QList & ctxUrlList = m_common->contextUrlList(); bool hasVersionControlledEntries = false; for( const QUrl& url : ctxUrlList) { if (isValidDirectory(url)) { hasVersionControlledEntries = true; break; } } if (!hasVersionControlledEntries) return IPlugin::contextMenuExtension(context, parent); QMenu * perforceMenu = m_common->commonActions(parent); perforceMenu->addSeparator(); perforceMenu->addSeparator(); if (!m_edit_action) { m_edit_action = new QAction(i18n("Edit"), this); connect(m_edit_action, &QAction::triggered, this, & PerforcePlugin::ctxEdit); } perforceMenu->addAction(m_edit_action); ContextMenuExtension menuExt; menuExt.addAction(ContextMenuExtension::VcsGroup, perforceMenu->menuAction()); return menuExt; } void PerforcePlugin::ctxEdit() { QList const & ctxUrlList = m_common->contextUrlList(); KDevelop::ICore::self()->runController()->registerJob(edit(ctxUrlList)); } void PerforcePlugin::setEnvironmentForJob(DVcsJob* job, const QFileInfo& curFile) { KProcess* jobproc = job->process(); jobproc->setEnv(QStringLiteral("P4CONFIG"), m_perforceConfigName); if (curFile.isDir()) { jobproc->setEnv(QStringLiteral("PWD"), curFile.filePath()); } else { jobproc->setEnv(QStringLiteral("PWD"), curFile.absolutePath()); } } QList PerforcePlugin::getQvariantFromLogOutput(QStringList const& outputLines) { static const QString LOGENTRY_START(QStringLiteral("... #")); static const QString DEPOTMESSAGE_START(QStringLiteral("... .")); QMap changes; QList commits; QString currentFileName; QString changeNumberStr, author,changeDescription, commitMessage; VcsEvent currentVcsEvent; VcsItemEvent currentRepoFile; VcsRevision rev; int indexofAt; int changeNumber = 0; foreach(const QString & line, outputLines) { if (!line.startsWith(LOGENTRY_START) && !line.startsWith(DEPOTMESSAGE_START) && !line.startsWith('\t')) { currentFileName = line; } if(line.indexOf(LOGENTRY_START) != -1) { // expecting the Logentry line to be of the form: //... #5 change 10 edit on 2010/12/06 12:07:31 by mvo@testbed (text) changeNumberStr = line.section(' ', 3, 3 ); // We use global change number changeNumber = changeNumberStr.toInt(); author = line.section(' ', 9, 9); changeDescription = line.section(' ' , 4, 4 ); indexofAt = author.indexOf('@'); author.remove(indexofAt, author.size()); // Only keep the username itself rev.setRevisionValue(changeNumberStr, KDevelop::VcsRevision::GlobalNumber); changes[changeNumber].setRevision(rev); changes[changeNumber].setAuthor(author); changes[changeNumber].setDate(QDateTime::fromString(line.section(' ', 6, 7), QStringLiteral("yyyy/MM/dd hh:mm:ss"))); currentRepoFile.setRepositoryLocation(currentFileName); currentRepoFile.setActions( actionsFromString(changeDescription) ); changes[changeNumber].addItem(currentRepoFile); commitMessage.clear(); // We have a new entry, clear message } if (line.startsWith('\t') || line.startsWith(DEPOTMESSAGE_START)) { commitMessage += line.trimmed() + '\n'; changes[changeNumber].setMessage(commitMessage); } } for(const auto& item : qAsConst(changes)) { commits.prepend(QVariant::fromValue(item)); } return commits; } void PerforcePlugin::parseP4StatusOutput(DVcsJob* job) { QStringList outputLines = job->output().split('\n', QString::SkipEmptyParts); QVariantList statuses; static const QString ACTION_STR(QStringLiteral("... action ")); static const QString CLIENT_FILE_STR(QStringLiteral("... clientFile ")); VcsStatusInfo status; status.setState(VcsStatusInfo::ItemUserState); foreach(const QString & line, outputLines) { int idx(line.indexOf(ACTION_STR)); if (idx != -1) { QString curr = line.right(line.size() - ACTION_STR.size()); if (curr == QLatin1String("edit")) { status.setState(VcsStatusInfo::ItemModified); } else if (curr == QLatin1String("add")) { status.setState(VcsStatusInfo::ItemAdded); } else { status.setState(VcsStatusInfo::ItemUserState); } continue; } idx = line.indexOf(CLIENT_FILE_STR); if (idx != -1) { QUrl fileUrl = QUrl::fromLocalFile(line.right(line.size() - CLIENT_FILE_STR.size())); status.setUrl(fileUrl); } } statuses.append(qVariantFromValue(status)); job->setResults(statuses); } void PerforcePlugin::parseP4LogOutput(KDevelop::DVcsJob* job) { QList commits(getQvariantFromLogOutput(job->output().split('\n', QString::SkipEmptyParts))); job->setResults(commits); } void PerforcePlugin::parseP4DiffOutput(DVcsJob* job) { VcsDiff diff; diff.setDiff(job->output()); QDir dir(job->directory()); do { if (dir.exists(m_perforceConfigName)) { break; } } while (dir.cdUp()); diff.setBaseDiff(QUrl::fromLocalFile(dir.absolutePath())); job->setResults(qVariantFromValue(diff)); } void PerforcePlugin::parseP4AnnotateOutput(DVcsJob *job) { QVariantList results; /// First get the changelists for this file QStringList strList(job->dvcsCommand()); QString localLocation(strList.last()); /// ASSUMPTION WARNING - localLocation is the last in the annotate command KDevelop::VcsRevision dummyRev; QScopedPointer logJob(new DVcsJob(job->directory(), this, OutputJob::Silent)); QFileInfo curFile(localLocation); setEnvironmentForJob(logJob.data(), curFile); *logJob << m_perforceExecutable << "filelog" << "-lit" << localLocation; QList commits; if (logJob->exec() && logJob->status() == KDevelop::VcsJob::JobSucceeded) { if (!job->output().isEmpty()) { commits = getQvariantFromLogOutput(logJob->output().split('\n', QString::SkipEmptyParts)); } } VcsEvent item; QMap globalCommits; /// Move the VcsEvents to a more suitable data strucure for (QList::const_iterator commitsIt = commits.constBegin(), commitsEnd = commits.constEnd(); commitsIt != commitsEnd; ++commitsIt) { if(commitsIt->canConvert()) { item = commitsIt->value(); } globalCommits.insert(item.revision().revisionValue().toLongLong(), item); } QStringList lines = job->output().split('\n'); int lineNumber = 0; QMap::iterator currentEvent; bool convertToIntOk(false); int globalRevisionInt(0); QString globalRevision; for (QStringList::const_iterator it = lines.constBegin(), itEnd = lines.constEnd(); it != itEnd; ++it) { if (it->isEmpty()) { continue; } globalRevision = it->left(it->indexOf(':')); VcsAnnotationLine annotation; annotation.setLineNumber(lineNumber); VcsRevision rev; rev.setRevisionValue(globalRevision, KDevelop::VcsRevision::GlobalNumber); annotation.setRevision(rev); // Find the other info in the commits list globalRevisionInt = globalRevision.toLongLong(&convertToIntOk); if(convertToIntOk) { currentEvent = globalCommits.find(globalRevisionInt); annotation.setAuthor(currentEvent->author()); annotation.setCommitMessage(currentEvent->message()); annotation.setDate(currentEvent->date()); } results += qVariantFromValue(annotation); ++lineNumber; } job->setResults(results); } KDevelop::VcsJob* PerforcePlugin::errorsFound(const QString& error, KDevelop::OutputJob::OutputJobVerbosity verbosity) { DVcsJob* j = new DVcsJob(QDir::temp(), this, verbosity); *j << "echo" << i18n("error: %1", error) << "-n"; return j; } diff --git a/plugins/perforce/ui/perforceimportmetadatawidget.cpp b/plugins/perforce/ui/perforceimportmetadatawidget.cpp index 118824e621..b2add33991 100644 --- a/plugins/perforce/ui/perforceimportmetadatawidget.cpp +++ b/plugins/perforce/ui/perforceimportmetadatawidget.cpp @@ -1,210 +1,211 @@ /*************************************************************************** * This file is part of KDevelop Perforce plugin, KDE project * * * * Copyright 2018 Morten Danielsen Volden * * * * 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, see . * ***************************************************************************/ #include "perforceimportmetadatawidget.h" #include #include #include #include #include using namespace KDevelop; PerforceImportMetadataWidget::PerforceImportMetadataWidget(QWidget* parent) : VcsImportMetadataWidget(parent) , m_ui(new Ui::PerforceImportMetadataWidget) { m_ui->setupUi(this); m_ui->executableLoc->setText("/usr/bin/p4"); m_ui->p4portEdit->setText("perforce:1666"); QProcessEnvironment curEnv = QProcessEnvironment::systemEnvironment(); m_ui->p4configEdit->setText(curEnv.contains("P4CONFIG") ? curEnv.value("P4CONFIG") : ""); m_ui->p4portEdit->setText(curEnv.contains("P4PORT") ? curEnv.value("P4PORT") : ""); m_ui->p4userEdit->setText(curEnv.contains("P4USER") ? curEnv.value("P4USER") : ""); curEnv.contains("P4CONFIG") ? m_ui->radioButtonConfig->setChecked(true) : m_ui->radioButtonVariables->setChecked(true); curEnv.contains("P4CONFIG") ? m_ui->p4configEdit->setEnabled(true) : m_ui->p4configEdit->setEnabled(false); m_ui->sourceLoc->setEnabled(false); m_ui->sourceLoc->setMode(KFile::Directory); m_ui->errorMsg->setTextColor(QColor(255, 0, 0)); m_ui->errorMsg->setReadOnly(true); m_ui->p4clientEdit->setEditable(true); connect(m_ui->p4clientEdit, static_cast(&KComboBox::returnPressed), this, &PerforceImportMetadataWidget::changed ); connect(m_ui->radioButtonConfig, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setEnabled); connect(m_ui->radioButtonVariables, &QRadioButton::clicked, m_ui->p4configEdit, &QLineEdit::setDisabled); connect(m_ui->testP4setupButton, &QPushButton::pressed, this, &PerforceImportMetadataWidget::testP4setup); } QUrl PerforceImportMetadataWidget::source() const { return m_ui->sourceLoc->url(); } VcsLocation PerforceImportMetadataWidget::destination() const { VcsLocation dest; dest.setRepositoryServer(m_ui->p4portEdit->text()); dest.setUserData(QVariant::fromValue(m_ui->p4userEdit->text())); dest.setRepositoryBranch(m_ui->p4clientEdit->itemText(0)); return dest; } QString PerforceImportMetadataWidget::message() const { return QString(); //TODO: return m_ui->message->toPlainText(); } void PerforceImportMetadataWidget::setSourceLocation(const VcsLocation& url) { m_ui->sourceLoc->setUrl(url.localUrl()); } void PerforceImportMetadataWidget::setSourceLocationEditable(bool enable) { m_ui->sourceLoc->setEnabled(enable); } void PerforceImportMetadataWidget::setMessage(const QString& message) { Q_UNUSED(message); //FIXME: correct ui field needs to be set //m_ui->message->setText(message); } bool PerforceImportMetadataWidget::hasValidData() const { // FIXME: It has valid data if testP4setup has completed correctly. AND client name has been set to something return !m_ui->p4clientEdit->itemText(0).isEmpty(); } void PerforceImportMetadataWidget::testP4setup() { m_ui->errorMsg->clear(); m_ui->p4clientEdit->clear(); if (!validateP4executable()) return; QDir execDir(m_ui->sourceLoc->url().toLocalFile()); QTemporaryDir tmpDir; if (!execDir.exists()) execDir = tmpDir.path(); if(!validateP4port(execDir.path())) return; if(!validateP4user(execDir.path())) return; emit changed(); } bool PerforceImportMetadataWidget::validateP4executable() { if (QStandardPaths::findExecutable(m_ui->executableLoc->url().toLocalFile()).isEmpty()) { m_ui->errorMsg->setText("Unable to find perforce executable. Is it installed on the system? Is it in your PATH?"); return false; } return true; } bool PerforceImportMetadataWidget::validateP4user(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); - exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList() << QStringLiteral("workspaces") << - QStringLiteral("-u") << m_ui->p4userEdit->text() + exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList{QStringLiteral("workspaces"), + QStringLiteral("-u"), m_ui->p4userEdit->text()} ); exec.waitForFinished(); QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); // std::cout << "Exited with code: " << exec.exitCode() << std::endl; // std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; // std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with exit code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } if(!processStdout.isEmpty()) { QStringList clientCmdOutput = processStdout.split(QLatin1Char('\n'),QString::SkipEmptyParts); QStringList clientItems; + clientItems.reserve(clientCmdOutput.size()); for(QString const& clientLine : clientCmdOutput) { QStringList wordsInLine = clientLine.split(QLatin1Char(' ')); // Client mvo_testkdevinteg 2017/05/22 root C:\P4repo 'Created by mvo. ' -- Line would be expected to look like so clientItems.append(wordsInLine.at(1)); } m_ui->p4clientEdit->addItems(clientItems); } return true; } bool PerforceImportMetadataWidget::validateP4port(const QString& projectDir) const { QProcess exec; QProcessEnvironment p4execEnvironment; p4execEnvironment.insert(QString("P4PORT"), m_ui->p4portEdit->displayText()); QTextStream out(stdout); for (QString x : p4execEnvironment.toStringList()) { out << x << endl; } exec.setWorkingDirectory(projectDir); exec.setProcessEnvironment(p4execEnvironment); exec.start(m_ui->executableLoc->url().toLocalFile(), QStringList() << QStringLiteral("info")); exec.waitForFinished(); //QString processStdout(exec.readAllStandardOutput()); QString processStderr(exec.readAllStandardError()); //std::cout << "Exited with code: " << exec.exitCode() << std::endl; //std::cout << "Exited with stdout" << processStdout.toStdString() << std::endl; //std::cout << "Exited with stderr" << processStderr.toStdString() << std::endl; if (exec.exitCode() != 0) { if(!processStderr.isEmpty()) { m_ui->errorMsg->setText(processStderr); } else { QString msg("P4 Client failed with error code: "); msg += QString::number(exec.exitCode()); m_ui->errorMsg->setText(msg); } return false; } return true; } diff --git a/plugins/problemreporter/problemreportermodel.cpp b/plugins/problemreporter/problemreportermodel.cpp index 3758d5ba6b..f64420668b 100644 --- a/plugins/problemreporter/problemreportermodel.cpp +++ b/plugins/problemreporter/problemreportermodel.cpp @@ -1,167 +1,169 @@ /* * 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 #include #include using namespace KDevelop; const int ProblemReporterModel::MinTimeout = 1000; const int ProblemReporterModel::MaxTimeout = 5000; ProblemReporterModel::ProblemReporterModel(QObject* parent) : ProblemModel(parent, new FilteredProblemStore()) { setFeatures(CanDoFullUpdate | CanShowImports | ScopeFilter | SeverityFilter | ShowSource); 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(), &FilteredProblemStore::changed, this, &ProblemReporterModel::onProblemsChanged); connect(ICore::self()->languageController()->staticAssistantsManager(), &StaticAssistantsManager::problemsChanged, this, &ProblemReporterModel::onProblemsChanged); } ProblemReporterModel::~ProblemReporterModel() { } QVector ProblemReporterModel::problems(const QSet& docs) const { QVector result; DUChainReadLocker lock; foreach (const IndexedString& doc, docs) { if (doc.isEmpty()) continue; TopDUContext* ctx = DUChain::self()->chainForDocument(doc); if (!ctx) continue; - foreach (const ProblemPointer& p, DUChainUtils::allProblemsForContext(ctx)) { + const auto allProblems = DUChainUtils::allProblemsForContext(ctx); + result.reserve(result.size() + allProblems.size()); + for (const ProblemPointer& p : allProblems) { result.append(p); } } return result; } void ProblemReporterModel::forceFullUpdate() { Q_ASSERT(thread() == QThread::currentThread()); QSet documents = store()->documents()->get(); if (showImports()) documents += store()->documents()->getImports(); DUChainReadLocker lock(DUChain::lock()); foreach (const IndexedString& document, documents) { if (document.isEmpty()) continue; TopDUContext::Features updateType = TopDUContext::ForceUpdate; if (documents.size() == 1) updateType = TopDUContext::ForceUpdateRecursive; DUChain::self()->updateContextForUrl( document, (TopDUContext::Features)(updateType | TopDUContext::VisibleDeclarationsAndContexts)); } } 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(); /// Will trigger signal changed() if problems change store()->setCurrentDocument(IndexedString(doc->url())); endResetModel(); } void ProblemReporterModel::problemsUpdated(const KDevelop::IndexedString& url) { Q_ASSERT(thread() == QThread::currentThread()); // skip update for urls outside current scope if (!store()->documents()->get().contains(url) && !(showImports() && store()->documents()->getImports().contains(url))) return; /// 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::rebuildProblemList() { /// No locking here, because it may be called from an already locked context beginResetModel(); QVector allProblems = problems(store()->documents()->get()); if (showImports()) allProblems += problems(store()->documents()->getImports()); store()->setProblems(allProblems); endResetModel(); } diff --git a/plugins/problemreporter/problemtreeview.cpp b/plugins/problemreporter/problemtreeview.cpp index b2050c82d6..fc9ea5043e 100644 --- a/plugins/problemreporter/problemtreeview.cpp +++ b/plugins/problemreporter/problemtreeview.cpp @@ -1,255 +1,257 @@ /* * 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 { QString descriptionFromProblem(IProblem::Ptr problem) { QString text; const auto location = problem->finalLocation(); if (location.isValid()) { text += location.document.toUrl() .adjusted(QUrl::NormalizePathSegments) .toDisplayString(QUrl::PreferLocalFile); if (location.start().line() >= 0) { - text += QStringLiteral(":") + QString::number(location.start().line() + 1); + text += QLatin1Char(':') + QString::number(location.start().line() + 1); if (location.start().column() >= 0) { - text += QStringLiteral(":") + QString::number(location.start().column() + 1); + text += QLatin1Char(':') + QString::number(location.start().column() + 1); } } text += QStringLiteral(": "); } text += problem->description(); if (!problem->explanation().isEmpty()) { - text += QStringLiteral("\n") + problem->explanation(); + text += QLatin1Char('\n') + problem->explanation(); } return text; } } 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)) { 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::ShowSource)) { hideColumn(ProblemModel::Source); } 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); m_proxy->setFilterKeyColumn(-1); m_proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); } 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(); } if (QFile::exists(url.toLocalFile())) { ICore::self()->documentController()->openDocument(url, start); } } 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(); } int ProblemTreeView::setFilter(const QString& filterText) { m_proxy->setFilterFixedString(filterText); return m_proxy->rowCount(); } 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()) return; const auto problem = index.data(ProblemModel::ProblemRole).value(); if (!problem) { return; } QPointer m = new QMenu(this); m->addSection(i18n("Problem")); auto copyDescriptionAction = m->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy Description")); connect(copyDescriptionAction, &QAction::triggered, this, [problem]() { QApplication::clipboard()->setText(descriptionFromProblem(problem), QClipboard::Clipboard); }); QExplicitlySharedDataPointer solution = problem->solutionAssistant(); if (solution && !solution->actions().isEmpty()) { QList actions; - foreach (KDevelop::IAssistantAction::Ptr assistantAction, solution->actions()) { + const auto solutionActions = solution->actions(); + actions.reserve(solutionActions.size()); + for (auto assistantAction : solutionActions) { auto action = assistantAction->toQAction(m.data()); action->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); actions << action; } QString title = solution->title(); title = KDevelop::htmlToPlainText(title); title.replace(QLatin1String("'"), QLatin1String("\'")); m->addSection(i18n("Solve: %1", 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/projectfilter/filter.cpp b/plugins/projectfilter/filter.cpp index 66fb58ce5f..b7e010cfaa 100644 --- a/plugins/projectfilter/filter.cpp +++ b/plugins/projectfilter/filter.cpp @@ -1,164 +1,165 @@ /* * 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) { } 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) { } SerializedFilter::SerializedFilter(const QString& pattern, Filter::Targets targets, Filter::Type type) : pattern(pattern) , targets(targets) , type(type) { } namespace KDevelop { SerializedFilters defaultFilters() { SerializedFilters ret; + ret.reserve(20); // 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 = { 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 = { // binary files (Unix) QStringLiteral("*.o"), QStringLiteral("*.a"), QStringLiteral("*.so"), QStringLiteral("*.so.*"), // binary files (Windows) QStringLiteral("*.obj"), QStringLiteral("*.lib"), QStringLiteral("*.dll"), QStringLiteral("*.exp"), QStringLiteral("*.pdb"), // generated files QStringLiteral("moc_*.cpp"), QStringLiteral("*.moc"), QStringLiteral("ui_*.h"), QStringLiteral("*.qmlc"), 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/projectfilterconfigpage.cpp b/plugins/projectfilter/projectfilterconfigpage.cpp index 898c1d57c4..ef45f24cac 100644 --- a/plugins/projectfilter/projectfilterconfigpage.cpp +++ b/plugins/projectfilter/projectfilterconfigpage.cpp @@ -1,213 +1,213 @@ /* This file is part of KDevelop Copyright 2008 Alexander Dymo This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectfilterconfigpage.h" #include #include #include #include #include #include "ui_projectfiltersettings.h" #include #include "filtermodel.h" #include "comboboxdelegate.h" #include "projectfilterprovider.h" using namespace KDevelop; ProjectFilterConfigPage::ProjectFilterConfigPage(ProjectFilterProvider* provider, const ProjectConfigOptions& options, QWidget* parent) : ProjectConfigPage(provider, options, parent) , m_model(new FilterModel(this)) , m_projectFilterProvider(provider) , m_ui(new Ui::ProjectFilterSettings) { m_ui->setupUi(this); m_ui->messageWidget->hide(); m_ui->filters->setSelectionMode(QAbstractItemView::SingleSelection); m_ui->filters->setModel(m_model); m_ui->filters->setRootIsDecorated(false); m_ui->filters->header()->setSectionResizeMode(FilterModel::Pattern, QHeaderView::Stretch); m_ui->filters->header()->setSectionResizeMode(FilterModel::Targets, QHeaderView::ResizeToContents); m_ui->filters->header()->setSectionResizeMode(FilterModel::Inclusive, QHeaderView::ResizeToContents); m_ui->filters->setItemDelegateForColumn(FilterModel::Targets, - new ComboBoxDelegate(QVector() - << ComboBoxDelegate::Item(i18n("Files"), static_cast(Filter::Files)) - << ComboBoxDelegate::Item(i18n("Folders"), static_cast(Filter::Folders)) - << ComboBoxDelegate::Item(i18n("Files and Folders"), static_cast(Filter::Folders | Filter::Files)) + new ComboBoxDelegate(QVector{ + {i18n("Files"), static_cast(Filter::Files)}, + {i18n("Folders"), static_cast(Filter::Folders)}, + {i18n("Files and Folders"), static_cast(Filter::Folders | Filter::Files)}} , this)); m_ui->filters->setItemDelegateForColumn(FilterModel::Inclusive, - new ComboBoxDelegate(QVector() - << ComboBoxDelegate::Item(i18n("Exclude"), false) - << ComboBoxDelegate::Item(i18n("Include"), true) + new ComboBoxDelegate(QVector{ + {i18n("Exclude"), false}, + {i18n("Include"), true}} , this)); m_ui->filters->installEventFilter(this); m_ui->filters->setDragEnabled(true); m_ui->filters->setDragDropMode(QAbstractItemView::InternalMove); m_ui->filters->setAutoScroll(true); reset(); selectionChanged(); connect(m_ui->filters->selectionModel(), &QItemSelectionModel::currentChanged, this, &ProjectFilterConfigPage::selectionChanged); connect(this, &ProjectFilterConfigPage::changed, this, &ProjectFilterConfigPage::selectionChanged); connect(m_model, &FilterModel::dataChanged, this, &ProjectFilterConfigPage::emitChanged); connect(m_model, &FilterModel::rowsInserted, this, &ProjectFilterConfigPage::emitChanged); connect(m_model, &FilterModel::rowsRemoved, this, &ProjectFilterConfigPage::emitChanged); connect(m_model, &FilterModel::modelReset, this, &ProjectFilterConfigPage::emitChanged); connect(m_model, &FilterModel::rowsMoved, this, &ProjectFilterConfigPage::emitChanged); connect(m_ui->add, &QPushButton::clicked, this, &ProjectFilterConfigPage::add); connect(m_ui->remove, &QPushButton::clicked, this, &ProjectFilterConfigPage::remove); connect(m_ui->moveUp, &QPushButton::clicked, this, &ProjectFilterConfigPage::moveUp); connect(m_ui->moveDown, &QPushButton::clicked, this, &ProjectFilterConfigPage::moveDown); } ProjectFilterConfigPage::~ProjectFilterConfigPage() { } void ProjectFilterConfigPage::apply() { ProjectConfigPage::apply(); writeFilters(m_model->filters(), project()->projectConfiguration()); m_projectFilterProvider->updateProjectFilters(project()); } void ProjectFilterConfigPage::reset() { ProjectConfigPage::reset(); m_model->setFilters(readFilters(project()->projectConfiguration())); } void ProjectFilterConfigPage::defaults() { ProjectConfigPage::defaults(); m_model->setFilters(defaultFilters()); } bool ProjectFilterConfigPage::eventFilter(QObject* object, QEvent* event) { if (object == m_ui->filters && event->type() == QEvent::KeyRelease) { QKeyEvent* key = static_cast(event); if (key->key() == Qt::Key_Delete && key->modifiers() == Qt::NoModifier && m_ui->filters->currentIndex().isValid()) { // workaround https://bugs.kde.org/show_bug.cgi?id=324451 // there is no other way I see to figure out whether an editor is showing... QWidget* editor = m_ui->filters->viewport()->findChild(); if (!editor || !editor->isVisible()) { // editor is not showing remove(); return true; // eat event } } } return ProjectConfigPage::eventFilter(object, event); } void ProjectFilterConfigPage::selectionChanged() { bool hasSelection = m_ui->filters->currentIndex().isValid(); int row = -1; if (hasSelection) { row = m_ui->filters->currentIndex().row(); } m_ui->remove->setEnabled(hasSelection); m_ui->moveDown->setEnabled(hasSelection && row != m_model->rowCount() - 1); m_ui->moveUp->setEnabled(hasSelection && row != 0); } void ProjectFilterConfigPage::add() { m_model->insertRows(m_model->rowCount(), 1); const QModelIndex index = m_model->index(m_model->rowCount() - 1, FilterModel::Pattern, QModelIndex()); m_ui->filters->setCurrentIndex(index); m_ui->filters->edit(index); } void ProjectFilterConfigPage::remove() { Q_ASSERT(m_ui->filters->currentIndex().isValid()); m_model->removeRows(m_ui->filters->currentIndex().row(), 1); } void ProjectFilterConfigPage::moveUp() { Q_ASSERT(m_ui->filters->currentIndex().isValid()); m_model->moveFilterUp(m_ui->filters->currentIndex().row()); } void ProjectFilterConfigPage::moveDown() { Q_ASSERT(m_ui->filters->currentIndex().isValid()); m_model->moveFilterDown(m_ui->filters->currentIndex().row()); } void ProjectFilterConfigPage::checkFilters() { // check for errors, only show one error at once QString errorText; foreach(const Filter& filter, m_model->filters()) { const QString &pattern = filter.pattern.pattern(); if (pattern.isEmpty()) { errorText = i18n("A filter with an empty pattern will match all items. Use \"*\" to make this explicit."); break; } else if (pattern.endsWith('/') && filter.targets == Filter::Files) { errorText = i18n("A filter ending on \"/\" can never match a file."); break; } } if (!errorText.isEmpty()) { m_ui->messageWidget->setMessageType(KMessageWidget::Error); m_ui->messageWidget->setText(errorText); m_ui->messageWidget->animatedShow(); } else { m_ui->messageWidget->animatedHide(); } } void ProjectFilterConfigPage::emitChanged() { checkFilters(); emit changed(); } QString ProjectFilterConfigPage::fullName() const { return i18n("Configure Project Filter"); } QIcon ProjectFilterConfigPage::icon() const { return QIcon::fromTheme(QStringLiteral("view-filter")); } QString ProjectFilterConfigPage::name() const { return i18n("Project Filter"); } diff --git a/plugins/projectmanagerview/cutcopypastehelpers.cpp b/plugins/projectmanagerview/cutcopypastehelpers.cpp index 282df8e931..8a0e0cca5a 100644 --- a/plugins/projectmanagerview/cutcopypastehelpers.cpp +++ b/plugins/projectmanagerview/cutcopypastehelpers.cpp @@ -1,352 +1,353 @@ /* This file is part of KDevelop Copyright (C) 2017 Alexander Potashev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cutcopypastehelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace CutCopyPasteHelpers { TaskInfo::TaskInfo(const TaskStatus status, const TaskType type, const Path::List& src, const Path& dest) : m_status(status), m_type(type), m_src(src), m_dest(dest) { } TaskInfo TaskInfo::createMove(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::MOVE, src, dest); } TaskInfo TaskInfo::createCopy(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::COPY, src, dest); } TaskInfo TaskInfo::createDeletion(const bool ok, const Path::List& src, const Path& dest) { return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE, TaskType::DELETION, src, dest); } static QWidget* createPasteStatsWidget(QWidget *parent, const QVector& tasks) { // TODO: Create a model for the task list, and use it here instead of using QTreeWidget QTreeWidget* treeWidget = new QTreeWidget(parent); QList items; + items.reserve(tasks.size()); for (const TaskInfo& task : tasks) { int srcCount = task.m_src.size(); const bool withChildren = srcCount != 1; const QString destPath = task.m_dest.pathOrUrl(); QString text; if (withChildren) { // Multiple source items in the current suboperation switch (task.m_type) { case TaskType::MOVE: text = i18np("Move %1 item into %2", "Move %1 items into %2", srcCount, destPath); break; case TaskType::COPY: text = i18np("Copy %1 item into %2", "Copy %1 items into %2", srcCount, destPath); break; case TaskType::DELETION: text = i18np("Delete %1 item", "Delete %1 items", srcCount); break; } } else { // One source item in the current suboperation const QString srcPath = task.m_src[0].pathOrUrl(); switch (task.m_type) { case TaskType::MOVE: text = i18n("Move item %1 into %2", srcPath, destPath); break; case TaskType::COPY: text = i18n("Copy item %1 into %2", srcPath, destPath); break; case TaskType::DELETION: text = i18n("Delete item %1", srcPath); break; } } QString tooltip; QString iconName; switch (task.m_status) { case TaskStatus::SUCCESS: tooltip = i18n("Suboperation succeeded"); iconName = QStringLiteral("dialog-ok"); break; case TaskStatus::FAILURE: tooltip = i18n("Suboperation failed"); iconName = QStringLiteral("dialog-error"); break; case TaskStatus::SKIPPED: tooltip = i18n("Suboperation skipped to prevent data loss"); iconName = QStringLiteral("dialog-warning"); break; } QTreeWidgetItem* item = new QTreeWidgetItem; item->setText(0, text); item->setIcon(0, QIcon::fromTheme(iconName)); item->setToolTip(0, tooltip); items.append(item); if (withChildren) { for (const Path& src : task.m_src) { QTreeWidgetItem* childItem = new QTreeWidgetItem; childItem->setText(0, src.pathOrUrl()); item->addChild(childItem); } } } treeWidget->insertTopLevelItems(0, items); treeWidget->headerItem()->setHidden(true); return treeWidget; } SourceToDestinationMap mapSourceToDestination(const Path::List& sourcePaths, const Path& destinationPath) { // For example you are moving the following items into /dest/ // * /tests/ // * /tests/abc.cpp // If you pass them as is, moveFilesAndFolders() will crash (see note: // "Do not attempt to move subitems along with their parents"). // Thus we filter out subitems from "Path::List filteredPaths". // // /tests/abc.cpp will be implicitly moved to /dest/tests/abc.cpp, for // that reason we add "/dest/tests/abc.cpp" into "result.finalPaths" as well as // "/dest/tests". // // "result.finalPaths" will be used to highlight destination items after // copy/move. Path::List sortedPaths = sourcePaths; std::sort(sortedPaths.begin(), sortedPaths.end()); SourceToDestinationMap result; for (const Path& path : sortedPaths) { if (!result.filteredPaths.isEmpty() && result.filteredPaths.back().isParentOf(path)) { // think: "/tests" const Path& previousPath = result.filteredPaths.back(); // think: "/dest" + "/".relativePath("/tests/abc.cpp") = /dest/tests/abc.cpp result.finalPaths[previousPath].append(Path(destinationPath, previousPath.parent().relativePath(path))); } else { // think: "/tests" result.filteredPaths.append(path); // think: "/dest" + "tests" = "/dest/tests" result.finalPaths[path].append(Path(destinationPath, path.lastPathSegment())); } } return result; } struct ClassifiedPaths { // Items originating from projects open in this KDevelop session QHash> itemsPerProject; // Items that do not belong to known projects Path::List alienSrcPaths; }; static ClassifiedPaths classifyPaths(const Path::List& paths, KDevelop::ProjectModel* projectModel) { ClassifiedPaths result; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); if (!items.empty()) { for (ProjectBaseItem* item : items) { IProject* project = item->project(); if (!result.itemsPerProject.contains(project)) { result.itemsPerProject[project] = QList(); } result.itemsPerProject[project].append(item); } } else { result.alienSrcPaths.append(path); } } return result; } QVector copyMoveItems(const Path::List& paths, ProjectBaseItem* destItem, const Operation operation) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); const ClassifiedPaths cl = classifyPaths(paths, projectModel); QVector tasks; IProject* destProject = destItem->project(); IProjectFileManager* destProjectFileManager = destProject->projectFileManager(); ProjectFolderItem* destFolder = destItem->folder(); Path destPath = destFolder->path(); for (IProject* srcProject : cl.itemsPerProject.keys()) { const auto& itemsList = cl.itemsPerProject[srcProject]; Path::List pathsList; pathsList.reserve(itemsList.size()); for (KDevelop::ProjectBaseItem* item : itemsList) { pathsList.append(item->path()); } if (srcProject == destProject) { if (operation == Operation::CUT) { // Move inside project const bool ok = destProjectFileManager->moveFilesAndFolders(itemsList, destFolder); tasks.append(TaskInfo::createMove(ok, pathsList, destPath)); } else { // Copy inside project const bool ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(ok, pathsList, destPath)); } } else { // Copy/move between projects: // 1. Copy and add into destination project; // 2. Remove from source project. const bool copy_ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder); tasks.append(TaskInfo::createCopy(copy_ok, pathsList, destPath)); if (operation == Operation::CUT) { if (copy_ok) { IProjectFileManager* srcProjectFileManager = srcProject->projectFileManager(); const bool deletion_ok = srcProjectFileManager->removeFilesAndFolders(itemsList); tasks.append(TaskInfo::createDeletion(deletion_ok, pathsList, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, pathsList, destPath)); } } } } // Copy/move items from outside of all open projects if (!cl.alienSrcPaths.isEmpty()) { const bool alien_copy_ok = destProjectFileManager->copyFilesAndFolders(cl.alienSrcPaths, destFolder); tasks.append(TaskInfo::createCopy(alien_copy_ok, cl.alienSrcPaths, destPath)); if (operation == Operation::CUT) { if (alien_copy_ok) { QList urlsToDelete; urlsToDelete.reserve(cl.alienSrcPaths.size()); for (const Path& path : cl.alienSrcPaths) { urlsToDelete.append(path.toUrl()); } KIO::DeleteJob* deleteJob = KIO::del(urlsToDelete); const bool deletion_ok = deleteJob->exec(); tasks.append(TaskInfo::createDeletion(deletion_ok, cl.alienSrcPaths, destPath)); } else { tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, cl.alienSrcPaths, destPath)); } } } return tasks; } void showWarningDialogForFailedPaste(QWidget* parent, const QVector& tasks) { QDialog* dialog = new QDialog(parent); dialog->setWindowTitle(i18nc("@title:window", "Paste Failed")); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok); QObject::connect(buttonBox, &QDialogButtonBox::clicked, dialog, &QDialog::accept); dialog->setWindowModality(Qt::WindowModal); dialog->setModal(true); QWidget* mainWidget = new QWidget(dialog); QVBoxLayout* mainLayout = new QVBoxLayout(mainWidget); const int spacingHint = mainWidget->style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); mainLayout->setSpacing(spacingHint * 2); // provide extra spacing mainLayout->setMargin(0); QHBoxLayout* hLayout = new QHBoxLayout; hLayout->setMargin(0); hLayout->setSpacing(-1); // use default spacing mainLayout->addLayout(hLayout, 0); QLabel* iconLabel = new QLabel(mainWidget); // Icon QStyleOption option; option.initFrom(mainWidget); QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning")); iconLabel->setPixmap(icon.pixmap(mainWidget->style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, mainWidget))); QVBoxLayout* iconLayout = new QVBoxLayout(); iconLayout->addStretch(1); iconLayout->addWidget(iconLabel); iconLayout->addStretch(5); hLayout->addLayout(iconLayout, 0); hLayout->addSpacing(spacingHint); const QString text = i18n("Failed to paste. Below is a list of suboperations that have been attempted."); QLabel* messageLabel = new QLabel(text, mainWidget); messageLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); hLayout->addWidget(messageLabel, 5); QWidget* statsWidget = createPasteStatsWidget(dialog, tasks); QVBoxLayout* topLayout = new QVBoxLayout; dialog->setLayout(topLayout); topLayout->addWidget(mainWidget); topLayout->addWidget(statsWidget, 1); topLayout->addWidget(buttonBox); dialog->setMinimumSize(300, qMax(150, qMax(iconLabel->sizeHint().height(), messageLabel->sizeHint().height()))); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } // namespace CutCopyPasteHelpers diff --git a/plugins/projectmanagerview/projectmanagerview.cpp b/plugins/projectmanagerview/projectmanagerview.cpp index d71ae29edc..29483c6f92 100644 --- a/plugins/projectmanagerview/projectmanagerview.cpp +++ b/plugins/projectmanagerview/projectmanagerview.cpp @@ -1,288 +1,290 @@ /* This file is part of KDevelop Copyright 2005 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2008 Aleix Pol This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectmanagerview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../openwith/iopenwith.h" #include #include #include "projectmanagerviewplugin.h" #include "vcsoverlayproxymodel.h" #include "ui_projectmanagerview.h" #include "debug.h" using namespace KDevelop; ProjectManagerViewItemContext::ProjectManagerViewItemContext(const QList< ProjectBaseItem* >& items, ProjectManagerView* view) : ProjectItemContextImpl(items), m_view(view) { } ProjectManagerView *ProjectManagerViewItemContext::view() const { return m_view; } static const char sessionConfigGroup[] = "ProjectManagerView"; static const char splitterStateConfigKey[] = "splitterState"; static const char targetsVisibleConfigKey[] = "targetsVisible"; static const int projectTreeViewStrechFactor = 75; // % static const int projectBuildSetStrechFactor = 25; // % ProjectManagerView::ProjectManagerView( ProjectManagerViewPlugin* plugin, QWidget *parent ) : QWidget( parent ), m_ui(new Ui::ProjectManagerView), m_plugin(plugin) { m_ui->setupUi( this ); setFocusProxy(m_ui->projectTreeView); m_ui->projectTreeView->installEventFilter(this); setWindowIcon( QIcon::fromTheme( QStringLiteral("project-development"), windowIcon() ) ); KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup); if (pmviewConfig.hasKey(splitterStateConfigKey)) { QByteArray geometry = pmviewConfig.readEntry(splitterStateConfigKey, QByteArray()); m_ui->splitter->restoreState(geometry); } else { m_ui->splitter->setStretchFactor(0, projectTreeViewStrechFactor); m_ui->splitter->setStretchFactor(1, projectBuildSetStrechFactor); } // keep the project tree view from collapsing (would confuse users) m_ui->splitter->setCollapsible(0, false); m_syncAction = plugin->actionCollection()->action(QStringLiteral("locate_document")); Q_ASSERT(m_syncAction); m_syncAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); m_syncAction->setText(i18n("Locate Current Document")); m_syncAction->setToolTip(i18n("Locates the current document in the project tree and selects it.")); m_syncAction->setIcon(QIcon::fromTheme(QStringLiteral("dirsync"))); m_syncAction->setShortcut(Qt::ControlModifier + Qt::Key_Less); connect(m_syncAction, &QAction::triggered, this, &ProjectManagerView::locateCurrentDocument); addAction(m_syncAction); updateSyncAction(); m_toggleTargetsAction = new QAction(i18n("Show Targets"), this); m_toggleTargetsAction->setCheckable(true); m_toggleTargetsAction->setChecked(pmviewConfig.readEntry(targetsVisibleConfigKey, true)); m_toggleTargetsAction->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); connect(m_toggleTargetsAction, &QAction::triggered, this, &ProjectManagerView::toggleHideTargets); addAction(m_toggleTargetsAction); addAction(plugin->actionCollection()->action(QStringLiteral("project_build"))); addAction(plugin->actionCollection()->action(QStringLiteral("project_install"))); addAction(plugin->actionCollection()->action(QStringLiteral("project_clean"))); connect(m_ui->projectTreeView, &ProjectTreeView::activate, this, &ProjectManagerView::open); m_ui->buildSetView->setProjectView( this ); m_modelFilter = new ProjectProxyModel( this ); m_modelFilter->showTargets(m_toggleTargetsAction->isChecked()); m_modelFilter->setSourceModel(ICore::self()->projectController()->projectModel()); m_overlayProxy = new VcsOverlayProxyModel( this ); m_overlayProxy->setSourceModel(m_modelFilter); m_ui->projectTreeView->setModel( m_overlayProxy ); connect( m_ui->projectTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectManagerView::selectionChanged ); connect( KDevelop::ICore::self()->documentController(), &IDocumentController::documentClosed, this, &ProjectManagerView::updateSyncAction); connect( KDevelop::ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ProjectManagerView::updateSyncAction); connect( qobject_cast(KDevelop::ICore::self()->uiController()->activeMainWindow()), &Sublime::MainWindow::areaChanged, this, &ProjectManagerView::updateSyncAction); selectionChanged(); //Update the "sync" button after the initialization has completed, to see whether there already is some open documents QMetaObject::invokeMethod(this, "updateSyncAction", Qt::QueuedConnection); // Need to set this to get horizontal scrollbar. Also needs to be done after // the setModel call m_ui->projectTreeView->header()->setSectionResizeMode( QHeaderView::ResizeToContents ); } bool ProjectManagerView::eventFilter(QObject* obj, QEvent* event) { if (obj == m_ui->projectTreeView) { if (event->type() == QEvent::KeyRelease) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Delete && keyEvent->modifiers() == Qt::NoModifier) { m_plugin->removeItems(selectedItems()); return true; } else if (keyEvent->key() == Qt::Key_F2 && keyEvent->modifiers() == Qt::NoModifier) { m_plugin->renameItems(selectedItems()); return true; } else if (keyEvent->key() == Qt::Key_C && keyEvent->modifiers() == Qt::ControlModifier) { m_plugin->copyFromContextMenu(); return true; } else if (keyEvent->key() == Qt::Key_V && keyEvent->modifiers() == Qt::ControlModifier) { m_plugin->pasteFromContextMenu(); return true; } } } return QObject::eventFilter(obj, event); } void ProjectManagerView::selectionChanged() { m_ui->buildSetView->selectionChanged(); QList selected; - foreach( const QModelIndex& idx, m_ui->projectTreeView->selectionModel()->selectedRows() ) - { + const auto selectedRows = m_ui->projectTreeView->selectionModel()->selectedRows(); + selected.reserve(selectedRows.size()); + for (const auto& idx : selectedRows) { selected << ICore::self()->projectController()->projectModel()->itemFromIndex(indexFromView( idx )); } selected.removeAll(nullptr); KDevelop::ICore::self()->selectionController()->updateSelection( new ProjectManagerViewItemContext( selected, this ) ); } void ProjectManagerView::updateSyncAction() { m_syncAction->setEnabled( KDevelop::ICore::self()->documentController()->activeDocument() ); } ProjectManagerView::~ProjectManagerView() { KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup); pmviewConfig.writeEntry(splitterStateConfigKey, m_ui->splitter->saveState()); pmviewConfig.sync(); delete m_ui; } QList ProjectManagerView::selectedItems() const { QList items; foreach( const QModelIndex &idx, m_ui->projectTreeView->selectionModel()->selectedIndexes() ) { KDevelop::ProjectBaseItem* item = ICore::self()->projectController()->projectModel()->itemFromIndex(indexFromView(idx)); if( item ) items << item; else qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "adding an unknown item"; } return items; } void ProjectManagerView::selectItems(const QList< ProjectBaseItem* >& items) { QItemSelection selection; + selection.reserve(items.size()); foreach (ProjectBaseItem *item, items) { QModelIndex indx = indexToView(item->index()); selection.append(QItemSelectionRange(indx, indx)); m_ui->projectTreeView->setCurrentIndex(indx); } m_ui->projectTreeView->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } void ProjectManagerView::expandItem(ProjectBaseItem* item) { m_ui->projectTreeView->expand( indexToView(item->index())); } void ProjectManagerView::toggleHideTargets(bool visible) { KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup); pmviewConfig.writeEntry(targetsVisibleConfigKey, visible); m_modelFilter->showTargets(visible); } void ProjectManagerView::locateCurrentDocument() { ICore::self()->uiController()->raiseToolView(this); KDevelop::IDocument *doc = ICore::self()->documentController()->activeDocument(); if (!doc) { // in theory we should never get a null pointer as the action is only enabled // when there is an active document. // but: in practice it can happen that you close the last document and press // the shortcut to locate a doc or vice versa... so just do the failsafe thing here... return; } QModelIndex bestMatch; foreach (IProject* proj, ICore::self()->projectController()->projects()) { foreach (KDevelop::ProjectFileItem* item, proj->filesForPath(IndexedString(doc->url()))) { QModelIndex index = indexToView(item->index()); if (index.isValid()) { if (!bestMatch.isValid()) { bestMatch = index; } else if (KDevelop::ProjectBaseItem* parent = item->parent()) { // prefer files in their real folders over the 'copies' in the target folders if (!parent->target()) { bestMatch = index; break; } } } } } if (bestMatch.isValid()) { m_ui->projectTreeView->clearSelection(); m_ui->projectTreeView->setCurrentIndex(bestMatch); m_ui->projectTreeView->expand(bestMatch); m_ui->projectTreeView->scrollTo(bestMatch); } } void ProjectManagerView::open( const Path& path ) { IOpenWith::openFiles(QList() << path.toUrl()); } QModelIndex ProjectManagerView::indexFromView(const QModelIndex& index) const { return m_modelFilter->mapToSource( m_overlayProxy->mapToSource(index) ); } QModelIndex ProjectManagerView::indexToView(const QModelIndex& index) const { return m_overlayProxy->mapFromSource( m_modelFilter->mapFromSource(index) ); } diff --git a/plugins/projectmanagerview/projectmanagerviewplugin.cpp b/plugins/projectmanagerview/projectmanagerviewplugin.cpp index 8a3995a256..5c1c89d03a 100644 --- a/plugins/projectmanagerview/projectmanagerviewplugin.cpp +++ b/plugins/projectmanagerview/projectmanagerviewplugin.cpp @@ -1,793 +1,797 @@ /* This file is part of KDevelop Copyright 2004 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2016, 2017 Alexander Potashev This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectmanagerviewplugin.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 "projectmanagerview.h" #include "debug.h" #include "cutcopypastehelpers.h" using namespace KDevelop; K_PLUGIN_FACTORY_WITH_JSON(ProjectManagerFactory, "kdevprojectmanagerview.json", registerPlugin();) namespace { QAction* createSeparatorAction() { QAction* separator = new QAction(nullptr); separator->setSeparator(true); return separator; } // Returns nullptr iff the list of URLs to copy/cut was empty QMimeData* createClipboardMimeData(const bool cut) { auto* ctx = dynamic_cast( ICore::self()->selectionController()->currentSelection()); QList urls; QList mostLocalUrls; for (const ProjectBaseItem* item : ctx->items()) { if (item->folder() || item->file()) { const QUrl& url = item->path().toUrl(); urls << url; mostLocalUrls << KFileItem(url).mostLocalUrl(); } } qCDebug(PLUGIN_PROJECTMANAGERVIEW) << urls; if (urls.isEmpty()) { return nullptr; } QMimeData* mimeData = new QMimeData; KIO::setClipboardDataCut(mimeData, cut); KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData); return mimeData; } } // anonymous namespace class KDevProjectManagerViewFactory: public KDevelop::IToolViewFactory { public: explicit KDevProjectManagerViewFactory( ProjectManagerViewPlugin *plugin ): mplugin( plugin ) {} QWidget* create( QWidget *parent = nullptr ) override { return new ProjectManagerView( mplugin, parent ); } Qt::DockWidgetArea defaultPosition() override { return Qt::LeftDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ProjectsView"); } private: ProjectManagerViewPlugin *mplugin; }; class ProjectManagerViewPluginPrivate { public: ProjectManagerViewPluginPrivate() {} KDevProjectManagerViewFactory *factory; QList ctxProjectItemList; QAction* m_buildAll; QAction* m_build; QAction* m_install; QAction* m_clean; QAction* m_configure; QAction* m_prune; }; static QList itemsFromIndexes(const QList& indexes) { QList items; ProjectModel* model = ICore::self()->projectController()->projectModel(); + items.reserve(indexes.size()); foreach(const QModelIndex& index, indexes) { items += model->itemFromIndex(index); } return items; } ProjectManagerViewPlugin::ProjectManagerViewPlugin( QObject *parent, const QVariantList& ) : IPlugin( QStringLiteral("kdevprojectmanagerview"), parent ), d(new ProjectManagerViewPluginPrivate) { d->m_buildAll = new QAction( i18n("Build all Projects"), this ); d->m_buildAll->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( d->m_buildAll, &QAction::triggered, this, &ProjectManagerViewPlugin::buildAllProjects ); actionCollection()->addAction( QStringLiteral("project_buildall"), d->m_buildAll ); d->m_build = new QAction( i18n("Build Selection"), this ); d->m_build->setIconText( i18n("Build") ); actionCollection()->setDefaultShortcut( d->m_build, Qt::Key_F8 ); d->m_build->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); d->m_build->setEnabled( false ); connect( d->m_build, &QAction::triggered, this, &ProjectManagerViewPlugin::buildProjectItems ); actionCollection()->addAction( QStringLiteral("project_build"), d->m_build ); d->m_install = new QAction( i18n("Install Selection"), this ); d->m_install->setIconText( i18n("Install") ); d->m_install->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); actionCollection()->setDefaultShortcut( d->m_install, Qt::SHIFT + Qt::Key_F8 ); d->m_install->setEnabled( false ); connect( d->m_install, &QAction::triggered, this, &ProjectManagerViewPlugin::installProjectItems ); actionCollection()->addAction( QStringLiteral("project_install"), d->m_install ); d->m_clean = new QAction( i18n("Clean Selection"), this ); d->m_clean->setIconText( i18n("Clean") ); d->m_clean->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); d->m_clean->setEnabled( false ); connect( d->m_clean, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanProjectItems ); actionCollection()->addAction( QStringLiteral("project_clean"), d->m_clean ); d->m_configure = new QAction( i18n("Configure Selection"), this ); d->m_configure->setMenuRole( QAction::NoRole ); // OSX: Be explicit about role, prevent hiding due to conflict with "Preferences..." menu item d->m_configure->setIconText( i18n("Configure") ); d->m_configure->setIcon(QIcon::fromTheme(QStringLiteral("run-build-configure"))); d->m_configure->setEnabled( false ); connect( d->m_configure, &QAction::triggered, this, &ProjectManagerViewPlugin::configureProjectItems ); actionCollection()->addAction( QStringLiteral("project_configure"), d->m_configure ); d->m_prune = new QAction( i18n("Prune Selection"), this ); d->m_prune->setIconText( i18n("Prune") ); d->m_prune->setIcon(QIcon::fromTheme(QStringLiteral("run-build-prune"))); d->m_prune->setEnabled( false ); connect( d->m_prune, &QAction::triggered, this, &ProjectManagerViewPlugin::pruneProjectItems ); actionCollection()->addAction( QStringLiteral("project_prune"), d->m_prune ); // only add the action so that its known in the actionCollection // and so that it's shortcut etc. pp. is restored // apparently that is not possible to be done in the view itself *sigh* actionCollection()->addAction( QStringLiteral("locate_document") ); setXMLFile( QStringLiteral("kdevprojectmanagerview.rc") ); d->factory = new KDevProjectManagerViewFactory( this ); core()->uiController()->addToolView( i18n("Projects"), d->factory ); connect(core()->selectionController(), &ISelectionController::selectionChanged, this, &ProjectManagerViewPlugin::updateActionState); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsInserted, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::rowsRemoved, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); connect(ICore::self()->projectController()->buildSetModel(), &KDevelop::ProjectBuildSetModel::modelReset, this, &ProjectManagerViewPlugin::updateFromBuildSetChange); } void ProjectManagerViewPlugin::updateFromBuildSetChange() { updateActionState( core()->selectionController()->currentSelection() ); } void ProjectManagerViewPlugin::updateActionState( KDevelop::Context* ctx ) { bool isEmpty = ICore::self()->projectController()->buildSetModel()->items().isEmpty(); if( isEmpty ) { isEmpty = !ctx || ctx->type() != Context::ProjectItemContext || static_cast(ctx)->items().isEmpty(); } d->m_build->setEnabled( !isEmpty ); d->m_install->setEnabled( !isEmpty ); d->m_clean->setEnabled( !isEmpty ); d->m_configure->setEnabled( !isEmpty ); d->m_prune->setEnabled( !isEmpty ); } ProjectManagerViewPlugin::~ProjectManagerViewPlugin() { delete d; } void ProjectManagerViewPlugin::unload() { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "unloading manager view"; core()->uiController()->removeToolView(d->factory); } ContextMenuExtension ProjectManagerViewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { if( context->type() != KDevelop::Context::ProjectItemContext ) return IPlugin::contextMenuExtension(context, parent); KDevelop::ProjectItemContext* ctx = static_cast(context); QList items = ctx->items(); d->ctxProjectItemList.clear(); if( items.isEmpty() ) return IPlugin::contextMenuExtension(context, parent); //TODO: also needs: removeTarget, removeFileFromTarget, runTargetsFromContextMenu ContextMenuExtension menuExt; bool needsCreateFile = true; bool needsCreateFolder = true; bool needsCloseProjects = true; bool needsBuildItems = true; bool needsFolderItems = true; bool needsCutRenameRemove = true; bool needsRemoveTargetFiles = true; bool needsPaste = true; //needsCreateFile if there is one item and it's a folder or target needsCreateFile &= (items.count() == 1) && (items.first()->folder() || items.first()->target()); //needsCreateFolder if there is one item and it's a folder needsCreateFolder &= (items.count() == 1) && (items.first()->folder()); needsPaste = needsCreateFolder; + d->ctxProjectItemList.reserve(items.size()); foreach( ProjectBaseItem* item, items ) { d->ctxProjectItemList << item->index(); //needsBuildItems if items are limited to targets and buildfolders needsBuildItems &= item->target() || item->type() == ProjectBaseItem::BuildFolder; //needsCloseProjects if items are limited to top level folders (Project Folders) needsCloseProjects &= item->folder() && !item->folder()->parent(); //needsFolderItems if items are limited to folders needsFolderItems &= (bool)item->folder(); //needsRemove if items are limited to non-top-level folders or files that don't belong to targets needsCutRenameRemove &= (item->folder() && item->parent()) || (item->file() && !item->parent()->target()); //needsRemoveTargets if items are limited to file items with target parents needsRemoveTargetFiles &= (item->file() && item->parent()->target()); } if ( needsCreateFile ) { QAction* action = new QAction(i18n("Create &File..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFileFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsCreateFolder ) { QAction* action = new QAction(i18n("Create F&older..."), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::createFolderFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } if ( needsBuildItems ) { QAction* action = new QAction(i18nc("@action", "&Build"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::buildItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Install"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-install"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::installItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18nc("@action", "&Clean"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("run-build-clean"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::cleanItemsFromContextMenu ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); action = new QAction(i18n("&Add to Build Set"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset ); menuExt.addAction( ContextMenuExtension::BuildGroup, action ); } if ( needsCloseProjects ) { QAction* close = new QAction(i18np("C&lose Project", "Close Projects", items.count()), parent); close->setIcon(QIcon::fromTheme(QStringLiteral("project-development-close"))); connect( close, &QAction::triggered, this, &ProjectManagerViewPlugin::closeProjects ); menuExt.addAction( ContextMenuExtension::ProjectGroup, close ); } if ( needsFolderItems ) { QAction* action = new QAction(i18n("&Reload"), parent); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect( action, &QAction::triggered, this, &ProjectManagerViewPlugin::reloadFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, action ); } // Populating cut/copy/paste group if ( !menuExt.actions(ContextMenuExtension::FileGroup).isEmpty() ) { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* cut = KStandardAction::cut(this, SLOT(cutFromContextMenu()), this); cut->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction(ContextMenuExtension::FileGroup, cut); } { QAction* copy = KStandardAction::copy(this, SLOT(copyFromContextMenu()), this); copy->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, copy ); } if (needsPaste) { QAction* paste = KStandardAction::paste(this, SLOT(pasteFromContextMenu()), this); paste->setShortcutContext(Qt::WidgetShortcut); menuExt.addAction( ContextMenuExtension::FileGroup, paste ); } // Populating rename/remove group { menuExt.addAction( ContextMenuExtension::FileGroup, createSeparatorAction() ); } if ( needsCutRenameRemove ) { QAction* remove = new QAction(i18n("Remo&ve"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); QAction* rename = new QAction(i18n("Re&name..."), parent); rename->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); connect( rename, &QAction::triggered, this, &ProjectManagerViewPlugin::renameItemFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, rename ); } if ( needsRemoveTargetFiles ) { QAction* remove = new QAction(i18n("Remove From &Target"), parent); remove->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); connect( remove, &QAction::triggered, this, &ProjectManagerViewPlugin::removeTargetFilesFromContextMenu ); menuExt.addAction( ContextMenuExtension::FileGroup, remove ); } if ( needsCutRenameRemove || needsRemoveTargetFiles ) { menuExt.addAction(ContextMenuExtension::FileGroup, createSeparatorAction()); } return menuExt; } void ProjectManagerViewPlugin::closeProjects() { QList projectsToClose; ProjectModel* model = ICore::self()->projectController()->projectModel(); foreach( const QModelIndex& index, d->ctxProjectItemList ) { KDevelop::ProjectBaseItem* item = model->itemFromIndex(index); if( !projectsToClose.contains( item->project() ) ) { projectsToClose << item->project(); } } d->ctxProjectItemList.clear(); foreach( KDevelop::IProject* proj, projectsToClose ) { core()->projectController()->closeProject( proj ); } } void ProjectManagerViewPlugin::installItemsFromContextMenu() { runBuilderJob( BuilderJob::Install, itemsFromIndexes(d->ctxProjectItemList) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::cleanItemsFromContextMenu() { runBuilderJob( BuilderJob::Clean, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } void ProjectManagerViewPlugin::buildItemsFromContextMenu() { runBuilderJob( BuilderJob::Build, itemsFromIndexes( d->ctxProjectItemList ) ); d->ctxProjectItemList.clear(); } QList ProjectManagerViewPlugin::collectAllProjects() { QList items; - foreach( KDevelop::IProject* project, core()->projectController()->projects() ) - { + const auto projects = core()->projectController()->projects(); + items.reserve(projects.size()); + for (auto* project : projects) { items << project->projectItem(); } return items; } void ProjectManagerViewPlugin::buildAllProjects() { runBuilderJob( BuilderJob::Build, collectAllProjects() ); } QList ProjectManagerViewPlugin::collectItems() { QList items; QList buildItems = ICore::self()->projectController()->buildSetModel()->items(); if( !buildItems.isEmpty() ) { foreach( const BuildItem& buildItem, buildItems ) { if( ProjectBaseItem* item = buildItem.findItem() ) { items << item; } } } else { KDevelop::ProjectItemContext* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); items = ctx->items(); } return items; } void ProjectManagerViewPlugin::runBuilderJob( BuilderJob::BuildType type, const QList& items ) { BuilderJob* builder = new BuilderJob; builder->addItems( type, items ); builder->updateJobName(); ICore::self()->uiController()->registerStatus(new JobStatus(builder)); ICore::self()->runController()->registerJob( builder ); } void ProjectManagerViewPlugin::installProjectItems() { runBuilderJob( KDevelop::BuilderJob::Install, collectItems() ); } void ProjectManagerViewPlugin::pruneProjectItems() { runBuilderJob( KDevelop::BuilderJob::Prune, collectItems() ); } void ProjectManagerViewPlugin::configureProjectItems() { runBuilderJob( KDevelop::BuilderJob::Configure, collectItems() ); } void ProjectManagerViewPlugin::cleanProjectItems() { runBuilderJob( KDevelop::BuilderJob::Clean, collectItems() ); } void ProjectManagerViewPlugin::buildProjectItems() { runBuilderJob( KDevelop::BuilderJob::Build, collectItems() ); } void ProjectManagerViewPlugin::addItemsFromContextMenuToBuildset( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { ICore::self()->projectController()->buildSetModel()->addProjectItem( item ); } } void ProjectManagerViewPlugin::runTargetsFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { KDevelop::ProjectExecutableTargetItem* t=item->executable(); if(t) { qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "Running target: " << t->text() << t->builtUrl(); } } } void ProjectManagerViewPlugin::projectConfiguration( ) { if( !d->ctxProjectItemList.isEmpty() ) { ProjectModel* model = ICore::self()->projectController()->projectModel(); core()->projectController()->configureProject( model->itemFromIndex(d->ctxProjectItemList.at( 0 ))->project() ); } } void ProjectManagerViewPlugin::reloadFromContextMenu( ) { QList< KDevelop::ProjectFolderItem* > folders; foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList ) ) { if ( item->folder() ) { // since reloading should be recursive, only pass the upper-most items bool found = false; foreach ( KDevelop::ProjectFolderItem* existing, folders ) { if ( existing->path().isParentOf(item->folder()->path()) ) { // simply skip this child found = true; break; } else if ( item->folder()->path().isParentOf(existing->path()) ) { // remove the child in the list and add the current item instead folders.removeOne(existing); // continue since there could be more than one existing child } } if ( !found ) { folders << item->folder(); } } } foreach( KDevelop::ProjectFolderItem* folder, folders ) { folder->project()->projectFileManager()->reload(folder); } } void ProjectManagerViewPlugin::createFolderFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList )) { if ( item->folder() ) { QWidget* window(ICore::self()->uiController()->activeMainWindow()->window()); QString name = QInputDialog::getText ( window, i18n ( "Create Folder in %1", item->folder()->path().pathOrUrl() ), i18n ( "Folder name:" ) ); if (!name.isEmpty()) { item->project()->projectFileManager()->addFolder( Path(item->path(), name), item->folder() ); } } } } void ProjectManagerViewPlugin::removeFromContextMenu() { removeItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::removeItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } //copy the list of selected items and sort it to guarantee parents will come before children QList sortedItems = items; std::sort(sortedItems.begin(), sortedItems.end(), ProjectBaseItem::pathLessThan); Path lastFolder; QHash< IProjectFileManager*, QList > filteredItems; QStringList itemPaths; foreach( KDevelop::ProjectBaseItem* item, sortedItems ) { if (item->isProjectRoot()) { continue; } else if (item->folder() || item->file()) { //make sure no children of folders that will be deleted are listed if (lastFolder.isParentOf(item->path())) { continue; } else if (item->folder()) { lastFolder = item->path(); } IProjectFileManager* manager = item->project()->projectFileManager(); if (manager) { filteredItems[manager] << item; itemPaths << item->path().pathOrUrl(); } } } if (filteredItems.isEmpty()) { return; } if (KMessageBox::warningYesNoList( QApplication::activeWindow(), i18np("Do you really want to delete this item?", "Do you really want to delete these %1 items?", itemPaths.size()), itemPaths, i18n("Delete Files"), KStandardGuiItem::del(), KStandardGuiItem::cancel() ) == KMessageBox::No) { return; } //Go though projectmanagers, have them remove the files and folders that they own QHash< IProjectFileManager*, QList >::iterator it; for (it = filteredItems.begin(); it != filteredItems.end(); ++it) { Q_ASSERT(it.key()); it.key()->removeFilesAndFolders(it.value()); } } void ProjectManagerViewPlugin::removeTargetFilesFromContextMenu() { QList items = itemsFromIndexes( d->ctxProjectItemList ); QHash< IBuildSystemManager*, QList > itemsByBuildSystem; foreach(ProjectBaseItem *item, items) itemsByBuildSystem[item->project()->buildSystemManager()].append(item->file()); QHash< IBuildSystemManager*, QList >::iterator it; for (it = itemsByBuildSystem.begin(); it != itemsByBuildSystem.end(); ++it) it.key()->removeFilesFromTargets(it.value()); } void ProjectManagerViewPlugin::renameItemFromContextMenu() { renameItems(itemsFromIndexes( d->ctxProjectItemList )); } void ProjectManagerViewPlugin::renameItems(const QList< ProjectBaseItem* >& items) { if (items.isEmpty()) { return; } QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); foreach( KDevelop::ProjectBaseItem* item, items ) { if ((item->type()!=ProjectBaseItem::BuildFolder && item->type()!=ProjectBaseItem::Folder && item->type()!=ProjectBaseItem::File) || !item->parent()) { continue; } const QString src = item->text(); //Change QInputDialog->KFileSaveDialog? QString name = QInputDialog::getText( window, i18n("Rename..."), i18n("New name for '%1':", item->text()), QLineEdit::Normal, item->text() ); if (!name.isEmpty() && name != src) { ProjectBaseItem::RenameStatus status = item->rename( name ); switch(status) { case ProjectBaseItem::RenameOk: break; case ProjectBaseItem::ExistingItemSameName: KMessageBox::error(window, i18n("There is already a file named '%1'", name)); break; case ProjectBaseItem::ProjectManagerRenameFailed: KMessageBox::error(window, i18n("Could not rename '%1'", name)); break; case ProjectBaseItem::InvalidNewName: KMessageBox::error(window, i18n("'%1' is not a valid file name", name)); break; } } } } ProjectFileItem* createFile(const ProjectFolderItem* item) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); QString name = QInputDialog::getText(window, i18n("Create File in %1", item->path().pathOrUrl()), i18n("File name:")); if(name.isEmpty()) return nullptr; ProjectFileItem* ret = item->project()->projectFileManager()->addFile( Path(item->path(), name), item->folder() ); if (ret) { ICore::self()->documentController()->openDocument( ret->path().toUrl() ); } return ret; } void ProjectManagerViewPlugin::createFileFromContextMenu( ) { foreach( KDevelop::ProjectBaseItem* item, itemsFromIndexes( d->ctxProjectItemList ) ) { if ( item->folder() ) { createFile(item->folder()); } else if ( item->target() ) { ProjectFolderItem* folder=dynamic_cast(item->parent()); if(folder) { ProjectFileItem* f=createFile(folder); if(f) item->project()->buildSystemManager()->addFilesToTarget(QList() << f, item->target()); } } } } void ProjectManagerViewPlugin::copyFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(false)); } void ProjectManagerViewPlugin::cutFromContextMenu() { qApp->clipboard()->setMimeData(createClipboardMimeData(true)); } static void selectItemsByPaths(ProjectManagerView* view, const Path::List& paths) { KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel(); QList newItems; for (const Path& path : paths) { QList items = projectModel->itemsForPath(IndexedString(path.path())); newItems.append(items); for (ProjectBaseItem* item : items) { view->expandItem(item->parent()); } } view->selectItems(newItems); } void ProjectManagerViewPlugin::pasteFromContextMenu() { KDevelop::ProjectItemContext* ctx = static_cast(ICore::self()->selectionController()->currentSelection()); if (ctx->items().count() != 1) { return; //do nothing if multiple or none items are selected } ProjectBaseItem* destItem = ctx->items().at(0); if (!destItem->folder()) { return; //do nothing if the target is not a directory } const QMimeData* data = qApp->clipboard()->mimeData(); qCDebug(PLUGIN_PROJECTMANAGERVIEW) << data->urls(); Path::List origPaths = toPathList(data->urls()); const bool isCut = KIO::isClipboardDataCut(data); const CutCopyPasteHelpers::SourceToDestinationMap map = CutCopyPasteHelpers::mapSourceToDestination(origPaths, destItem->folder()->path()); QVector tasks = CutCopyPasteHelpers::copyMoveItems( map.filteredPaths, destItem, isCut ? CutCopyPasteHelpers::Operation::CUT : CutCopyPasteHelpers::Operation::COPY); // Select new items in the project manager view ProjectManagerViewItemContext* itemCtx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemCtx) { Path::List finalPathsList; for (const auto& task : tasks) { if (task.m_status == CutCopyPasteHelpers::TaskStatus::SUCCESS && task.m_type != CutCopyPasteHelpers::TaskType::DELETION) { + finalPathsList.reserve(finalPathsList.size() + task.m_src.size()); for (const Path& src : task.m_src) { finalPathsList.append(map.finalPaths[src]); } } } selectItemsByPaths(itemCtx->view(), finalPathsList); } // If there was a single failure, display a warning dialog. const bool anyFailed = std::any_of(tasks.begin(), tasks.end(), [](const CutCopyPasteHelpers::TaskInfo& task) { return task.m_status != CutCopyPasteHelpers::TaskStatus::SUCCESS; }); if (anyFailed) { QWidget* window = ICore::self()->uiController()->activeMainWindow()->window(); showWarningDialogForFailedPaste(window, tasks); } } #include "projectmanagerviewplugin.moc" diff --git a/plugins/projectmanagerview/projecttreeview.cpp b/plugins/projectmanagerview/projecttreeview.cpp index 237f80b52b..58cd5f86a2 100644 --- a/plugins/projectmanagerview/projecttreeview.cpp +++ b/plugins/projectmanagerview/projecttreeview.cpp @@ -1,478 +1,479 @@ /* This file is part of KDevelop Copyright 2005 Roberto Raggi Copyright 2007 Andreas Pakulat Copyright 2009 Aleix Pol This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projecttreeview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectmanagerviewplugin.h" #include "projectmodelsaver.h" #include "projectmodelitemdelegate.h" #include "debug.h" #include #include using namespace KDevelop; namespace { const char settingsConfigGroup[] = "ProjectTreeView"; QList fileItemsWithin(const QList& items) { QList fileItems; fileItems.reserve(items.size()); foreach(ProjectBaseItem* item, items) { if (ProjectFileItem *file = item->file()) fileItems.append(file); else if (item->folder()) fileItems.append(fileItemsWithin(item->children())); } return fileItems; } QList topLevelItemsWithin(QList items) { std::sort(items.begin(), items.end(), ProjectBaseItem::pathLessThan); Path lastFolder; for (int i = items.size() - 1; i >= 0; --i) { if (lastFolder.isParentOf(items[i]->path())) items.removeAt(i); else if (items[i]->folder()) lastFolder = items[i]->path(); } return items; } template void filterDroppedItems(QList &items, ProjectBaseItem* dest) { for (int i = items.size() - 1; i >= 0; --i) { //No drag and drop from and to same location if (items[i]->parent() == dest) items.removeAt(i); //No moving between projects (technically feasible if the projectmanager is the same though...) else if (items[i]->project() != dest->project()) items.removeAt(i); } } //TODO test whether this could be replaced by projectbuildsetwidget.cpp::showContextMenu_appendActions void popupContextMenu_appendActions(QMenu& menu, const QList& actions) { menu.addActions(actions); menu.addSeparator(); } } ProjectTreeView::ProjectTreeView( QWidget *parent ) : QTreeView( parent ), m_previousSelection ( nullptr ) { header()->hide(); setEditTriggers( QAbstractItemView::EditKeyPressed ); setContextMenuPolicy( Qt::CustomContextMenu ); setSelectionMode( QAbstractItemView::ExtendedSelection ); setIndentation(10); setDragEnabled(true); setDragDropMode(QAbstractItemView::InternalMove); setAutoScroll(true); setAutoExpandDelay(300); setItemDelegate(new ProjectModelItemDelegate(this)); connect( this, &ProjectTreeView::customContextMenuRequested, this, &ProjectTreeView::popupContextMenu ); connect( this, &ProjectTreeView::activated, this, &ProjectTreeView::slotActivated ); connect( ICore::self(), &ICore::aboutToShutdown, this, &ProjectTreeView::aboutToShutdown); connect( ICore::self()->projectController(), &IProjectController::projectOpened, this, &ProjectTreeView::restoreState ); connect( ICore::self()->projectController(), &IProjectController::projectClosed, this, &ProjectTreeView::projectClosed ); } ProjectTreeView::~ProjectTreeView() { } ProjectBaseItem* ProjectTreeView::itemAtPos(const QPoint& pos) const { return indexAt(pos).data(ProjectModel::ProjectItemRole).value(); } void ProjectTreeView::dropEvent(QDropEvent* event) { ProjectItemContext* selectionCtxt = static_cast(KDevelop::ICore::self()->selectionController()->currentSelection()); ProjectBaseItem* destItem = itemAtPos(event->pos()); if (destItem && (dropIndicatorPosition() == AboveItem || dropIndicatorPosition() == BelowItem)) destItem = destItem->parent(); if (selectionCtxt && destItem) { if (ProjectFolderItem *folder = destItem->folder()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ShiftModifier ).toString(); seq.chop(1); // chop superfluous '+' QAction* move = new QAction(i18n( "&Move Here" ) + '\t' + seq, &dropMenu); move->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); dropMenu.addAction(move); seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* copy = new QAction(i18n( "&Copy Here" ) + '\t' + seq, &dropMenu); copy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); dropMenu.addAction(copy); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n( "C&ancel" ) + '\t' + QKeySequence( Qt::Key_Escape ).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = copy; } else if (modifiers == Qt::ShiftModifier) { executedAction = move; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } QList usefulItems = topLevelItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); Path::List paths; + paths.reserve(usefulItems.size()); foreach (ProjectBaseItem* i, usefulItems) { paths << i->path(); } bool success = false; if (executedAction == copy) { success = destItem->project()->projectFileManager()->copyFilesAndFolders(paths, folder); } else if (executedAction == move) { success = destItem->project()->projectFileManager()->moveFilesAndFolders(usefulItems, folder); } if (success) { //expand target folder expand( mapFromItem(folder)); //and select new items QItemSelection selection; foreach (const Path &path, paths) { const Path targetPath(folder->path(), path.lastPathSegment()); foreach (ProjectBaseItem *item, folder->children()) { if (item->path() == targetPath) { QModelIndex indx = mapFromItem( item ); selection.append(QItemSelectionRange(indx, indx)); setCurrentIndex(indx); } } } selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } } else if (destItem->target() && destItem->project()->buildSystemManager()) { QMenu dropMenu(this); QString seq = QKeySequence( Qt::ControlModifier ).toString(); seq.chop(1); QAction* addToTarget = new QAction(i18n( "&Add to Target" ) + '\t' + seq, &dropMenu); addToTarget->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); dropMenu.addAction(addToTarget); dropMenu.addSeparator(); QAction* cancel = new QAction(i18n( "C&ancel" ) + '\t' + QKeySequence( Qt::Key_Escape ).toString(), &dropMenu); cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); dropMenu.addAction(cancel); QAction *executedAction = nullptr; Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); if (modifiers == Qt::ControlModifier) { executedAction = addToTarget; } else { executedAction = dropMenu.exec(this->mapToGlobal(event->pos())); } if (executedAction == addToTarget) { QList usefulItems = fileItemsWithin(selectionCtxt->items()); filterDroppedItems(usefulItems, destItem); destItem->project()->buildSystemManager()->addFilesToTarget(usefulItems, destItem->target()); } } } event->accept(); } QModelIndex ProjectTreeView::mapFromSource(const QAbstractProxyModel* proxy, const QModelIndex& sourceIdx) { const QAbstractItemModel* next = proxy->sourceModel(); Q_ASSERT(next == sourceIdx.model() || qobject_cast(next)); if(next == sourceIdx.model()) return proxy->mapFromSource(sourceIdx); else { const QAbstractProxyModel* nextProxy = qobject_cast(next); QModelIndex idx = mapFromSource(nextProxy, sourceIdx); Q_ASSERT(idx.model() == nextProxy); return proxy->mapFromSource(idx); } } QModelIndex ProjectTreeView::mapFromItem(const ProjectBaseItem* item) { QModelIndex ret = mapFromSource(qobject_cast(model()), item->index()); Q_ASSERT(ret.model() == model()); return ret; } void ProjectTreeView::slotActivated( const QModelIndex &index ) { if ( QApplication::keyboardModifiers() & Qt::CTRL || QApplication::keyboardModifiers() & Qt::SHIFT ) { // Do not open file when Ctrl or Shift is pressed; that's for selection return; } KDevelop::ProjectBaseItem *item = index.data(ProjectModel::ProjectItemRole).value(); if ( item && item->file() ) { emit activate( item->file()->path() ); } } void ProjectTreeView::projectClosed(KDevelop::IProject* project) { if ( project == m_previousSelection ) m_previousSelection = nullptr; } QList ProjectTreeView::selectedProjects() { QList itemlist; if ( selectionModel()->hasSelection() ) { QModelIndexList indexes = selectionModel()->selectedRows(); for ( const QModelIndex& index: indexes ) { ProjectBaseItem* item = index.data( ProjectModel::ProjectItemRole ).value(); if ( item ) { itemlist << item; m_previousSelection = item->project(); } } } // add previous selection if nothing is selected right now if ( itemlist.isEmpty() && m_previousSelection ) { itemlist << m_previousSelection->projectItem(); } return itemlist; } KDevelop::IProject* ProjectTreeView::getCurrentProject() { auto itemList = selectedProjects(); if ( !itemList.isEmpty() ) { return itemList.at( 0 )->project(); } return nullptr; } void ProjectTreeView::popupContextMenu( const QPoint &pos ) { QList itemlist; if ( indexAt( pos ).isValid() ) { itemlist = selectedProjects(); } QMenu menu( this ); KDevelop::ProjectItemContextImpl context(itemlist); QList extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, &menu); QList buildActions; QList vcsActions; QList analyzeActions; QList extActions; QList projectActions; QList fileActions; QList runActions; foreach( const ContextMenuExtension& ext, extensions ) { buildActions += ext.actions(ContextMenuExtension::BuildGroup); fileActions += ext.actions(ContextMenuExtension::FileGroup); projectActions += ext.actions(ContextMenuExtension::ProjectGroup); vcsActions += ext.actions(ContextMenuExtension::VcsGroup); analyzeActions += ext.actions(ContextMenuExtension::AnalyzeProjectGroup); extActions += ext.actions(ContextMenuExtension::ExtensionGroup); runActions += ext.actions(ContextMenuExtension::RunGroup); } if ( analyzeActions.count() ) { QMenu* analyzeMenu = new QMenu(i18n("Analyze With"), &menu); analyzeMenu->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); foreach( QAction* act, analyzeActions ) { analyzeMenu->addAction( act ); } analyzeActions = {analyzeMenu->menuAction()}; } popupContextMenu_appendActions(menu, buildActions); popupContextMenu_appendActions(menu, runActions ); popupContextMenu_appendActions(menu, fileActions); popupContextMenu_appendActions(menu, vcsActions); popupContextMenu_appendActions(menu, analyzeActions); popupContextMenu_appendActions(menu, extActions); if (itemlist.size() == 1 && itemlist.first()->folder() && !itemlist.first()->folder()->parent()) { QAction* projectConfig = new QAction(i18n("Open Configuration..."), &menu); projectConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect( projectConfig, &QAction::triggered, this, &ProjectTreeView::openProjectConfig ); projectActions << projectConfig; } popupContextMenu_appendActions(menu, projectActions); if ( !menu.isEmpty() ) { menu.exec(viewport()->mapToGlobal(pos)); } } void ProjectTreeView::openProjectConfig() { if ( IProject* project = getCurrentProject() ) { IProjectController* ip = ICore::self()->projectController(); ip->configureProject( project ); } } void ProjectTreeView::saveState( IProject* project ) { // nullptr won't create a usable saved state, so spare the effort if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), QString( settingsConfigGroup ).append( project->name() ) ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.saveState( configGroup ); } void ProjectTreeView::restoreState( IProject* project ) { if ( !project ) { return; } KConfigGroup configGroup( ICore::self()->activeSession()->config(), QString( settingsConfigGroup ).append( project->name() ) ); ProjectModelSaver saver; saver.setProject( project ); saver.setView( this ); saver.restoreState( configGroup ); } void ProjectTreeView::rowsInserted( const QModelIndex& parent, int start, int end ) { QTreeView::rowsInserted( parent, start, end ); if ( !parent.model() ) { for ( const auto& project: selectedProjects() ) { restoreState( project->project() ); } } } void ProjectTreeView::rowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ) { if ( !parent.model() ) { for ( const auto& project: selectedProjects() ) { saveState( project->project() ); } } QTreeView::rowsAboutToBeRemoved( parent, start, end ); } void ProjectTreeView::aboutToShutdown() { // save all projects, not just the selected ones const auto projects = ICore::self()->projectController()->projects(); for ( const auto& project: projects ) { saveState( project ); } } void ProjectTreeView::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Return && currentIndex().isValid() && state()!=QAbstractItemView::EditingState) { event->accept(); slotActivated(currentIndex()); } else QTreeView::keyPressEvent(event); } void ProjectTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { if (WidgetColorizer::colorizeByProject()) { const auto projectPath = index.data(ProjectModel::ProjectRole).value()->path(); const QColor color = WidgetColorizer::colorForId(qHash(projectPath), palette(), true); WidgetColorizer::drawBranches(this, painter, rect, index, color); } QTreeView::drawBranches(painter, rect, index); } diff --git a/plugins/qmakebuilder/qmakejob.cpp b/plugins/qmakebuilder/qmakejob.cpp index 3017558ea0..a65bb51294 100644 --- a/plugins/qmakebuilder/qmakejob.cpp +++ b/plugins/qmakebuilder/qmakejob.cpp @@ -1,123 +1,123 @@ /* KDevelop QMake Support * * Copyright 2006-2007 Andreas Pakulat * Copyright 2008 Hamish Rodda * * 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 "qmakejob.h" #include #include "qmakeconfig.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; QMakeJob::QMakeJob(QObject* parent) : OutputExecuteJob(parent) { setCapabilities(Killable); setFilteringStrategy(OutputModel::CompilerFilter); setProperties(NeedWorkingDirectory | PortableMessages | DisplayStderr | IsBuilderHint); setToolTitle(i18n("QMake")); setStandardToolView(IOutputView::BuildView); setBehaviours(IOutputView::AllowUserClose | IOutputView::AutoScroll ); } void QMakeJob::start() { qCDebug(KDEV_QMAKEBUILDER) << "Running qmake in" << workingDirectory(); if (!m_project) { setError(NoProjectError); setErrorText(i18n("No project specified.")); return emitResult(); } // create build directory if it does not exist yet QDir::temp().mkpath(workingDirectory().toLocalFile()); OutputExecuteJob::start(); } QUrl QMakeJob::workingDirectory() const { if (!m_project) { return QUrl(); } return QMakeConfig::buildDirFromSrc(m_project, m_project->path()).toUrl(); } QStringList QMakeJob::commandLine() const { if (!m_project) { return {}; } - QStringList args; - args << QMakeConfig::qmakeExecutable(m_project); - - args << m_project->path().toUrl().toLocalFile(); + const QStringList args{ + QMakeConfig::qmakeExecutable(m_project), + m_project->path().toUrl().toLocalFile(), + }; return args; } void QMakeJob::setProject(KDevelop::IProject* project) { m_project = project; if (m_project) setObjectName(i18n("QMake: %1", m_project->name())); } void QMakeJob::slotFailed(QProcess::ProcessError error) { qCDebug(KDEV_QMAKEBUILDER) << error; if (!m_killed) { setError(ConfigureError); // FIXME need more detail i guess setErrorText(i18n("Configure error")); } emitResult(); } void QMakeJob::slotCompleted(int code) { if (code != 0) { setError(FailedShownError); } emitResult(); } bool QMakeJob::doKill() { m_killed = true; m_cmd->kill(); return true; } diff --git a/plugins/qmakemanager/qmakefilevisitor.cpp b/plugins/qmakemanager/qmakefilevisitor.cpp index 35eea8cc33..5d65ad8655 100644 --- a/plugins/qmakemanager/qmakefilevisitor.cpp +++ b/plugins/qmakemanager/qmakefilevisitor.cpp @@ -1,248 +1,249 @@ /***************************************************************************** * Copyright (c) 2010 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) 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, see . * *****************************************************************************/ #include "qmakefilevisitor.h" #include "qmakefile.h" #include "qmakeincludefile.h" #include "parser/ast.h" #include #include #include #include #define ifDebug(x) // BEGIN QMakeFileVisitor QMakeFileVisitor::QMakeFileVisitor(const QMakeVariableResolver* resolver, QMakeFile* baseFile) : m_resolver(resolver) , m_baseFile(baseFile) { } QMakeFileVisitor::~QMakeFileVisitor() { } void QMakeFileVisitor::setVariables(const VariableMap& vars) { m_variableValues = vars; } QMakeVariableResolver::VariableMap QMakeFileVisitor::visitFile(QMake::ProjectAST* node) { visitProject(node); return m_variableValues; } QStringList QMakeFileVisitor::visitMacro(QMake::ScopeBodyAST* node, const QStringList& arguments) { m_arguments = arguments; visitScopeBody(node); return m_lastReturn; } QStringList QMakeFileVisitor::resolveVariable(const QString& variable, VariableInfo::VariableType type) const { if (type == VariableInfo::QMakeVariable && m_variableValues.contains(variable)) { return m_variableValues.value(variable); } else { return m_resolver->resolveVariable(variable, type); } } QStringList QMakeFileVisitor::getValueList(const QList& list) const { QStringList result; foreach (QMake::ValueAST* v, list) { result += resolveVariables(v->value); } return result; } void QMakeFileVisitor::visitFunctionCall(QMake::FunctionCallAST* node) { if (node->identifier->value == QLatin1String("include") || node->identifier->value == QLatin1String("!include")) { if (node->args.isEmpty()) return; QStringList arguments = getValueList(node->args); ifDebug(qCDebug(KDEV_QMAKE) << "found include" << node->identifier->value << arguments;) QString argument = arguments.join(QString()).trimmed(); if (!argument.isEmpty() && QFileInfo(argument).isRelative()) { argument = QFileInfo(m_baseFile->absoluteDir() + '/' + argument).canonicalFilePath(); } if (argument.isEmpty()) { qCWarning(KDEV_QMAKE) << "empty include file detected in line" << node->startLine; if (node->identifier->value.startsWith('!')) { visitNode(node->body); } return; } ifDebug(qCDebug(KDEV_QMAKE) << "Reading Include file:" << argument;) QMakeIncludeFile includefile(argument, m_baseFile, m_variableValues); bool read = includefile.read(); ifDebug(qCDebug(KDEV_QMAKE) << "successfully read:" << read;) if (read) { // TODO: optimize by using variableMap and iterator, don't compare values foreach (const QString& var, includefile.variables()) { if (m_variableValues.value(var) != includefile.variableValues(var)) { m_variableValues[var] = includefile.variableValues(var); } } if (!node->identifier->value.startsWith('!')) { visitNode(node->body); } } else if (node->identifier->value.startsWith('!')) { visitNode(node->body); } } else if (node->body && (node->identifier->value == QLatin1String("defineReplace") || node->identifier->value == QLatin1String("defineTest"))) { // TODO: differentiate between replace and test functions? QStringList args = getValueList(node->args); if (!args.isEmpty()) { m_userMacros[args.first()] = node->body; } // TODO: else return error } else if (node->identifier->value == QLatin1String("return")) { m_lastReturn = getValueList(node->args); } else { // TODO: only visit when test function returned true? qCWarning(KDEV_QMAKE) << "unhandled function call" << node->identifier->value; visitNode(node->body); } } void QMakeFileVisitor::visitAssignment(QMake::AssignmentAST* node) { QString op = node->op->value; QStringList values = getValueList(node->values); if (op == QLatin1String("=")) { m_variableValues[node->identifier->value] = values; } else if (op == QLatin1String("+=")) { m_variableValues[node->identifier->value] += values; } else if (op == QLatin1String("-=")) { foreach (const QString& value, values) { m_variableValues[node->identifier->value].removeAll(value); } } else if (op == QLatin1String("*=")) { foreach (const QString& value, values) { if (!m_variableValues.value(node->identifier->value).contains(value)) { m_variableValues[node->identifier->value].append(value); } } } else if (op == QLatin1String("~=")) { if (values.isEmpty()) return; QString value = values.first().trimmed(); QString regex = value.mid(2, value.indexOf(QLatin1Char('/'), 2)); QString replacement = value.mid(value.indexOf(QLatin1Char('/'), 2) + 1, value.lastIndexOf(QLatin1Char('/'))); m_variableValues[node->identifier->value].replaceInStrings(QRegExp(regex), replacement); } } /// return 1-n for numeric variable, 0 otherwise int functionArgument(const QString& var) { bool ok; int arg = var.toInt(&ok); if (!ok) { return 0; } else { return arg; } } QStringList QMakeFileVisitor::resolveVariables(const QString& var) const { VariableReferenceParser parser; parser.setContent(var); if (!parser.parse()) { qCWarning(KDEV_QMAKE) << "Couldn't parse" << var << "to replace variables in it"; return QStringList() << var; } if (parser.variableReferences().isEmpty()) { return QStringList() << var; } /// TODO: multiple vars in one place will make the offsets go bonkers QString value = var; foreach (const QString& variable, parser.variableReferences()) { VariableInfo vi = parser.variableInfo(variable); QString varValue; switch (vi.type) { case VariableInfo::QMakeVariable: if (int arg = functionArgument(variable)) { if (arg > 0 && arg <= m_arguments.size()) { varValue = m_arguments.at(arg - 1); } else { qCWarning(KDEV_QMAKE) << "undefined macro argument:" << variable; } } else { varValue = resolveVariable(variable, vi.type).join(QLatin1Char(' ')); } break; case VariableInfo::ShellVariableResolveQMake: case VariableInfo::ShellVariableResolveMake: /// TODO: make vs qmake time varValue = QProcessEnvironment::systemEnvironment().value(variable); break; case VariableInfo::QtConfigVariable: varValue = resolveVariable(variable, vi.type).join(QLatin1Char(' ')); break; case VariableInfo::FunctionCall: { QStringList arguments; + arguments.reserve(vi.positions.size()); foreach (const VariableInfo::Position& pos, vi.positions) { int start = pos.start + 3 + variable.length(); QString args = value.mid(start, pos.end - start); varValue = resolveVariables(args).join(QLatin1Char(' ')); arguments << varValue; } varValue = evaluateMacro(variable, arguments).join(QLatin1Char(' ')); break; } case VariableInfo::Invalid: qCWarning(KDEV_QMAKE) << "invalid qmake variable:" << variable; continue; } foreach (const VariableInfo::Position& pos, vi.positions) { value.replace(pos.start, pos.end - pos.start + 1, varValue); } } QStringList ret = value.split(QLatin1Char(' '), QString::SkipEmptyParts); ifDebug(qCDebug(KDEV_QMAKE) << "resolved variable" << var << "to" << ret;) return ret; } QStringList QMakeFileVisitor::evaluateMacro(const QString& function, const QStringList& arguments) const { if (function == QLatin1String("qtLibraryTarget")) { return QStringList() << arguments.first(); } /// TODO: support more built-in qmake functions QHash::const_iterator it = m_userMacros.find(function); if (it != m_userMacros.constEnd()) { qCDebug(KDEV_QMAKE) << "calling user macro:" << function << arguments; QMakeFileVisitor visitor(this, m_baseFile); return visitor.visitMacro(it.value(), arguments); } else { qCWarning(KDEV_QMAKE) << "unhandled macro call:" << function << arguments; } return QStringList(); } diff --git a/plugins/qmakemanager/qmakemanager.cpp b/plugins/qmakemanager/qmakemanager.cpp index 7639c90895..79ee664a2f 100644 --- a/plugins/qmakemanager/qmakemanager.cpp +++ b/plugins/qmakemanager/qmakemanager.cpp @@ -1,515 +1,514 @@ /* KDevelop QMake Support * * Copyright 2006 Andreas Pakulat * * 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 "qmakemanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qmakemodelitems.h" #include "qmakeprojectfile.h" #include "qmakecache.h" #include "qmakemkspecs.h" #include "qmakejob.h" #include "qmakebuilddirchooserdialog.h" #include "qmakeconfig.h" #include "qmakeutils.h" #include using namespace KDevelop; // BEGIN Helpers QMakeFolderItem* findQMakeFolderParent(ProjectBaseItem* item) { QMakeFolderItem* p = nullptr; while (!p && item) { p = dynamic_cast(item); item = item->parent(); } return p; } // END Helpers K_PLUGIN_FACTORY_WITH_JSON(QMakeSupportFactory, "kdevqmakemanager.json", registerPlugin();) QMakeProjectManager::QMakeProjectManager(QObject* parent, const QVariantList&) : AbstractFileManagerPlugin(QStringLiteral("kdevqmakemanager"), parent) , IBuildSystemManager() { IPlugin* i = core()->pluginController()->pluginForExtension(QStringLiteral("org.kdevelop.IQMakeBuilder")); Q_ASSERT(i); m_builder = i->extension(); Q_ASSERT(m_builder); connect(this, SIGNAL(folderAdded(KDevelop::ProjectFolderItem*)), this, SLOT(slotFolderAdded(KDevelop::ProjectFolderItem*))); m_runQMake = new QAction(QIcon::fromTheme(QStringLiteral("qtlogo")), i18n("Run QMake"), this); connect(m_runQMake, &QAction::triggered, this, &QMakeProjectManager::slotRunQMake); } QMakeProjectManager::~QMakeProjectManager() { } IProjectFileManager::Features QMakeProjectManager::features() const { return Features(Folders | Targets | Files); } bool QMakeProjectManager::isValid(const Path& path, const bool isFolder, IProject* project) const { if (!isFolder && path.lastPathSegment().startsWith(QLatin1String("Makefile"))) { return false; } return AbstractFileManagerPlugin::isValid(path, isFolder, project); } Path QMakeProjectManager::buildDirectory(ProjectBaseItem* item) const { /// TODO: support includes by some other parent or sibling in a different file-tree-branch QMakeFolderItem* qmakeItem = findQMakeFolderParent(item); Path dir; if (qmakeItem) { if (!qmakeItem->parent()) { // build root item dir = QMakeConfig::buildDirFromSrc(qmakeItem->project(), qmakeItem->path()); } else { // build sub-item foreach (QMakeProjectFile* pro, qmakeItem->projectFiles()) { if (QDir(pro->absoluteDir()) == QFileInfo(qmakeItem->path().toUrl().toLocalFile() + '/').absoluteDir() || pro->hasSubProject(qmakeItem->path().toUrl().toLocalFile())) { // get path from project root and it to buildDir dir = QMakeConfig::buildDirFromSrc(qmakeItem->project(), Path(pro->absoluteDir())); break; } } } } qCDebug(KDEV_QMAKE) << "build dir for" << item->text() << item->path() << "is:" << dir; return dir; } ProjectFolderItem* QMakeProjectManager::createFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent) { if (!parent) { return projectRootItem(project, path); } else if (ProjectFolderItem* buildFolder = buildFolderItem(project, path, parent)) { // child folder in a qmake folder return buildFolder; } else { return AbstractFileManagerPlugin::createFolderItem(project, path, parent); } } ProjectFolderItem* QMakeProjectManager::projectRootItem(IProject* project, const Path& path) { QFileInfo fi(path.toLocalFile()); QDir dir(path.toLocalFile()); auto item = new QMakeFolderItem(project, path); QHash qmvars = QMakeUtils::queryQMake(project); const QString mkSpecFile = QMakeConfig::findBasicMkSpec(qmvars); Q_ASSERT(!mkSpecFile.isEmpty()); QMakeMkSpecs* mkspecs = new QMakeMkSpecs(mkSpecFile, qmvars); mkspecs->setProject(project); mkspecs->read(); QMakeCache* cache = findQMakeCache(project); if (cache) { cache->setMkSpecs(mkspecs); cache->read(); } QStringList projectfiles = dir.entryList(QStringList() << QStringLiteral("*.pro")); for (const auto& projectfile : projectfiles) { Path proPath(path, projectfile); /// TODO: use Path in QMakeProjectFile QMakeProjectFile* scope = new QMakeProjectFile(proPath.toLocalFile()); scope->setProject(project); scope->setMkSpecs(mkspecs); if (cache) { scope->setQMakeCache(cache); } scope->read(); qCDebug(KDEV_QMAKE) << "top-level scope with variables:" << scope->variables(); item->addProjectFile(scope); } return item; } ProjectFolderItem* QMakeProjectManager::buildFolderItem(IProject* project, const Path& path, ProjectBaseItem* parent) { // find .pro or .pri files in dir QDir dir(path.toLocalFile()); - QStringList projectFiles = dir.entryList(QStringList() << QStringLiteral("*.pro") - << QStringLiteral("*.pri"), + QStringList projectFiles = dir.entryList(QStringList{QStringLiteral("*.pro"), QStringLiteral("*.pri")}, QDir::Files); if (projectFiles.isEmpty()) { return nullptr; } auto folderItem = new QMakeFolderItem(project, path, parent); // TODO: included by not-parent file (in a nother file-tree-branch). QMakeFolderItem* qmakeParent = findQMakeFolderParent(parent); if (!qmakeParent) { // happens for bad qmake configurations return nullptr; } foreach (const QString& file, projectFiles) { const QString absFile = dir.absoluteFilePath(file); // TODO: multiple includes by different .pro's QMakeProjectFile* parentPro = nullptr; foreach (QMakeProjectFile* p, qmakeParent->projectFiles()) { if (p->hasSubProject(absFile)) { parentPro = p; break; } } if (!parentPro && file.endsWith(QLatin1String(".pri"))) { continue; } qCDebug(KDEV_QMAKE) << "add project file:" << absFile; if (parentPro) { qCDebug(KDEV_QMAKE) << "parent:" << parentPro->absoluteFile(); } else { qCDebug(KDEV_QMAKE) << "no parent, assume project root"; } auto qmscope = new QMakeProjectFile(absFile); qmscope->setProject(project); const QFileInfo info(absFile); const QDir d = info.dir(); /// TODO: cleanup if (parentPro) { // subdir if (QMakeCache* cache = findQMakeCache(project, Path(d.canonicalPath()))) { cache->setMkSpecs(parentPro->mkSpecs()); cache->read(); qmscope->setQMakeCache(cache); } else { qmscope->setQMakeCache(parentPro->qmakeCache()); } qmscope->setMkSpecs(parentPro->mkSpecs()); } else { // new project QMakeFolderItem* root = dynamic_cast(project->projectItem()); Q_ASSERT(root); qmscope->setMkSpecs(root->projectFiles().first()->mkSpecs()); if (root->projectFiles().first()->qmakeCache()) { qmscope->setQMakeCache(root->projectFiles().first()->qmakeCache()); } } if (qmscope->read()) { // TODO: only on read? folderItem->addProjectFile(qmscope); } else { delete qmscope; return nullptr; } } return folderItem; } void QMakeProjectManager::slotFolderAdded(ProjectFolderItem* folder) { QMakeFolderItem* qmakeParent = dynamic_cast(folder); if (!qmakeParent) { return; } qCDebug(KDEV_QMAKE) << "adding targets for" << folder->path(); foreach (QMakeProjectFile* pro, qmakeParent->projectFiles()) { foreach (const QString& s, pro->targets()) { if (!isValid(Path(folder->path(), s), false, folder->project())) { continue; } qCDebug(KDEV_QMAKE) << "adding target:" << s; Q_ASSERT(!s.isEmpty()); auto target = new QMakeTargetItem(pro, folder->project(), s, folder); foreach (const QString& path, pro->filesForTarget(s)) { new ProjectFileItem(folder->project(), Path(path), target); /// TODO: signal? } } } } ProjectFolderItem* QMakeProjectManager::import(IProject* project) { const Path dirName = project->path(); if (dirName.isRemote()) { // FIXME turn this into a real warning qCWarning(KDEV_QMAKE) << "not a local file. QMake support doesn't handle remote projects"; return nullptr; } QMakeUtils::checkForNeedingConfigure(project); ProjectFolderItem* ret = AbstractFileManagerPlugin::import(project); connect(projectWatcher(project), &KDirWatch::dirty, this, &QMakeProjectManager::slotDirty); return ret; } void QMakeProjectManager::slotDirty(const QString& path) { if (!path.endsWith(QLatin1String(".pro")) && !path.endsWith(QLatin1String(".pri"))) { return; } QFileInfo info(path); if (!info.isFile()) { return; } const QUrl url = QUrl::fromLocalFile(path); if (!isValid(Path(url), false, nullptr)) { return; } IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if (!project) { // this can happen when we create/remove lots of files in a // sub dir of a project - ignore such cases for now return; } bool finished = false; foreach (ProjectFolderItem* folder, project->foldersForPath(IndexedString(KIO::upUrl(url)))) { if (QMakeFolderItem* qmakeFolder = dynamic_cast(folder)) { foreach (QMakeProjectFile* pro, qmakeFolder->projectFiles()) { if (pro->absoluteFile() == path) { // TODO: children // TODO: cache added qCDebug(KDEV_QMAKE) << "reloading" << pro << path; pro->read(); } } finished = true; } else if (ProjectFolderItem* newFolder = buildFolderItem(project, folder->path(), folder->parent())) { qCDebug(KDEV_QMAKE) << "changing from normal folder to qmake project folder:" << folder->path().toUrl(); // .pro / .pri file did not exist before while (folder->rowCount()) { newFolder->appendRow(folder->takeRow(0)); } folder->parent()->removeRow(folder->row()); folder = newFolder; finished = true; } if (finished) { // remove existing targets and readd them for (int i = 0; i < folder->rowCount(); ++i) { if (folder->child(i)->target()) { folder->removeRow(i); } } /// TODO: put into it's own function once we add more stuff to that slot slotFolderAdded(folder); break; } } } QList QMakeProjectManager::targets(ProjectFolderItem* item) const { Q_UNUSED(item) return QList(); } IProjectBuilder* QMakeProjectManager::builder() const { Q_ASSERT(m_builder); return m_builder; } Path::List QMakeProjectManager::collectDirectories(ProjectBaseItem* item, const bool collectIncludes) const { Path::List list; QMakeFolderItem* folder = findQMakeFolderParent(item); if (folder) { foreach (QMakeProjectFile* pro, folder->projectFiles()) { if (pro->files().contains(item->path().toLocalFile())) { const QStringList directories = collectIncludes ? pro->includeDirectories() : pro->frameworkDirectories(); foreach (const QString& dir, directories) { Path path(dir); if (!list.contains(path)) { list << path; } } } } if (list.isEmpty()) { // fallback for new files, use all possible include dirs foreach (QMakeProjectFile* pro, folder->projectFiles()) { const QStringList directories = collectIncludes ? pro->includeDirectories() : pro->frameworkDirectories(); foreach (const QString& dir, directories) { Path path(dir); if (!list.contains(path)) { list << path; } } } } // make sure the base dir is included if (!list.contains(folder->path())) { list << folder->path(); } // qCDebug(KDEV_QMAKE) << "include dirs for" << item->path() << ":" << list; } return list; } Path::List QMakeProjectManager::includeDirectories(ProjectBaseItem* item) const { return collectDirectories(item); } Path::List QMakeProjectManager::frameworkDirectories(ProjectBaseItem* item) const { return collectDirectories(item, false); } QHash QMakeProjectManager::defines(ProjectBaseItem* item) const { QHash d; QMakeFolderItem* folder = findQMakeFolderParent(item); if (!folder) { // happens for bad qmake configurations return d; } foreach (QMakeProjectFile* pro, folder->projectFiles()) { foreach (QMakeProjectFile::DefinePair def, pro->defines()) { d.insert(def.first, def.second); } } return d; } QString QMakeProjectManager::extraArguments(KDevelop::ProjectBaseItem *item) const { QMakeFolderItem* folder = findQMakeFolderParent(item); if (!folder) { // happens for bad qmake configurations return {}; } QStringList d; foreach (QMakeProjectFile* pro, folder->projectFiles()) { d << pro->extraArguments(); } return d.join(QLatin1Char(' ')); } bool QMakeProjectManager::hasBuildInfo(KDevelop::ProjectBaseItem* item) const { return findQMakeFolderParent(item); } QMakeCache* QMakeProjectManager::findQMakeCache(IProject* project, const Path& path) const { QDir curdir(QMakeConfig::buildDirFromSrc(project, !path.isValid() ? project->path() : path).toLocalFile()); curdir.makeAbsolute(); while (!curdir.exists(QStringLiteral(".qmake.cache")) && !curdir.isRoot() && curdir.cdUp()) { qCDebug(KDEV_QMAKE) << curdir; } if (curdir.exists(QStringLiteral(".qmake.cache"))) { qCDebug(KDEV_QMAKE) << "Found QMake cache in " << curdir.absolutePath(); return new QMakeCache(curdir.canonicalPath() + "/.qmake.cache"); } return nullptr; } ContextMenuExtension QMakeProjectManager::contextMenuExtension(Context* context, QWidget* parent) { Q_UNUSED(parent); ContextMenuExtension ext; if (context->hasType(Context::ProjectItemContext)) { ProjectItemContext* pic = dynamic_cast(context); Q_ASSERT(pic); if (pic->items().isEmpty()) { return ext; } m_actionItem = dynamic_cast(pic->items().first()); if (m_actionItem) { ext.addAction(ContextMenuExtension::ProjectGroup, m_runQMake); } } return ext; } void QMakeProjectManager::slotRunQMake() { Q_ASSERT(m_actionItem); Path srcDir = m_actionItem->path(); Path buildDir = QMakeConfig::buildDirFromSrc(m_actionItem->project(), srcDir); QMakeJob* job = new QMakeJob(srcDir.toLocalFile(), buildDir.toLocalFile(), this); job->setQMakePath(QMakeConfig::qmakeExecutable(m_actionItem->project())); KConfigGroup cg(m_actionItem->project()->projectConfiguration(), QMakeConfig::CONFIG_GROUP); QString installPrefix = cg.readEntry(QMakeConfig::INSTALL_PREFIX, QString()); if (!installPrefix.isEmpty()) job->setInstallPrefix(installPrefix); job->setBuildType(cg.readEntry(QMakeConfig::BUILD_TYPE, 0)); job->setExtraArguments(cg.readEntry(QMakeConfig::EXTRA_ARGUMENTS, QString())); KDevelop::ICore::self()->runController()->registerJob(job); } #include "qmakemanager.moc" diff --git a/plugins/qmakemanager/qmakeprojectfile.cpp b/plugins/qmakemanager/qmakeprojectfile.cpp index 7025e59e09..13d2064755 100644 --- a/plugins/qmakemanager/qmakeprojectfile.cpp +++ b/plugins/qmakemanager/qmakeprojectfile.cpp @@ -1,444 +1,445 @@ /* KDevelop QMake Support * * Copyright 2006 Andreas Pakulat * * 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 "qmakeprojectfile.h" #include #include #include #include "debug.h" #include "parser/ast.h" #include "qmakecache.h" #include "qmakemkspecs.h" #include "qmakeconfig.h" #include #include #define ifDebug(x) QHash> QMakeProjectFile::m_qmakeQueryCache; -const QStringList QMakeProjectFile::FileVariables = QStringList() << QStringLiteral("IDLS") - << QStringLiteral("RESOURCES") - << QStringLiteral("IMAGES") - << QStringLiteral("LEXSOURCES") - << QStringLiteral("DISTFILES") - << QStringLiteral("YACCSOURCES") - << QStringLiteral("TRANSLATIONS") - << QStringLiteral("HEADERS") - << QStringLiteral("SOURCES") - << QStringLiteral("INTERFACES") - << QStringLiteral("FORMS"); +const QStringList QMakeProjectFile::FileVariables = QStringList{ + QStringLiteral("IDLS"), + QStringLiteral("RESOURCES"), + QStringLiteral("IMAGES"), + QStringLiteral("LEXSOURCES"), + QStringLiteral("DISTFILES"), + QStringLiteral("YACCSOURCES"), + QStringLiteral("TRANSLATIONS"), + QStringLiteral("HEADERS"), + QStringLiteral("SOURCES"), + QStringLiteral("INTERFACES"), + QStringLiteral("FORMS"), +}; QMakeProjectFile::QMakeProjectFile(const QString& projectfile) : QMakeFile(projectfile) , m_mkspecs(nullptr) , m_cache(nullptr) { } void QMakeProjectFile::setQMakeCache(QMakeCache* cache) { m_cache = cache; } void QMakeProjectFile::setMkSpecs(QMakeMkSpecs* mkspecs) { m_mkspecs = mkspecs; } bool QMakeProjectFile::read() { // default values // NOTE: if we already have such a var, e.g. in an include file, we must not overwrite it here! if (!m_variableValues.contains(QStringLiteral("QT"))) { - m_variableValues[QStringLiteral("QT")] = QStringList() << QStringLiteral("core") - << QStringLiteral("gui"); + m_variableValues[QStringLiteral("QT")] = QStringList{QStringLiteral("core"), QStringLiteral("gui")}; } if (!m_variableValues.contains(QStringLiteral("CONFIG"))) { m_variableValues[QStringLiteral("CONFIG")] = QStringList() << QStringLiteral("qt"); } Q_ASSERT(m_mkspecs); foreach (const QString& var, m_mkspecs->variables()) { if (!m_variableValues.contains(var)) { m_variableValues[var] = m_mkspecs->variableValues(var); } } if (m_cache) { foreach (const QString& var, m_cache->variables()) { if (!m_variableValues.contains(var)) { m_variableValues[var] = m_cache->variableValues(var); } } } /// TODO: more special variables m_variableValues[QStringLiteral("PWD")] = QStringList() << pwd(); m_variableValues[QStringLiteral("_PRO_FILE_")] = QStringList() << proFile(); m_variableValues[QStringLiteral("_PRO_FILE_PWD_")] = QStringList() << proFilePwd(); m_variableValues[QStringLiteral("OUT_PWD")] = QStringList() << outPwd(); const QString qtInstallHeaders = QStringLiteral("QT_INSTALL_HEADERS"); const QString qtVersion = QStringLiteral("QT_VERSION"); const QString qtInstallLibs = QStringLiteral("QT_INSTALL_LIBS"); const QString executable = QMakeConfig::qmakeExecutable(project()); if (!m_qmakeQueryCache.contains(executable)) { const auto queryResult = QMakeConfig::queryQMake(executable, {qtInstallHeaders, qtVersion, qtInstallLibs}); if (queryResult.isEmpty()) { qCWarning(KDEV_QMAKE) << "Failed to query qmake - bad qmake executable configured?" << executable; } m_qmakeQueryCache[executable] = queryResult; } const auto cachedQueryResult = m_qmakeQueryCache.value(executable); m_qtIncludeDir = cachedQueryResult.value(qtInstallHeaders); m_qtVersion = cachedQueryResult.value(qtVersion); m_qtLibDir = cachedQueryResult.value(qtInstallLibs); return QMakeFile::read(); } QStringList QMakeProjectFile::subProjects() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching subprojects";) QStringList list; foreach (QString subdir, variableValues("SUBDIRS")) { QString fileOrPath; ifDebug(qCDebug(KDEV_QMAKE) << "Found value:" << subdir;) if (containsVariable(subdir + ".file") && !variableValues(subdir + ".file").isEmpty()) { subdir = variableValues(subdir + ".file").first(); } else if (containsVariable(subdir + ".subdir") && !variableValues(subdir + ".subdir").isEmpty()) { subdir = variableValues(subdir + ".subdir").first(); } if (subdir.endsWith(QLatin1String(".pro"))) { fileOrPath = resolveToSingleFileName(subdir.trimmed()); } else { fileOrPath = resolveToSingleFileName(subdir.trimmed()); } if (fileOrPath.isEmpty()) { qCWarning(KDEV_QMAKE) << "could not resolve subdir" << subdir << "to file or path, skipping"; continue; } list << fileOrPath; } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "subprojects";) return list; } bool QMakeProjectFile::hasSubProject(const QString& file) const { foreach (const QString& sub, subProjects()) { if (sub == file) { return true; } else if (QFileInfo(file).absoluteDir() == sub) { return true; } } return false; } void QMakeProjectFile::addPathsForVariable(const QString& variable, QStringList* list, const QString& base) const { const QStringList values = variableValues(variable); ifDebug(qCDebug(KDEV_QMAKE) << variable << values;) foreach (const QString& val, values) { QString path = resolveToSingleFileName(val, base); if (!path.isEmpty() && !list->contains(val)) { list->append(path); } } } QStringList QMakeProjectFile::includeDirectories() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching include dirs" << m_qtIncludeDir;) ifDebug(qCDebug(KDEV_QMAKE) << "CONFIG" << variableValues("CONFIG");) QStringList list; addPathsForVariable(QStringLiteral("INCLUDEPATH"), &list); addPathsForVariable(QStringLiteral("QMAKE_INCDIR"), &list); if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("opengl"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_OPENGL"), &list); } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("qt"))) { if (!list.contains(m_qtIncludeDir)) list << m_qtIncludeDir; QDir incDir(m_qtIncludeDir); auto modules = variableValues(QStringLiteral("QT")); if (!modules.isEmpty() && !modules.contains(QStringLiteral("core"))) { // TODO: proper dependency tracking of modules // for now, at least include core if we include any other module modules << QStringLiteral("core"); } // TODO: This is all very fragile, should rather read QMake module .pri files (e.g. qt_lib_core_private.pri) foreach (const QString& module, modules) { QString pattern = module; bool isPrivate = false; if (module.endsWith(QLatin1String("-private"))) { pattern.chop(qstrlen("-private")); isPrivate = true; } else if (module.endsWith(QLatin1String("_private"))) { // _private is less common, but still a valid suffix pattern.chop(qstrlen("_private")); isPrivate = true; } if (pattern == QLatin1String("qtestlib") || pattern == QLatin1String("testlib")) { pattern = QStringLiteral("QtTest"); } else if (pattern == QLatin1String("qaxcontainer")) { pattern = QStringLiteral("ActiveQt"); } else if (pattern == QLatin1String("qaxserver")) { pattern = QStringLiteral("ActiveQt"); } QFileInfoList match = incDir.entryInfoList({QString("Qt%1").arg(pattern)}, QDir::Dirs); if (match.isEmpty()) { // try non-prefixed pattern match = incDir.entryInfoList({pattern}, QDir::Dirs); if (match.isEmpty()) { qCWarning(KDEV_QMAKE) << "unhandled Qt module:" << module << pattern; continue; } } QString path = match.first().canonicalFilePath(); if (isPrivate) { path += '/' + m_qtVersion + '/' + match.first().fileName() + "/private/"; } if (!list.contains(path)) { list << path; } } } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("thread"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_THREAD"), &list); } if (variableValues(QStringLiteral("CONFIG")).contains(QStringLiteral("x11"))) { addPathsForVariable(QStringLiteral("QMAKE_INCDIR_X11"), &list); } addPathsForVariable(QStringLiteral("MOC_DIR"), &list, outPwd()); addPathsForVariable(QStringLiteral("OBJECTS_DIR"), &list, outPwd()); addPathsForVariable(QStringLiteral("UI_DIR"), &list, outPwd()); ifDebug(qCDebug(KDEV_QMAKE) << "final list:" << list;) return list; } // Scan QMAKE_C*FLAGS for -F and -iframework and QMAKE_LFLAGS for good measure. Time will // tell if we need to scan the release/debug/... specific versions of QMAKE_C*FLAGS. // Also include QT_INSTALL_LIBS which corresponds to Qt's framework directory on OS X. QStringList QMakeProjectFile::frameworkDirectories() const { const auto variablesToCheck = {QStringLiteral("QMAKE_CFLAGS"), QStringLiteral("QMAKE_CXXFLAGS"), QStringLiteral("QMAKE_LFLAGS")}; const QLatin1String fOption("-F"); const QLatin1String iframeworkOption("-iframework"); QStringList fwDirs; foreach (const auto& var, variablesToCheck) { bool storeArg = false; foreach (const auto& arg, variableValues(var)) { if (arg == fOption || arg == iframeworkOption) { // detached -F/-iframework arg; set a warrant to store the next argument storeArg = true; } else { if (arg.startsWith(fOption)) { fwDirs << arg.mid(fOption.size()); } else if (arg.startsWith(iframeworkOption)) { fwDirs << arg.mid(iframeworkOption.size()); } else if (storeArg) { fwDirs << arg; } // cancel any outstanding warrants to store the next argument storeArg = false; } } } #ifdef Q_OS_OSX fwDirs << m_qtLibDir; #endif return fwDirs; } QStringList QMakeProjectFile::extraArguments() const { const auto variablesToCheck = {QStringLiteral("QMAKE_CXXFLAGS")}; const auto prefixes = { "-F", "-iframework", "-I", "-D" }; QStringList args; foreach (const auto& var, variablesToCheck) { foreach (const auto& arg, variableValues(var)) { auto argHasPrefix = [arg](const char* prefix) { return arg.startsWith(prefix); }; if ( !std::any_of(prefixes.begin(), prefixes.end(), argHasPrefix)) { args << arg; } } } return args; } QStringList QMakeProjectFile::files() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching files";) QStringList list; foreach (const QString& variable, QMakeProjectFile::FileVariables) { foreach (const QString& value, variableValues(variable)) { list += resolveFileName(value); } } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "files";) return list; } QStringList QMakeProjectFile::filesForTarget(const QString& s) const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching files";) QStringList list; if (variableValues(QStringLiteral("INSTALLS")).contains(s)) { const QStringList files = variableValues(s + ".files"); if (!files.isEmpty()) { foreach (const QString& val, files) { list += QStringList(resolveFileName(val)); } } } if (!variableValues(QStringLiteral("INSTALLS")).contains(s) || s == QLatin1String("target")) { foreach (const QString& variable, QMakeProjectFile::FileVariables) { foreach (const QString& value, variableValues(variable)) { list += QStringList(resolveFileName(value)); } } } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "files";) return list; } QString QMakeProjectFile::getTemplate() const { QString templ = QStringLiteral("app"); if (!variableValues(QStringLiteral("TEMPLATE")).isEmpty()) { templ = variableValues(QStringLiteral("TEMPLATE")).first(); } return templ; } QStringList QMakeProjectFile::targets() const { ifDebug(qCDebug(KDEV_QMAKE) << "Fetching targets";) QStringList list; list += variableValues(QStringLiteral("TARGET")); if (list.isEmpty() && getTemplate() != QLatin1String("subdirs")) { list += QFileInfo(absoluteFile()).baseName(); } foreach (const QString& target, variableValues("INSTALLS")) { if (!target.isEmpty() && target != QLatin1String("target")) list << target; } if (list.removeAll(QString())) { // remove empty targets - which is probably a bug... qCWarning(KDEV_QMAKE) << "got empty entry in TARGET of file" << absoluteFile(); } ifDebug(qCDebug(KDEV_QMAKE) << "found" << list.size() << "targets";) return list; } QMakeProjectFile::~QMakeProjectFile() { // TODO: delete cache, specs, ...? } QStringList QMakeProjectFile::resolveVariable(const QString& variable, VariableInfo::VariableType type) const { if (type == VariableInfo::QtConfigVariable) { if (m_mkspecs->isQMakeInternalVariable(variable)) { return QStringList() << m_mkspecs->qmakeInternalVariable(variable); } else { qCWarning(KDEV_QMAKE) << "unknown QtConfig Variable:" << variable; return QStringList(); } } return QMakeFile::resolveVariable(variable, type); } QMakeMkSpecs* QMakeProjectFile::mkSpecs() const { return m_mkspecs; } QMakeCache* QMakeProjectFile::qmakeCache() const { return m_cache; } QList QMakeProjectFile::defines() const { QList d; foreach (QString def, variableMap()["DEFINES"]) { int pos = def.indexOf('='); if (pos >= 0) { // a value is attached to define d.append(DefinePair(def.left(pos), def.right(def.length() - (pos + 1)))); } else { // a value-less define d.append(DefinePair(def, QString())); } } return d; } QString QMakeProjectFile::pwd() const { return absoluteDir(); } QString QMakeProjectFile::outPwd() const { if (!project()) { return absoluteDir(); } else { return QMakeConfig::buildDirFromSrc(project(), KDevelop::Path(absoluteDir())).toLocalFile(); } } QString QMakeProjectFile::proFile() const { return absoluteFile(); } QString QMakeProjectFile::proFilePwd() const { return absoluteDir(); } diff --git a/plugins/qmljs/codecompletion/context.cpp b/plugins/qmljs/codecompletion/context.cpp index 8ebefa9f54..bb6cba43a3 100644 --- a/plugins/qmljs/codecompletion/context.cpp +++ b/plugins/qmljs/codecompletion/context.cpp @@ -1,503 +1,504 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2013 Sven Brauch * * 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 "context.h" #include "items/modulecompletionitem.h" #include "items/functioncalltipcompletionitem.h" #include #include #include #include #include #include #include #include #include #include "../duchain/expressionvisitor.h" #include "../duchain/helper.h" #include "../duchain/cache.h" #include "../duchain/frameworks/nodejs.h" #include #include using namespace KDevelop; typedef QPair DeclarationDepthPair; namespace QmlJS { CodeCompletionContext::CodeCompletionContext(const DUContextPointer& context, const QString& text, const CursorInRevision& position, int depth) : KDevelop::CodeCompletionContext(context, extractLastLine(text), position, depth), m_completionKind(NormalCompletion) { // Detect "import ..." and provide import completions if (m_text.startsWith(QLatin1String("import "))) { m_completionKind = ImportCompletion; } // Node.js module completions if (m_text.endsWith(QLatin1String("require("))) { m_completionKind = NodeModulesCompletion; } // Detect whether the cursor is in a comment bool isLastLine = true; bool inString = false; for (int index = text.size()-1; index > 0; --index) { const QChar c = text.at(index); const QChar prev = text.at(index - 1); if (c == QLatin1Char('\n')) { isLastLine = false; } else if (isLastLine && prev == QLatin1Char('/') && c == QLatin1Char('/')) { // Single-line comment on the current line, we are in a comment m_completionKind = CommentCompletion; break; } else if (prev == QLatin1Char('/') && c == QLatin1Char('*')) { // Start of a multi-line comment encountered m_completionKind = CommentCompletion; break; } else if (prev == QLatin1Char('*') && c == QLatin1Char('/')) { // End of a multi-line comment. Because /* and */ cannot be nested, // encountering a */ is enough to know that the cursor is outside a // comment break; } else if (prev != QLatin1Char('\\') && (c == QLatin1Char('"') || c == QLatin1Char('\''))) { // Toggle whether we are in a string or not inString = !inString; } } if (inString) { m_completionKind = StringCompletion; } // Some specific constructs don't need any code-completion at all (mainly // because the user will declare new things, not use ones) if (m_text.contains(QRegExp(QLatin1String("(var|function)\\s+$"))) || // "var |" or "function |" m_text.contains(QRegExp(QLatin1String("property\\s+[a-zA-Z0-9_]+\\s+$"))) || // "property |" m_text.contains(QRegExp(QLatin1String("function(\\s+[a-zA-Z0-9_]+)?\\s*\\($"))) || // "function (|" or "function (|" m_text.contains(QRegExp(QLatin1String("id:\\s*"))) // "id: |" ) { m_completionKind = NoCompletion; } } QList CodeCompletionContext::completionItems(bool& abort, bool fullCompletion) { Q_UNUSED (fullCompletion); if (abort) { return QList(); } switch (m_completionKind) { case NormalCompletion: return normalCompletion(); case CommentCompletion: return commentCompletion(); case ImportCompletion: return importCompletion(); case NodeModulesCompletion: return nodeModuleCompletions(); case StringCompletion: case NoCompletion: break; } return QList(); } AbstractType::Ptr CodeCompletionContext::typeToMatch() const { return m_typeToMatch; } QList CodeCompletionContext::normalCompletion() { QList items; QChar lastChar = m_text.size() > 0 ? m_text.at(m_text.size() - 1) : QLatin1Char('\0'); bool inQmlObjectScope = (m_duContext->type() == DUContext::Class); // Start with the function call-tips, because functionCallTips is also responsible // for setting m_declarationForTypeMatch items << functionCallTips(); if (lastChar == QLatin1Char('.') || lastChar == QLatin1Char('[')) { // Offer completions for object members and array subscripts items << fieldCompletions( m_text.left(m_text.size() - 1), lastChar == QLatin1Char('[') ? CompletionItem::QuotesAndBracket : CompletionItem::NoDecoration ); } // "object." must only display the members of object, the declarations // available in the current context. if (lastChar != QLatin1Char('.')) { if (inQmlObjectScope) { DUChainReadLocker lock; // The cursor is in a QML object and there is nothing before it. Display // a list of properties and signals that can be used in a script binding. // Note that the properties/signals of parent QML objects are not displayed here items << completionsInContext(m_duContext, CompletionOnlyLocal | CompletionHideWrappers, CompletionItem::ColonOrBracket); items << completionsFromImports(CompletionHideWrappers); items << completionsInContext(DUContextPointer(m_duContext->topContext()), CompletionHideWrappers, CompletionItem::NoDecoration); } else { items << completionsInContext(m_duContext, CompletionInContextFlags(), CompletionItem::NoDecoration); items << completionsFromImports(CompletionInContextFlags()); items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_ecmascript")); if (!QmlJS::isQmlFile(m_duContext.data())) { items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_dom")); } } } return items; } QList CodeCompletionContext::commentCompletion() { return QList(); } QList CodeCompletionContext::importCompletion() { QList items; QString fragment = m_text.section(QLatin1Char(' '), -1, -1); // Use the cache to find the directory corresponding to the fragment // (org.kde is, for instance, /usr/lib64/kde4/imports/org/kde), and list // its subdirectories QString dataDir = Cache::instance().modulePath(m_duContext->url(), fragment); QDir dir; if (!dataDir.isEmpty()) { dir.setPath(dataDir); const auto dirEntries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); items.reserve(dirEntries.size()); for (const QString& entry : dirEntries) { items.append(CompletionTreeItemPointer(new ModuleCompletionItem( fragment + entry.section(QLatin1Char('.'), 0, 0), ModuleCompletionItem::Import ))); } } return items; } QList CodeCompletionContext::nodeModuleCompletions() { QList items; QDir dir; for (auto path : NodeJS::instance().moduleDirectories(m_duContext->url().str())) { dir.setPath(path.toLocalFile()); for (QString entry : dir.entryList(QDir::Files, QDir::Name)) { entry.remove(QLatin1String(".js")); if (entry.startsWith(QLatin1String("__"))) { // Internal module, don't show continue; } items.append(CompletionTreeItemPointer( new ModuleCompletionItem(entry, ModuleCompletionItem::Quotes) )); } } return items; } QList CodeCompletionContext::functionCallTips() { Stack stack = expressionStack(m_text); QList items; int argumentHintDepth = 1; bool isTopOfStack = true; DUChainReadLocker lock; while (!stack.isEmpty()) { ExpressionStackEntry entry = stack.pop(); if (isTopOfStack && entry.operatorStart > entry.startPosition) { // Deduce the declaration for type matching using operatorStart: // // table[document.base + // [ ^ // // ^ = operatorStart. Just before operatorStart is a code snippet that ends // with the declaration whose type should be used. DeclarationPointer decl = declarationAtEndOfString(m_text.left(entry.operatorStart)); if (decl) { m_typeToMatch = decl->abstractType(); } } if (entry.startPosition > 0 && m_text.at(entry.startPosition - 1) == QLatin1Char('(')) { // The current entry represents a function call, create a call-tip for it DeclarationPointer functionDecl = declarationAtEndOfString(m_text.left(entry.startPosition - 1)); if (functionDecl) { auto item = new FunctionCalltipCompletionItem( functionDecl, argumentHintDepth, entry.commas ); items << CompletionTreeItemPointer(item); argumentHintDepth++; if (isTopOfStack && !m_typeToMatch) { m_typeToMatch = item->currentArgumentType(); } } } isTopOfStack = false; } return items; } QList CodeCompletionContext::completionsFromImports(CompletionInContextFlags flags) { QList items; // Iterate over all the imported namespaces and add their definitions DUChainReadLocker lock; QList imports = m_duContext->findDeclarations(globalImportIdentifier()); QList realImports; foreach (Declaration* import, imports) { if (import->kind() != Declaration::NamespaceAlias) { continue; } NamespaceAliasDeclaration* decl = static_cast(import); realImports << m_duContext->findDeclarations(decl->importIdentifier()); } + items.reserve(realImports.size()); foreach (Declaration* import, realImports) { items << completionsInContext( DUContextPointer(import->internalContext()), flags, CompletionItem::NoDecoration ); } return items; } QList CodeCompletionContext::completionsFromNodeModule(CompletionInContextFlags flags, const QString& module) { return completionsInContext( DUContextPointer(QmlJS::getInternalContext( QmlJS::NodeJS::instance().moduleExports(module, m_duContext->url()) )), flags | CompletionOnlyLocal, CompletionItem::NoDecoration ); } QList CodeCompletionContext::completionsInContext(const DUContextPointer& context, CompletionInContextFlags flags, CompletionItem::Decoration decoration) { QList items; DUChainReadLocker lock; if (context) { const auto declarations = context->allDeclarations( CursorInRevision::invalid(), context->topContext(), !flags.testFlag(CompletionOnlyLocal) ); foreach (const DeclarationDepthPair& decl, declarations) { DeclarationPointer declaration(decl.first); CompletionItem::Decoration decorationOfThisItem = decoration; if (declaration->identifier() == globalImportIdentifier()) { continue; } if (declaration->qualifiedIdentifier().isEmpty()) { continue; } else if (context->owner() && ( context->owner()->kind() == Declaration::Namespace || context->owner()->kind() == Declaration::NamespaceAlias ) && decl.second != 0 && decl.second != 1001) { // Only show the local declarations of modules, or the declarations // immediately in its imported parent contexts (that are global // contexts, hence the distance of 1001). This prevens "String()", // "QtQuick1.0" and "builtins" from being listed when the user // types "PlasmaCore.". continue; } else if (decorationOfThisItem == CompletionItem::NoDecoration && declaration->abstractType() && declaration->abstractType()->whichType() == AbstractType::TypeFunction) { // Decorate function calls with brackets decorationOfThisItem = CompletionItem::Brackets; } else if (flags.testFlag(CompletionHideWrappers)) { ClassDeclaration* classDecl = dynamic_cast(declaration.data()); if (classDecl && classDecl->classType() == ClassDeclarationData::Interface) { continue; } } items << CompletionTreeItemPointer(new CompletionItem(declaration, decl.second, decorationOfThisItem)); } } return items; } QList CodeCompletionContext::fieldCompletions(const QString& expression, CompletionItem::Decoration decoration) { // The statement given to this method ends with an expression that may identify // a declaration ("foo" in "test(1, 2, foo"). List the declarations of this // inner context DUContext* context = getInternalContext(declarationAtEndOfString(expression)); if (context) { return completionsInContext(DUContextPointer(context), CompletionOnlyLocal, decoration); } else { return QList(); } } Stack CodeCompletionContext::expressionStack(const QString& expression) { Stack stack; ExpressionStackEntry entry; QmlJS::Lexer lexer(nullptr); bool atEnd = false; lexer.setCode(expression, 1, false); entry.startPosition = 0; entry.operatorStart = 0; entry.operatorEnd = 0; entry.commas = 0; stack.push(entry); // NOTE: KDevelop uses 0-indexed columns while QMLJS uses 1-indexed columns while (!atEnd) { switch (lexer.lex()) { case QmlJSGrammar::EOF_SYMBOL: atEnd = true; break; case QmlJSGrammar::T_LBRACE: case QmlJSGrammar::T_LBRACKET: case QmlJSGrammar::T_LPAREN: entry.startPosition = lexer.tokenEndColumn() - 1; entry.operatorStart = entry.startPosition; entry.operatorEnd = entry.startPosition; entry.commas = 0; stack.push(entry); break; case QmlJSGrammar::T_RBRACE: case QmlJSGrammar::T_RBRACKET: case QmlJSGrammar::T_RPAREN: if (stack.count() > 1) { stack.pop(); } break; case QmlJSGrammar::T_IDENTIFIER: case QmlJSGrammar::T_DOT: case QmlJSGrammar::T_THIS: break; case QmlJSGrammar::T_COMMA: stack.top().commas++; default: // The last operator of every sub-expression is stored on the stack // so that "A = foo." can know that attributes of foo having the same // type as A should be highlighted. stack.top().operatorStart = lexer.tokenStartColumn() - 1; stack.top().operatorEnd = lexer.tokenEndColumn() - 1; } } return stack; } DeclarationPointer CodeCompletionContext::declarationAtEndOfString(const QString& expression) { // Build the expression stack of expression and use the valid portion of the // top sub-expression to find the right-most declaration that can be found // in expression. QmlJS::Document::MutablePtr doc = QmlJS::Document::create(QStringLiteral("inline"), Dialect::JavaScript); ExpressionStackEntry topEntry = expressionStack(expression).top(); doc->setSource(expression.mid(topEntry.operatorEnd)); doc->parseExpression(); if (!doc || !doc->isParsedCorrectly()) { return DeclarationPointer(); } // Use ExpressionVisitor to find the type (and associated declaration) of // the snippet that has been parsed. The inner context of the declaration // can be used to get the list of completions ExpressionVisitor visitor(m_duContext.data()); doc->ast()->accept(&visitor); return visitor.lastDeclaration(); } bool CodeCompletionContext::containsOnlySpaces(const QString& str) { for (int i=0; i * * 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 "completionitem.h" #include "context.h" #include #include #include #include #include #include #include #include #include #include #include "../../duchain/functiontype.h" using namespace QmlJS; using namespace KDevelop; CompletionItem::CompletionItem(const DeclarationPointer& decl, int inheritanceDepth, Decoration decoration) : NormalDeclarationCompletionItem(decl, QExplicitlySharedDataPointer(), inheritanceDepth), m_decoration(decoration) { } QVariant CompletionItem::data(const QModelIndex& index, int role, const CodeCompletionModel* model) const { DUChainReadLocker lock; Declaration* decl = declaration().data(); if (!decl) { return QVariant(); } ClassDeclaration* classDecl = dynamic_cast(decl); StructureType::Ptr declType = StructureType::Ptr::dynamicCast(decl->abstractType()); auto funcType = QmlJS::FunctionType::Ptr::dynamicCast(decl->abstractType()); if (role == CodeCompletionModel::BestMatchesCount) { return 5; } else if (role == CodeCompletionModel::MatchQuality) { AbstractType::Ptr referenceType = static_cast(model->completionContext().data())->typeToMatch(); if (!referenceType) { return QVariant(); } AbstractType::Ptr declType = decl->abstractType(); if (!declType) { return QVariant(); } QmlJS::FunctionType::Ptr declFunc = QmlJS::FunctionType::Ptr::dynamicCast(declType); if (declType->equals(referenceType.constData())) { // Perfect type match return QVariant(10); } else if (declFunc && declFunc->returnType() && declFunc->returnType()->equals(referenceType.constData())) { // Also very nice: a function returning the proper type return QVariant(9); } else { // Completely different types, no luck return QVariant(); } } else if (role == Qt::DisplayRole && funcType) { // Functions are displayed using the "type funcName(arg, arg, arg...)" format Declaration* funcDecl = funcType->declaration(decl->topContext()); if (funcDecl) { switch (index.column()) { case CodeCompletionModel::Prefix: return funcType->returnType()->toString(); case CodeCompletionModel::Name: // Return the identifier of the declaration being listed, not of its // function declaration (because the function may have been declared // anonymously, even if it has been assigned to a variable) return decl->identifier().toString(); case CodeCompletionModel::Arguments: { QStringList args; const auto localDeclarations = funcDecl->internalContext()->localDeclarations(); args.reserve(localDeclarations.size()); for (auto* arg : localDeclarations) { args.append(arg->toString()); } return QStringLiteral("(%1)").arg(args.join(QStringLiteral(", "))); } } } } else if (role == Qt::DisplayRole && index.column() == CodeCompletionModel::Prefix) { if (classDecl) { if (classDecl->classType() == ClassDeclarationData::Class) { // QML component return QStringLiteral("component"); } else if (classDecl->classType() == ClassDeclarationData::Interface) { // C++-ish QML component return QStringLiteral("wrapper"); } } if (decl && ( decl->kind() == Declaration::NamespaceAlias || decl->kind() == Declaration::Namespace )) { // Display namespaces and namespace aliases as modules return QStringLiteral("module"); } if (decl && decl->abstractType() && decl->kind() == Declaration::Type && decl->abstractType()->whichType() == AbstractType::TypeEnumeration) { // Enum return QStringLiteral("enum"); } if (declType && decl->kind() == Declaration::Instance && declType->declarationId().qualifiedIdentifier().isEmpty()) { // QML component instance. The type that should be displayed is the // base class of its anonymous class ClassDeclaration* anonymousClass = dynamic_cast(declType->declaration(decl->topContext())); if (anonymousClass && anonymousClass->baseClassesSize() > 0) { return anonymousClass->baseClasses()[0].baseClass.abstractType()->toString(); } } } return NormalDeclarationCompletionItem::data(index, role, model); } QString CompletionItem::declarationName() const { ClassFunctionDeclaration* classFuncDecl = dynamic_cast(declaration().data()); if (classFuncDecl && classFuncDecl->isSignal() && m_decoration == QmlJS::CompletionItem::ColonOrBracket) { // Signals, when completed in a QML component context, are transformed into slots QString signal = classFuncDecl->identifier().toString(); if (signal.size() > 0) { return QLatin1String("on") + signal.at(0).toUpper() + signal.mid(1); } } return NormalDeclarationCompletionItem::declarationName(); } CodeCompletionModel::CompletionProperties CompletionItem::completionProperties() const { DUChainReadLocker lock; // Variables having a function type should have a function icon. FunctionDeclarations // are skipped here because they are already handled properly by completionProperties() if (declaration() && declaration()->abstractType() && !declaration()->isFunctionDeclaration() && declaration()->abstractType()->whichType() == AbstractType::TypeFunction) { return CodeCompletionModel::Function; } // Put declarations in a context owned by a namespace in the namespace scope auto properties = NormalDeclarationCompletionItem::completionProperties(); if (declaration() && declaration()->context() && declaration()->context()->owner() && ( declaration()->context()->owner()->kind() == Declaration::Namespace || declaration()->context()->type() == DUContext::Enum )) { properties &= ~(CodeCompletionModel::LocalScope | CodeCompletionModel::GlobalScope | CodeCompletionModel::Public); properties |= CodeCompletionModel::NamespaceScope; } return properties; } void CompletionItem::execute(KTextEditor::View* view, const KTextEditor::Range& word) { KTextEditor::Document* document = view->document(); QString base = declarationName(); switch (m_decoration) { case QmlJS::CompletionItem::NoDecoration: document->replaceText(word, base); break; case QmlJS::CompletionItem::Quotes: - document->replaceText(word, "\"" + base + "\""); + document->replaceText(word, '\"' + base + '\"'); break; case QmlJS::CompletionItem::QuotesAndBracket: - document->replaceText(word, "\"" + base + "\"]"); + document->replaceText(word, '\"' + base + "\"]"); break; case QmlJS::CompletionItem::ColonOrBracket: if (declaration() && declaration()->abstractType() && declaration()->abstractType()->whichType() == AbstractType::TypeStructure) { document->replaceText(word, base + " {}"); } else { document->replaceText(word, base + ": "); } break; case QmlJS::CompletionItem::Brackets: document->replaceText(word, base + "()"); } } diff --git a/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp b/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp index 500f463d5e..d4adb6cc9e 100644 --- a/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp +++ b/plugins/qmljs/codecompletion/items/functioncalltipcompletionitem.cpp @@ -1,181 +1,182 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2014 Denis Steckelmacher * * 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 "functioncalltipcompletionitem.h" #include "../../duchain/helper.h" #include "../../duchain/functiontype.h" #include #include #include #include using namespace KDevelop; using namespace QmlJS; FunctionCalltipCompletionItem::FunctionCalltipCompletionItem(const DeclarationPointer& decl, int depth, int argumentIndex) : m_declaration(decl), m_depth(depth) { // Ensure that decl has a function type if (!decl) { return; } QmlJS::FunctionType::Ptr func = QmlJS::FunctionType::Ptr::dynamicCast(decl->abstractType()); if (!func) { return; } // Arguments can be fetch from the function declaration (if available), or // from its function type Declaration* funcDecl = func->declaration(decl->topContext()); DUContext* argsContext = (funcDecl ? funcDecl->internalContext() : nullptr); QStringList arguments; if (argsContext) { auto args = argsContext->allDeclarations(CursorInRevision::invalid(), decl->topContext(), false); arguments.reserve(args.size()); for (auto pair : args) { arguments.append(pair.first->toString()); } if (argumentIndex < args.count()) { m_currentArgumentType = args.at(argumentIndex).first->abstractType(); } } else { const auto args = func->arguments(); arguments.reserve(args.size()); for (auto type : args) { arguments.append(type->toString()); } if (argumentIndex < func->arguments().count()) { m_currentArgumentType = func->arguments().at(argumentIndex); } } // [type] functionName if (func->returnType()) { - m_prefix = func->returnType()->toString() + QLatin1String(" "); + m_prefix = func->returnType()->toString() + QLatin1Char(' '); } m_prefix += decl->identifier().toString(); // (arg1, arg2, [currentArgument in m_currentArgument], arg4, arg5) - m_arguments = QStringLiteral("("); + m_arguments = QLatin1Char('('); for (int i=0; i 0) { m_arguments += QLatin1String(", "); } if (i == argumentIndex) { m_currentArgumentStart = m_arguments.length(); m_currentArgumentLength = arguments.at(i).length(); } m_arguments += arguments.at(i); } - m_arguments += QLatin1String(")"); + m_arguments += QLatin1Char(')'); } AbstractType::Ptr FunctionCalltipCompletionItem::currentArgumentType() const { return m_currentArgumentType; } QVariant FunctionCalltipCompletionItem::data(const QModelIndex& index, int role, const CodeCompletionModel* model) const { Q_UNUSED(model) switch (role) { case Qt::DisplayRole: switch (index.column()) { case CodeCompletionModel::Prefix: return m_prefix; case CodeCompletionModel::Arguments: return m_arguments; } break; case CodeCompletionModel::ArgumentHintDepth: return argumentHintDepth(); case CodeCompletionModel::CompletionRole: return (int)completionProperties(); case CodeCompletionModel::HighlightingMethod: if (index.column() == CodeCompletionModel::Arguments) { return (int)CodeCompletionModel::CustomHighlighting; } break; case CodeCompletionModel::CustomHighlight: if (index.column() == CodeCompletionModel::Arguments) { QTextFormat format; format.setBackground(QBrush(QColor::fromRgb(142, 186, 255))); // Same color as kdev-python format.setProperty(QTextFormat::FontWeight, 99); - return QVariantList() - << m_currentArgumentStart - << m_currentArgumentLength - << format; + return QVariantList{ + m_currentArgumentStart, + m_currentArgumentLength, + format, + }; } break; case Qt::DecorationRole: if (index.column() == CodeCompletionModel::Prefix) { return DUChainUtils::iconForProperties(completionProperties()); } break; } return QVariant(); } DeclarationPointer FunctionCalltipCompletionItem::declaration() const { return m_declaration; } int FunctionCalltipCompletionItem::argumentHintDepth() const { return m_depth; } int FunctionCalltipCompletionItem::inheritanceDepth() const { return 0; } CodeCompletionModel::CompletionProperties FunctionCalltipCompletionItem::completionProperties() const { return CodeCompletionModel::Function; } diff --git a/plugins/qmljs/duchain/cache.cpp b/plugins/qmljs/duchain/cache.cpp index acf9ee94e5..227905f2e2 100644 --- a/plugins/qmljs/duchain/cache.cpp +++ b/plugins/qmljs/duchain/cache.cpp @@ -1,262 +1,262 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2013 Sven Brauch * Copyright (c) 2014 Denis Steckelmacher * * 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 "cache.h" #include "debug.h" #include #include #include #include #include #include QmlJS::Cache::Cache() { // qmlplugindump from Qt4 and Qt5. They will be tried in order when dumping // a binary QML file. m_pluginDumpExecutables << PluginDumpExecutable(QStringLiteral("qmlplugindump"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt4"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt5"), QStringLiteral("2.0")) << PluginDumpExecutable(QStringLiteral("qml1plugindump-qt5"), QStringLiteral("1.0")); } QmlJS::Cache& QmlJS::Cache::instance() { static Cache *c = nullptr; if (!c) { c = new Cache(); } return *c; } QString QmlJS::Cache::modulePath(const KDevelop::IndexedString& baseFile, const QString& uri, const QString& version) { QMutexLocker lock(&m_mutex); QString cacheKey = uri + version; QString path = m_modulePaths.value(cacheKey, QString()); if (!path.isEmpty()) { return path; } // List of the paths in which the modules will be looked for KDevelop::Path::List paths; for (auto path : QCoreApplication::instance()->libraryPaths()) { KDevelop::Path p(path); // Change /path/to/qt5/plugins to /path/to/qt5/{qml,imports} paths << p.cd(QStringLiteral("../qml")); paths << p.cd(QStringLiteral("../imports")); } paths << m_includeDirs[baseFile]; // Find the path for which /u/r/i exists QString fragment = QString(uri).replace(QLatin1Char('.'), QDir::separator()); bool isVersion1 = version.startsWith(QLatin1String("1.")); bool isQtQuick = (uri == QLatin1String("QtQuick")); const QStringList modulesWithoutVersionSuffix{"QtQml", "QtMultimedia", "QtQuick.LocalStorage", "QtQuick.XmlListModel"}; if (!version.isEmpty() && !isVersion1 && !modulesWithoutVersionSuffix.contains(uri)) { // Modules having a version greater or equal to 2 are stored in a directory // name like QtQuick.2 fragment += QLatin1Char('.') + version.section(QLatin1Char('.'), 0, 0); } for (auto p : paths) { QString pathString = p.cd(fragment).path(); // HACK: QtQuick 1.0 is put in $LIB/qt5/imports/builtins.qmltypes. The "QtQuick" // identifier appears nowhere. if (isQtQuick && isVersion1) { if (QFile::exists(p.cd(QStringLiteral("builtins.qmltypes")).path())) { path = p.path(); break; } } else if (QFile::exists(pathString + QLatin1String("/plugins.qmltypes"))) { path = pathString; break; } } m_modulePaths.insert(cacheKey, path); return path; } QStringList QmlJS::Cache::getFileNames(const QFileInfoList& fileInfos) { QStringList result; for (const QFileInfo& fileInfo : fileInfos) { QString filePath = fileInfo.canonicalFilePath(); // If the module directory contains a plugins.qmltypes files, use it // and skip everything else if (filePath.endsWith(QLatin1String("plugins.qmltypes"))) { return QStringList() << filePath; } else if (fileInfo.dir().exists(QLatin1String("plugins.qmltypes"))) { return {fileInfo.dir().filePath(QLatin1String("plugins.qmltypes"))}; } // Non-so files don't need any treatment if (!filePath.endsWith(QLatin1String(".so"))) { result.append(filePath); continue; } // Use the cache to speed-up reparses { QMutexLocker lock(&m_mutex); if (m_modulePaths.contains(filePath)) { QString cachedFilePath = m_modulePaths.value(filePath); if (!cachedFilePath.isEmpty()) { result.append(cachedFilePath); } continue; } } // Locate an existing dump of the file QString dumpFile = QStringLiteral("kdevqmljssupport/%1.qml").arg( QString::fromLatin1(QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Md5).toHex()) ); QString dumpPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dumpFile ); if (!dumpPath.isEmpty()) { QMutexLocker lock(&m_mutex); result.append(dumpPath); m_modulePaths.insert(filePath, dumpPath); continue; } // Create a dump of the file const QStringList args = {QStringLiteral("-noinstantiate"), QStringLiteral("-path"), filePath}; for (const PluginDumpExecutable& executable : m_pluginDumpExecutables) { QProcess qmlplugindump; qmlplugindump.setProcessChannelMode(QProcess::SeparateChannels); qmlplugindump.start(executable.executable, args, QIODevice::ReadOnly); qCDebug(KDEV_QMLJS_DUCHAIN) << "starting qmlplugindump with args:" << executable.executable << args << qmlplugindump.state() << fileInfo.absolutePath(); if (!qmlplugindump.waitForFinished(3000)) { if (qmlplugindump.state() == QProcess::Running) { qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump didn't finish in time -- killing"; qmlplugindump.kill(); qmlplugindump.waitForFinished(100); } else { qCDebug(KDEV_QMLJS_DUCHAIN) << "qmlplugindump attempt failed" << qmlplugindump.program() << qmlplugindump.arguments() << qmlplugindump.readAllStandardError(); } continue; } if (qmlplugindump.exitCode() != 0) { qCWarning(KDEV_QMLJS_DUCHAIN) << "qmlplugindump finished with exit code:" << qmlplugindump.exitCode(); continue; } // Open a file in which the dump can be written QFile dumpFile( QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + dumpPath ); if (dumpFile.open(QIODevice::WriteOnly)) { qmlplugindump.readLine(); // Skip "import QtQuick.tooling 1.1" - dumpFile.write("// " + filePath.toUtf8() + "\n"); - dumpFile.write("import QtQuick " + executable.quickVersion.toUtf8() + "\n"); + dumpFile.write("// " + filePath.toUtf8() + '\n'); + dumpFile.write("import QtQuick " + executable.quickVersion.toUtf8() + '\n'); dumpFile.write(qmlplugindump.readAllStandardOutput()); dumpFile.close(); result.append(dumpFile.fileName()); QMutexLocker lock(&m_mutex); m_modulePaths.insert(filePath, dumpFile.fileName()); break; } } } return result; } void QmlJS::Cache::setFileCustomIncludes(const KDevelop::IndexedString& file, const KDevelop::Path::List& dirs) { QMutexLocker lock(&m_mutex); m_includeDirs[file] = dirs; } void QmlJS::Cache::addDependency(const KDevelop::IndexedString& file, const KDevelop::IndexedString& dependency) { QMutexLocker lock(&m_mutex); m_dependees[dependency].insert(file); m_dependencies[file].insert(dependency); } QList QmlJS::Cache::filesThatDependOn(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependees[file].toList(); } QList QmlJS::Cache::dependencies(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependencies[file].toList(); } bool QmlJS::Cache::isUpToDate(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_isUpToDate.value(file, false); } void QmlJS::Cache::setUpToDate(const KDevelop::IndexedString& file, bool upToDate) { QMutexLocker lock(&m_mutex); m_isUpToDate[file] = upToDate; } diff --git a/plugins/qmljs/duchain/declarationbuilder.cpp b/plugins/qmljs/duchain/declarationbuilder.cpp index c0029bc09f..31273f48b4 100644 --- a/plugins/qmljs/duchain/declarationbuilder.cpp +++ b/plugins/qmljs/duchain/declarationbuilder.cpp @@ -1,1542 +1,1542 @@ /************************************************************************************* * Copyright (C) 2012 by Aleix Pol * * Copyright (C) 2012 by 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) 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 "declarationbuilder.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include "expressionvisitor.h" #include "parsesession.h" #include "functiondeclaration.h" #include "functiontype.h" #include "helper.h" #include "cache.h" #include "frameworks/nodejs.h" #include #include #include using namespace KDevelop; DeclarationBuilder::DeclarationBuilder(ParseSession* session) : m_prebuilding(false) { m_session = session; } ReferencedTopDUContext DeclarationBuilder::build(const IndexedString& url, QmlJS::AST::Node* node, ReferencedTopDUContext updateContext) { Q_ASSERT(m_session->url() == url); // The declaration builder needs to run twice, so it can resolve uses of e.g. functions // which are called before they are defined (which is easily possible, due to JS's dynamic nature). if (!m_prebuilding) { qCDebug(KDEV_QMLJS_DUCHAIN) << "building, but running pre-builder first"; auto prebuilder = new DeclarationBuilder(m_session); prebuilder->m_prebuilding = true; updateContext = prebuilder->build(url, node, updateContext); qCDebug(KDEV_QMLJS_DUCHAIN) << "pre-builder finished"; delete prebuilder; if (!m_session->allDependenciesSatisfied()) { qCDebug(KDEV_QMLJS_DUCHAIN) << "dependencies were missing, don't perform the second parsing pass"; return updateContext; } } else { qCDebug(KDEV_QMLJS_DUCHAIN) << "prebuilding"; } return DeclarationBuilderBase::build(url, node, updateContext); } void DeclarationBuilder::startVisiting(QmlJS::AST::Node* node) { DUContext* builtinQmlContext = nullptr; if (QmlJS::isQmlFile(currentContext()) && !currentContext()->url().str().contains(QLatin1String("__builtin_qml.qml"))) { builtinQmlContext = m_session->contextOfFile( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/nodejsmodules/__builtin_qml.qml")) ); } { DUChainWriteLocker lock; // Remove all the imported parent contexts: imports may have been edited // and there musn't be any leftover parent context currentContext()->topContext()->clearImportedParentContexts(); // Initialize Node.js QmlJS::NodeJS::instance().initialize(this); // Built-in QML types (color, rect, etc) if (builtinQmlContext) { topContext()->addImportedParentContext(builtinQmlContext); } } DeclarationBuilderBase::startVisiting(node); } /* * Functions */ template void DeclarationBuilder::declareFunction(QmlJS::AST::Node* node, bool newPrototypeContext, const Identifier& name, const RangeInRevision& nameRange, QmlJS::AST::Node* parameters, const RangeInRevision& parametersRange, QmlJS::AST::Node* body, const RangeInRevision& bodyRange) { setComment(node); // Declare the function QmlJS::FunctionType::Ptr func(new QmlJS::FunctionType); Decl* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, nameRange); decl->setKind(Declaration::Type); func->setDeclaration(decl); decl->setType(func); } openType(func); // Parameters, if any (a function must always have an internal function context, // so always open a context here even if there are no parameters) DUContext* parametersContext = openContext( node + 1, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, bodyRange.end), // Ensure that this context contains both the parameters and the body DUContext::Function, QualifiedIdentifier(name) ); if (parameters) { QmlJS::AST::Node::accept(parameters, this); } // The internal context of the function is its parameter context { DUChainWriteLocker lock; decl->setInternalContext(parametersContext); } // Open the prototype context, if any. This has to be done before the body // because this context is needed for "this" to be properly resolved // in it. if (newPrototypeContext) { DUChainWriteLocker lock; QmlJS::FunctionDeclaration* d = reinterpret_cast(decl); d->setPrototypeContext(openContext( node + 2, // Don't call setContextOnNode on node, only the body context can be associated with node RangeInRevision(parametersRange.start, parametersRange.start), DUContext::Function, // This allows QmlJS::getOwnerOfContext to know that the parent of this context is the function declaration QualifiedIdentifier(name) )); if (name != Identifier(QStringLiteral("Object"))) { // Every class inherit from Object QmlJS::importObjectContext(currentContext(), topContext()); } closeContext(); } // Body, if any (it is a child context of the parameters) openContext( node, bodyRange, DUContext::Other, QualifiedIdentifier(name) ); if (body) { QmlJS::AST::Node::accept(body, this); } // Close the body context and then the parameters context closeContext(); closeContext(); } template void DeclarationBuilder::declareParameters(Node* node, QStringRef Node::*typeAttribute) { for (Node *plist = node; plist; plist = plist->next) { const Identifier name(plist->name.toString()); const RangeInRevision range = m_session->locationToRange(plist->identifierToken); AbstractType::Ptr type = (typeAttribute ? typeFromName((plist->*typeAttribute).toString()) : // The typeAttribute attribute of plist contains the type name of the argument AbstractType::Ptr(new IntegralType(IntegralType::TypeMixed)) // No type information, use mixed ); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); closeAndAssignType(); if (QmlJS::FunctionType::Ptr funType = currentType()) { funType->addArgument(type); } } } bool DeclarationBuilder::visit(QmlJS::AST::FunctionDeclaration* node) { declareFunction( node, true, // A function declaration always has its own prototype context Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FunctionExpression* node) { declareFunction( node, false, Identifier(), QmlJS::emptyRangeOnLine(node->functionToken), node->formals, m_session->locationsToRange(node->lparenToken, node->rparenToken), node->body, m_session->locationsToRange(node->lbraceToken, node->rbraceToken) ); return false; } bool DeclarationBuilder::visit(QmlJS::AST::FormalParameterList* node) { declareParameters(node, (QStringRef QmlJS::AST::FormalParameterList::*)nullptr); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiParameterList* node) { declareParameters(node, &QmlJS::AST::UiParameterList::type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::ReturnStatement* node) { if (QmlJS::FunctionType::Ptr func = currentType()) { AbstractType::Ptr returnType; if (node->expression) { returnType = findType(node->expression).type; } else { returnType = new IntegralType(IntegralType::TypeVoid); } DUChainWriteLocker lock; func->setReturnType(QmlJS::mergeTypes(func->returnType(), returnType)); } return false; // findType has already explored node } void DeclarationBuilder::endVisitFunction() { QmlJS::FunctionType::Ptr func = currentType(); if (func && !func->returnType()) { // A function that returns nothing returns void DUChainWriteLocker lock; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); } closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionDeclaration* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } void DeclarationBuilder::endVisit(QmlJS::AST::FunctionExpression* node) { DeclarationBuilderBase::endVisit(node); endVisitFunction(); } /* * Variables */ void DeclarationBuilder::inferArgumentsFromCall(QmlJS::AST::Node* base, QmlJS::AST::ArgumentList* arguments) { ContextBuilder::ExpressionType expr = findType(base); QmlJS::FunctionType::Ptr func_type = QmlJS::FunctionType::Ptr::dynamicCast(expr.type); DUChainWriteLocker lock; if (!func_type) { return; } auto func_declaration = dynamic_cast(func_type->declaration(topContext())); if (!func_declaration || !func_declaration->internalContext()) { return; } // Put the argument nodes in a list that has a definite size QVector argumentDecls = func_declaration->internalContext()->localDeclarations(); QVector args; for (auto argument = arguments; argument; argument = argument->next) { args.append(argument); } // Don't update a function when it is called with the wrong number // of arguments if (args.size() != argumentDecls.count()) { return; } // Update the types of the function arguments QmlJS::FunctionType::Ptr new_func_type(new QmlJS::FunctionType); for (int i=0; iabstractType(); // Merge the current type of the argument with its type in the call expression AbstractType::Ptr call_type = findType(argument->expression).type; AbstractType::Ptr new_type = QmlJS::mergeTypes(current_type, call_type); // Update the declaration of the argument and its type in the function type if (func_declaration->topContext() == topContext()) { new_func_type->addArgument(new_type); argumentDecls.at(i)->setAbstractType(new_type); } // Add a warning if it is possible that the argument types don't match if (!m_prebuilding && !areTypesEqual(current_type, call_type)) { m_session->addProblem(argument, i18n( "Possible type mismatch between the argument type (%1) and the value passed as argument (%2)", current_type->toString(), call_type->toString() ), IProblem::Hint); } } // Replace the function's type with the new type having updated arguments if (func_declaration->topContext() == topContext()) { new_func_type->setReturnType(func_type->returnType()); new_func_type->setDeclaration(func_declaration); func_declaration->setAbstractType(new_func_type.cast()); if (expr.declaration) { // expr.declaration is the variable that contains the function, while // func_declaration is the declaration of the function. They can be // different and both need to be updated expr.declaration->setAbstractType(new_func_type.cast()); } } return; } bool DeclarationBuilder::visit(QmlJS::AST::VariableDeclaration* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); const Identifier name(node->name.toString()); const RangeInRevision range = m_session->locationToRange(node->identifierToken); const AbstractType::Ptr type = findType(node->expression).type; { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); return false; // findType has already explored node } void DeclarationBuilder::endVisit(QmlJS::AST::VariableDeclaration* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } bool DeclarationBuilder::visit(QmlJS::AST::BinaryExpression* node) { if (node->op == QSOperator::Assign) { ExpressionType leftType = findType(node->left); ExpressionType rightType = findType(node->right); DUChainWriteLocker lock; if (leftType.declaration) { DUContext* leftCtx = leftType.declaration->context(); DUContext* leftInternalCtx = QmlJS::getInternalContext(leftType.declaration); // object.prototype.method = function(){} : when assigning a function // to a variable living in a Class context, set the prototype // context of the function to the context of the variable if (rightType.declaration && leftCtx->type() == DUContext::Class) { auto func = rightType.declaration.dynamicCast(); if (!QmlJS::getOwnerOfContext(leftCtx) && !leftCtx->importers().isEmpty()) { // MyClass.prototype.myfunc declares "myfunc" in a small context // that is imported by MyClass. The prototype of myfunc should // be the context of MyClass, not the small context in which // it has been declared leftCtx = leftCtx->importers().at(0); } if (func && !func->prototypeContext()) { func->setPrototypeContext(leftCtx); } } if (leftType.declaration->topContext() != topContext()) { // Do not modify a declaration of another file } else if (leftType.isPrototype && leftInternalCtx) { // Assigning something to a prototype is equivalent to making it // inherit from a class: "Class.prototype = ClassOrObject;" leftInternalCtx->clearImportedParentContexts(); QmlJS::importDeclarationInContext( leftInternalCtx, rightType.declaration ); } else { // Merge the already-known type of the variable with the new one leftType.declaration->setAbstractType(QmlJS::mergeTypes(leftType.type, rightType.type)); } } return false; // findType has already explored node } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::CallExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } bool DeclarationBuilder::visit(QmlJS::AST::NewMemberExpression* node) { inferArgumentsFromCall(node->base, node->arguments); return false; } /* * Arrays */ void DeclarationBuilder::declareFieldMember(const KDevelop::DeclarationPointer& declaration, const QString& member, QmlJS::AST::Node* node, const QmlJS::AST::SourceLocation& location) { if (QmlJS::isPrototypeIdentifier(member)) { // Don't declare "prototype", this is a special member return; } if (!m_session->allDependenciesSatisfied()) { // Don't declare anything automatically if dependencies are missing: the // checks hereafter may pass now but fail later, thus causing disappearing // declarations return; } DUChainWriteLocker lock; Identifier identifier(member); // Declaration must have an internal context so that the member can be added // into it. DUContext* ctx = QmlJS::getInternalContext(declaration); if (!ctx || ctx->topContext() != topContext()) { return; } // No need to re-declare a field if it already exists // TODO check if we can make getDeclaration receive an Identifier directly if (QmlJS::getDeclaration(QualifiedIdentifier(identifier), ctx, false)) { return; } // The internal context of declaration is already closed and does not contain // location. This can be worked around by opening a new context, declaring the // new field in it, and then adding the context as a parent of // declaration->internalContext(). RangeInRevision range = m_session->locationToRange(location); IntegralType::Ptr type = IntegralType::Ptr(new IntegralType(IntegralType::TypeMixed)); DUContext* importedContext = openContext(node, range, DUContext::Class); Declaration* decl = openDeclaration(identifier, range); decl->setInSymbolTable(false); // This declaration is in an anonymous context, and the symbol table acts as if the declaration was in the global context openType(type); closeAndAssignType(); closeContext(); ctx->addImportedParentContext(importedContext); } bool DeclarationBuilder::visit(QmlJS::AST::FieldMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, node->name.toString(), node, node->identifierToken ); } return false; // findType has already visited node->base } bool DeclarationBuilder::visit(QmlJS::AST::ArrayMemberExpression* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // When the user types array["new_key"], declare "new_key" as a new field of // array. auto stringLiteral = QmlJS::AST::cast(node->expression); if (!stringLiteral) { return DeclarationBuilderBase::visit(node); } ExpressionType type = findType(node->base); if (type.declaration) { declareFieldMember( type.declaration, stringLiteral->value.toString(), node, stringLiteral->literalToken ); } node->expression->accept(this); return false; // findType has already visited node->base, and we have just visited node->expression } bool DeclarationBuilder::visit(QmlJS::AST::ObjectLiteral* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); // Object literals can appear in the "values" property of enumerations. Their // keys must be declared in the enumeration, not in an anonymous class if (currentContext()->type() == DUContext::Enum) { return DeclarationBuilderBase::visit(node); } // Open an anonymous class declaration, with its internal context StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; ClassDeclaration* decl = openDeclaration( Identifier(), QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->setKind(Declaration::Type); decl->setInternalContext(openContext( node, m_session->locationsToRange(node->lbraceToken, node->rbraceToken), DUContext::Class )); type->setDeclaration(decl); // Every object literal inherits from Object QmlJS::importObjectContext(currentContext(), topContext()); } openType(type); return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::PropertyNameAndValue* node) { setComment(node); if (!node->name || !node->value) { return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->name->propertyNameToken)); Identifier name(QmlJS::getNodeValue(node->name)); // The type of the declaration can either be an enumeration value or the type // of its expression ExpressionType type; bool inSymbolTable = false; if (currentContext()->type() == DUContext::Enum) { // This is an enumeration value auto value = QmlJS::AST::cast(node->value); EnumeratorType::Ptr enumerator(new EnumeratorType); enumerator->setDataType(IntegralType::TypeInt); if (value) { enumerator->setValue((int)value->value); } type.type = AbstractType::Ptr::staticCast(enumerator); type.declaration = nullptr; inSymbolTable = true; } else { // Normal value type = findType(node->value); } // If a function is assigned to an object member, set the prototype context // of the function to the object containing the member if (type.declaration) { DUChainWriteLocker lock; auto func = type.declaration.dynamicCast(); if (func && !func->prototypeContext()) { func->setPrototypeContext(currentContext()); } } // Open the declaration { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setInSymbolTable(inSymbolTable); } openType(type.type); return false; // findType has already explored node->expression } void DeclarationBuilder::endVisit(QmlJS::AST::PropertyNameAndValue* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } void DeclarationBuilder::endVisit(QmlJS::AST::ObjectLiteral* node) { DeclarationBuilderBase::endVisit(node); if (currentContext()->type() != DUContext::Enum) { // Enums are special-cased in visit(ObjectLiteral) closeContext(); closeAndAssignType(); } } /* * plugins.qmltypes files */ void DeclarationBuilder::declareComponent(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QString baseClass = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("prototype")).value.section('/', -1, -1); // Declare the component itself StructureType::Ptr type(new StructureType); ClassDeclaration* decl; { DUChainWriteLocker lock; decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Interface); decl->clearBaseClasses(); if (!baseClass.isEmpty()) { addBaseClass(decl, baseClass); } type->setDeclaration(decl); decl->setType(type); // declareExports needs to know the type of decl } openType(type); } void DeclarationBuilder::declareMethod(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name, bool isSlot, bool isSignal) { QString type_name = QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value; QmlJS::FunctionType::Ptr type(new QmlJS::FunctionType); if (type_name.isEmpty()) { type->setReturnType(typeFromName(QStringLiteral("void"))); } else { type->setReturnType(typeFromName(type_name)); } { DUChainWriteLocker lock; ClassFunctionDeclaration* decl = openDeclaration(name, range); decl->setIsSlot(isSlot); decl->setIsSignal(isSignal); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareProperty(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setAbstractType(type); } openType(type); } void DeclarationBuilder::declareParameter(QmlJS::AST::UiObjectInitializer* node, const RangeInRevision &range, const Identifier &name) { QmlJS::FunctionType::Ptr function = currentType(); AbstractType::Ptr type = typeFromName(QmlJS::getQMLAttributeValue(node->members, QStringLiteral("type")).value); Q_ASSERT(function); function->addArgument(type); { DUChainWriteLocker lock; openDeclaration(name, range); } openType(type); } void DeclarationBuilder::declareEnum(const RangeInRevision &range, const Identifier &name) { EnumerationType::Ptr type(new EnumerationType); { DUChainWriteLocker lock; ClassMemberDeclaration* decl = openDeclaration(name, range); decl->setKind(Declaration::Type); decl->setType(type); // The type needs to be set here because closeContext is called before closeAndAssignType and needs to know the type of decl type->setDataType(IntegralType::TypeEnumeration); type->setDeclaration(decl); } openType(type); } void DeclarationBuilder::declareComponentSubclass(QmlJS::AST::UiObjectInitializer* node, const KDevelop::RangeInRevision& range, const QString& baseclass, QmlJS::AST::UiQualifiedId* qualifiedId) { Identifier name( QmlJS::getQMLAttributeValue(node->members, QStringLiteral("name")).value.section('/', -1, -1) ); DUContext::ContextType contextType = DUContext::Class; if (baseclass == QLatin1String("Component")) { // QML component, equivalent to a QML class declareComponent(node, range, name); } else if (baseclass == QLatin1String("Method") || baseclass == QLatin1String("Signal") || baseclass == QLatin1String("Slot")) { // Method (that can also be a signal or a slot) declareMethod(node, range, name, baseclass == QLatin1String("Slot"), baseclass == QLatin1String("Signal")); contextType = DUContext::Function; } else if (baseclass == QLatin1String("Property")) { // A property declareProperty(node, range, name); } else if (baseclass == QLatin1String("Parameter") && currentType()) { // One parameter of a signal/slot/method declareParameter(node, range, name); } else if (baseclass == QLatin1String("Enum")) { // Enumeration. The "values" key contains a dictionary of name -> number entries. declareEnum(range, name); contextType = DUContext::Enum; name = Identifier(); // Enum contexts should have no name so that their members have the correct scope } else { // Define an anonymous subclass of the baseclass. This subclass will // be instantiated when "id:" is encountered name = Identifier(); // Use ExpressionVisitor to find the declaration of the base class DeclarationPointer baseClass = findType(qualifiedId).declaration; StructureType::Ptr type(new StructureType); { DUChainWriteLocker lock; ClassDeclaration* decl = openDeclaration( currentContext()->type() == DUContext::Global ? Identifier(m_session->moduleName()) : name, QmlJS::emptyRangeOnLine(node->lbraceToken) ); decl->clearBaseClasses(); decl->setKind(Declaration::Type); decl->setType(type); // The class needs to know its type early because it contains definitions that depend on that type type->setDeclaration(decl); if (baseClass) { addBaseClass(decl, baseClass->indexedType()); } } openType(type); } // Open a context of the proper type and identifier openContext( node, m_session->locationsToInnerRange(node->lbraceToken, node->rbraceToken), contextType, QualifiedIdentifier(name) ); DUContext* ctx = currentContext(); Declaration* decl = currentDeclaration(); { // Set the inner context of the current declaration, because nested classes // need to know the inner context of their parents DUChainWriteLocker lock; decl->setInternalContext(ctx); if (contextType == DUContext::Enum) { ctx->setPropagateDeclarations(true); } } // If we have have declared a class, import the context of its base classes registerBaseClasses(); } void DeclarationBuilder::declareComponentInstance(QmlJS::AST::ExpressionStatement* expression) { if (!expression) { return; } auto identifier = QmlJS::AST::cast(expression->expression); if (!identifier) { return; } { DUChainWriteLocker lock; injectContext(topContext()); Declaration* decl = openDeclaration( Identifier(identifier->name.toString()), m_session->locationToRange(identifier->identifierToken) ); closeInjectedContext(); // Put the declaration in the global scope decl->setKind(Declaration::Instance); decl->setType(currentAbstractType()); } closeDeclaration(); } DeclarationBuilder::ExportLiteralsAndNames DeclarationBuilder::exportedNames(QmlJS::AST::ExpressionStatement* exports) { ExportLiteralsAndNames res; if (!exports) { return res; } auto exportslist = QmlJS::AST::cast(exports->expression); if (!exportslist) { return res; } // Explore all the exported symbols for this component and keep only those // having a version compatible with the one of this module QSet knownNames; for (auto it = exportslist->elements; it && it->expression; it = it->next) { auto stringliteral = QmlJS::AST::cast(it->expression); if (!stringliteral) { continue; } // String literal like "Namespace/Class version". QStringList nameAndVersion = stringliteral->value.toString().section('/', -1, -1).split(' '); QString name = nameAndVersion.at(0); QString version = (nameAndVersion.count() > 1 ? nameAndVersion.at(1) : QStringLiteral("1.0")); if (!knownNames.contains(name)) { knownNames.insert(name); res.append(qMakePair(stringliteral, name)); } } return res; } void DeclarationBuilder::declareExports(const ExportLiteralsAndNames& exports, ClassDeclaration* classdecl) { DUChainWriteLocker lock; // Create the exported versions of the component for (auto exp : exports) { QmlJS::AST::StringLiteral* literal = exp.first; QString name = exp.second; StructureType::Ptr type(new StructureType); injectContext(currentContext()->parentContext()); // Don't declare the export in its C++-ish component, but in the scope above ClassDeclaration* decl = openDeclaration( Identifier(name), m_session->locationToRange(literal->literalToken) ); closeInjectedContext(); // The exported version inherits from the C++ component decl->setKind(Declaration::Type); decl->setClassType(ClassDeclarationData::Class); decl->clearBaseClasses(); type->setDeclaration(decl); addBaseClass(decl, classdecl->indexedType()); // Open a context for the exported class, and register its base class in it decl->setInternalContext(openContext( literal, DUContext::Class, QualifiedIdentifier(name) )); registerBaseClasses(); closeContext(); openType(type); closeAndAssignType(); } } /* * UI */ void DeclarationBuilder::importDirectory(const QString& directory, QmlJS::AST::UiImport* node) { DUChainWriteLocker lock; QString currentFilePath = currentContext()->topContext()->url().str(); QFileInfo dir(directory); QFileInfoList entries; if (dir.isDir()) { // Import all the files in the given directory entries = QDir(directory).entryInfoList( - QStringList() - << (QLatin1String("*.") + currentFilePath.section(QLatin1Char('.'), -1, -1)) - << QStringLiteral("*.qmltypes") - << QStringLiteral("*.so"), + QStringList{ + (QLatin1String("*.") + currentFilePath.section(QLatin1Char('.'), -1, -1)), + QStringLiteral("*.qmltypes"), + QStringLiteral("*.so")}, QDir::Files ); } else if (dir.isFile()) { // Import the specific file given in the import statement entries.append(dir); } else if (!m_prebuilding) { m_session->addProblem(node, i18n("Module not found, some types or properties may not be recognized")); return; } // Translate the QFileInfos into QStrings (and replace .so files with // qmlplugindump dumps) lock.unlock(); QStringList filePaths = QmlJS::Cache::instance().getFileNames(entries); lock.lock(); if (node && !node->importId.isEmpty()) { // Open a namespace that will contain the declarations Identifier identifier(node->importId.toString()); RangeInRevision range = m_session->locationToRange(node->importIdToken); Declaration* decl = openDeclaration(identifier, range); decl->setKind(Declaration::Namespace); decl->setInternalContext(openContext(node, range, DUContext::Class, QualifiedIdentifier(identifier))); } for (const QString& filePath : filePaths) { if (filePath == currentFilePath) { continue; } ReferencedTopDUContext context = m_session->contextOfFile(filePath); if (context) { currentContext()->addImportedParentContext(context.data()); } } if (node && !node->importId.isEmpty()) { // Close the namespace containing the declarations closeContext(); closeDeclaration(); } } void DeclarationBuilder::importModule(QmlJS::AST::UiImport* node) { QmlJS::AST::UiQualifiedId *part = node->importUri; QString uri; while (part) { if (!uri.isEmpty()) { uri.append('.'); } uri.append(part->name.toString()); part = part->next; } // Version of the import QString version = m_session->symbolAt(node->versionToken); // Import the directory containing the module QString modulePath = QmlJS::Cache::instance().modulePath(m_session->url(), uri, version); importDirectory(modulePath, node); } bool DeclarationBuilder::visit(QmlJS::AST::UiImport* node) { if (node->importUri) { importModule(node); } else if (!node->fileName.isEmpty() && node->fileName != QLatin1String(".")) { QUrl currentFileUrl = currentContext()->topContext()->url().toUrl(); QUrl importUrl = QUrl(node->fileName.toString()); importDirectory(currentFileUrl.resolved(importUrl).toLocalFile(), node); } return DeclarationBuilderBase::visit(node); } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectDefinition* node) { setComment(node); // Do not crash if the user has typed an empty object definition if (!node->initializer || !node->initializer->members) { m_skipEndVisit.push(true); return DeclarationBuilderBase::visit(node); } RangeInRevision range(m_session->locationToRange(node->qualifiedTypeNameId->identifierToken)); QString baseclass = node->qualifiedTypeNameId->name.toString(); // "Component" needs special care: a component that appears only in a future // version of this module, or that already appeared in a former version, must // be skipped because it is useless ExportLiteralsAndNames exports; if (baseclass == QLatin1String("Component")) { QmlJS::AST::Statement* statement = QmlJS::getQMLAttribute(node->initializer->members, QStringLiteral("exports")); exports = exportedNames(QmlJS::AST::cast(statement)); if (statement && exports.count() == 0) { // This component has an "exports:" member but no export matched // the version of this module. Skip the component m_skipEndVisit.push(true); return false; } } else if (baseclass == QLatin1String("Module")) { // "Module" is disabled. This allows the declarations of a module // dump to appear in the same namespace as the .qml files in the same // directory. m_skipEndVisit.push(true); return true; } // Declare the component subclass declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); // If we had a component with exported names, declare these exports if (baseclass == QLatin1String("Component")) { ClassDeclaration* classDecl = currentDeclaration(); if (classDecl) { declareExports(exports, classDecl); } } m_skipEndVisit.push(false); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectDefinition* node) { DeclarationBuilderBase::endVisit(node); // Do not crash if the user has typed an empty object definition if (!m_skipEndVisit.pop()) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiScriptBinding* node) { setComment(node); if (!node->qualifiedId) { return DeclarationBuilderBase::visit(node); } // Special-case some binding names QString bindingName = node->qualifiedId->name.toString(); if (bindingName == QLatin1String("id")) { // Instantiate a QML component: its type is the current type (the anonymous // QML class that surrounds the declaration) declareComponentInstance(QmlJS::AST::cast(node->statement)); } // Use ExpressionVisitor to find the signal/property bound DeclarationPointer bindingDecl = findType(node->qualifiedId).declaration; DUChainPointer signal; // If a Javascript block is used as expression or if the script binding is a // slot, open a subcontext so that variables declared in the binding are kept // local, and the signal parameters can be visible to the slot if (( bindingDecl && (signal = bindingDecl.dynamicCast()) && signal->isSignal() ) || node->statement->kind == QmlJS::AST::Node::Kind_Block) { openContext( node->statement, m_session->locationsToInnerRange( node->statement->firstSourceLocation(), node->statement->lastSourceLocation() ), DUContext::Other ); // If this script binding is a slot, import the parameters of its signal if (signal && signal->isSignal() && signal->internalContext()) { DUChainWriteLocker lock; currentContext()->addIndirectImport(DUContext::Import( signal->internalContext(), nullptr )); } } else { // Check that the type of the value matches the type of the property AbstractType::Ptr expressionType = findType(node->statement).type; DUChainReadLocker lock; if (!m_prebuilding && bindingDecl && !areTypesEqual(bindingDecl->abstractType(), expressionType)) { m_session->addProblem(node->qualifiedId, i18n( "Mismatch between the value type (%1) and the property type (%2)", expressionType->toString(), bindingDecl->abstractType()->toString() ), IProblem::Error); } } return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiScriptBinding* node) { QmlJS::AST::Visitor::endVisit(node); // If visit(UiScriptBinding) has opened a context, close it if (currentContext()->type() == DUContext::Other) { closeContext(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiObjectBinding* node) { setComment(node); if (!node->qualifiedId || !node->qualifiedTypeNameId || !node->initializer) { return DeclarationBuilderBase::visit(node); } // Declare the component subclass. "Behavior on ... {}" is treated exactly // like "Behavior {}". RangeInRevision range = m_session->locationToRange(node->qualifiedTypeNameId->identifierToken); QString baseclass = node->qualifiedTypeNameId->name.toString(); declareComponentSubclass(node->initializer, range, baseclass, node->qualifiedTypeNameId); return DeclarationBuilderBase::visit(node); } void DeclarationBuilder::endVisit(QmlJS::AST::UiObjectBinding* node) { DeclarationBuilderBase::endVisit(node); if (node->qualifiedId && node->qualifiedTypeNameId && node->initializer) { closeContext(); closeAndAssignType(); } } bool DeclarationBuilder::visit(QmlJS::AST::UiPublicMember* node) { setComment(node); RangeInRevision range = m_session->locationToRange(node->identifierToken); Identifier id(node->name.toString()); QString typeName = node->memberType.toString(); bool res = DeclarationBuilderBase::visit(node); // Build the type of the public member if (node->type == QmlJS::AST::UiPublicMember::Signal) { // Open a function declaration corresponding to this signal declareFunction( node, false, Identifier(node->name.toString()), m_session->locationToRange(node->identifierToken), node->parameters, m_session->locationToRange(node->identifierToken), // The AST does not provide the location of the parens nullptr, m_session->locationToRange(node->identifierToken) // A body range must be provided ); // This declaration is a signal and its return type is void { DUChainWriteLocker lock; currentDeclaration()->setIsSignal(true); currentType()->setReturnType(typeFromName(QStringLiteral("void"))); } } else { AbstractType::Ptr type; if (typeName == QLatin1String("alias")) { // Property aliases take the type of their aliased property type = findType(node->statement).type; res = false; // findType has already explored node->statement } else { type = typeFromName(typeName); if (node->typeModifier == QLatin1String("list")) { // QML list, noted "list" in the source file ArrayType::Ptr array(new ArrayType); array->setElementType(type); type = array.cast(); } } { DUChainWriteLocker lock; Declaration* decl = openDeclaration(id, range); decl->setInSymbolTable(false); } openType(type); } return res; } void DeclarationBuilder::endVisit(QmlJS::AST::UiPublicMember* node) { DeclarationBuilderBase::endVisit(node); closeAndAssignType(); } /* * Utils */ void DeclarationBuilder::setComment(QmlJS::AST::Node* node) { setComment(m_session->commentForLocation(node->firstSourceLocation()).toUtf8()); } void DeclarationBuilder::closeAndAssignType() { closeType(); Declaration* dec = currentDeclaration(); Q_ASSERT(dec); if (auto type = lastType()) { DUChainWriteLocker lock; dec->setType(type); } closeDeclaration(); } AbstractType::Ptr DeclarationBuilder::typeFromName(const QString& name) { auto type = IntegralType::TypeNone; QString realName = name; // Built-in types if (name == QLatin1String("string")) { type = IntegralType::TypeString; } else if (name == QLatin1String("bool")) { type = IntegralType::TypeBoolean; } else if (name == QLatin1String("int")) { type = IntegralType::TypeInt; } else if (name == QLatin1String("float")) { type = IntegralType::TypeFloat; } else if (name == QLatin1String("double") || name == QLatin1String("real")) { type = IntegralType::TypeDouble; } else if (name == QLatin1String("void")) { type = IntegralType::TypeVoid; } else if (name == QLatin1String("var") || name == QLatin1String("variant")) { type = IntegralType::TypeMixed; } else if (m_session->language() == QmlJS::Dialect::Qml) { // In QML files, some Qt type names need to be renamed to the QML equivalent if (name == QLatin1String("QFont")) { realName = QStringLiteral("Font"); } else if (name == QLatin1String("QColor")) { realName = QStringLiteral("color"); } else if (name == QLatin1String("QDateTime")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QDate")) { realName = QStringLiteral("date"); } else if (name == QLatin1String("QTime")) { realName = QStringLiteral("time"); } else if (name == QLatin1String("QRect") || name == QLatin1String("QRectF")) { realName = QStringLiteral("rect"); } else if (name == QLatin1String("QPoint") || name == QLatin1String("QPointF")) { realName = QStringLiteral("point"); } else if (name == QLatin1String("QSize") || name == QLatin1String("QSizeF")) { realName = QStringLiteral("size"); } else if (name == QLatin1String("QUrl")) { realName = QStringLiteral("url"); } else if (name == QLatin1String("QVector3D")) { realName = QStringLiteral("vector3d"); } else if (name.endsWith(QLatin1String("ScriptString"))) { // Q{Declarative,Qml}ScriptString represents a JS snippet auto func = new QmlJS::FunctionType; func->setReturnType(AbstractType::Ptr(new IntegralType(IntegralType::TypeVoid))); return AbstractType::Ptr(func); } } if (type == IntegralType::TypeNone) { // Not a built-in type, but a class return typeFromClassName(realName); } else { return AbstractType::Ptr(new IntegralType(type)); } } AbstractType::Ptr DeclarationBuilder::typeFromClassName(const QString& name) { DeclarationPointer decl = QmlJS::getDeclaration(QualifiedIdentifier(name), currentContext()); if (!decl) { if (name == QLatin1String("QRegExp")) { decl = QmlJS::NodeJS::instance().moduleMember(QStringLiteral("__builtin_ecmascript"), QStringLiteral("RegExp"), currentContext()->url()); } } if (decl) { return decl->abstractType(); } else { DelayedType::Ptr type(new DelayedType); type->setKind(DelayedType::Unresolved); type->setIdentifier(IndexedTypeIdentifier(name)); return type; } } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const QString& name) { addBaseClass(classDecl, IndexedType(typeFromClassName(name))); } void DeclarationBuilder::addBaseClass(ClassDeclaration* classDecl, const IndexedType& type) { BaseClassInstance baseClass; baseClass.access = Declaration::Public; baseClass.virtualInheritance = false; baseClass.baseClass = type; classDecl->addBaseClass(baseClass); } void DeclarationBuilder::registerBaseClasses() { ClassDeclaration* classdecl = currentDeclaration(); DUContext *ctx = currentContext(); if (classdecl) { DUChainWriteLocker lock; for (uint i=0; ibaseClassesSize(); ++i) { const BaseClassInstance &baseClass = classdecl->baseClasses()[i]; StructureType::Ptr baseType = StructureType::Ptr::dynamicCast(baseClass.baseClass.abstractType()); TopDUContext* topctx = topContext(); if (baseType && baseType->declaration(topctx)) { QmlJS::importDeclarationInContext(ctx, DeclarationPointer(baseType->declaration(topctx))); } } } } static bool enumContainsEnumerator(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { Q_ASSERT(a->whichType() == AbstractType::TypeEnumeration); auto aEnum = EnumerationType::Ptr::staticCast(a); Q_ASSERT(b->whichType() == AbstractType::TypeEnumerator); auto bEnumerator = EnumeratorType::Ptr::staticCast(b); return bEnumerator->qualifiedIdentifier().beginsWith(aEnum->qualifiedIdentifier()); } static bool isNumeric(const IntegralType::Ptr& type) { return type->dataType() == IntegralType::TypeInt || type->dataType() == IntegralType::TypeIntegral || type->dataType() == IntegralType::TypeFloat || type->dataType() == IntegralType::TypeDouble; } bool DeclarationBuilder::areTypesEqual(const AbstractType::Ptr& a, const AbstractType::Ptr& b) { if (!a || !b) { return true; } if (a->whichType() == AbstractType::TypeUnsure || b->whichType() == AbstractType::TypeUnsure) { // Don't try to guess something if one of the types is unsure return true; } const auto bIntegral = IntegralType::Ptr::dynamicCast(b); if (bIntegral && (bIntegral->dataType() == IntegralType::TypeString || bIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, a string can be converted to nearly everything else, similarly ignore mixed types return true; } const auto aIntegral = IntegralType::Ptr::dynamicCast(a); if (aIntegral && (aIntegral->dataType() == IntegralType::TypeString || aIntegral->dataType() == IntegralType::TypeMixed)) { // In QML/JS, nearly everything can be to a string, similarly ignore mixed types return true; } if (aIntegral && bIntegral) { if (isNumeric(aIntegral) && isNumeric(bIntegral)) { // Casts between integral types is possible return true; } } if (a->whichType() == AbstractType::TypeEnumeration && b->whichType() == AbstractType::TypeEnumerator) { return enumContainsEnumerator(a, b); } else if (a->whichType() == AbstractType::TypeEnumerator && b->whichType() == AbstractType::TypeEnumeration) { return enumContainsEnumerator(b, a); } { auto aId = dynamic_cast(a.constData()); auto bId = dynamic_cast(b.constData()); if (aId && bId && aId->qualifiedIdentifier() == bId->qualifiedIdentifier()) return true; } { auto aStruct = StructureType::Ptr::dynamicCast(a); auto bStruct = StructureType::Ptr::dynamicCast(b); if (aStruct && bStruct) { auto top = currentContext()->topContext(); auto aDecl = dynamic_cast(aStruct->declaration(top)); auto bDecl = dynamic_cast(bStruct->declaration(top)); if (aDecl && bDecl) { if (aDecl->isPublicBaseClass(bDecl, top) || bDecl->isPublicBaseClass(aDecl, top)) { return true; } } } } return a->equals(b.constData()); } diff --git a/plugins/qmljs/duchain/frameworks/nodejs.cpp b/plugins/qmljs/duchain/frameworks/nodejs.cpp index c26f226de1..1a33a3eca8 100644 --- a/plugins/qmljs/duchain/frameworks/nodejs.cpp +++ b/plugins/qmljs/duchain/frameworks/nodejs.cpp @@ -1,215 +1,216 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2014 Denis Steckelmacher * * 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 "nodejs.h" #include "../helper.h" #include "../parsesession.h" #include #include #include #include #include #include #include #include using namespace KDevelop; namespace QmlJS { NodeJS::NodeJS() { } NodeJS& NodeJS::instance() { static NodeJS* i = nullptr; if (!i) { i = new NodeJS(); } return *i; } void NodeJS::initialize(DeclarationBuilder* builder) { QMutexLocker lock(&m_mutex); // Create "module", a structure that may contain "exports" if the module // refers to module.exports createObject(QStringLiteral("module"), 1, builder); // Create "exports", that can also contain the exported symbols of the module createObject(QStringLiteral("exports"), 2, builder); } void NodeJS::createObject(const QString& name, int index, DeclarationBuilder* builder) { Identifier identifier(name); StructureType::Ptr type(new StructureType); Declaration* decl = builder->openDeclaration(identifier, RangeInRevision()); type->setDeclaration(decl); decl->setAlwaysForceDirect(true); decl->setKind(Declaration::Type); // Not exactly what the user would expect, but this ensures that QmlJS::getInternalContext does not recurse infinitely decl->setInternalContext(builder->openContext( (QmlJS::AST::Node*)nullptr + index, // Index is used to disambiguate the contexts. "node" is never dereferenced and is only stored in a hash table RangeInRevision(), DUContext::Class, QualifiedIdentifier(identifier) )); builder->closeContext(); builder->openType(type); builder->closeAndAssignType(); } DeclarationPointer NodeJS::moduleExports(const QString& moduleName, const IndexedString& url) { QString urlStr = url.str(); QString fileName = moduleFileName(moduleName, urlStr); DeclarationPointer exports; if (fileName.isEmpty() || urlStr.contains(QLatin1String("__builtin_ecmascript.js")) || urlStr == fileName) { return exports; } ReferencedTopDUContext topContext = ParseSession::contextOfFile(fileName, url, 0); DUChainReadLocker lock; if (topContext) { static const QualifiedIdentifier idModule(QStringLiteral("module")); static const QualifiedIdentifier idExports(QStringLiteral("exports")); // Try "module.exports". If this declaration exists, it contains the // module's exports exports = getDeclaration(idModule, topContext.data()); if (exports && exports->internalContext()) { exports = getDeclaration(idExports, exports->internalContext(), false); } // Try "exports", that always exist, has a structure type, and contains // the exported symbols if (!exports) { exports = getDeclaration(idExports, topContext.data()); } } return exports; } DeclarationPointer NodeJS::moduleMember(const QString& moduleName, const QString& memberName, const IndexedString& url) { DeclarationPointer module = moduleExports(moduleName, url); DeclarationPointer member; if (module) { member = QmlJS::getDeclaration( QualifiedIdentifier(memberName), QmlJS::getInternalContext(module), false ); } return member; } Path::List NodeJS::moduleDirectories(const QString& url) { Path::List paths; // QML/JS ships several modules that exist only in binary form in Node.js QStringList dirs = QStandardPaths::locateAll( QStandardPaths::GenericDataLocation, QStringLiteral("kdevqmljssupport/nodejsmodules"), QStandardPaths::LocateDirectory ); + paths.reserve(dirs.size()); for (auto dir : dirs) { paths.append(Path(dir)); } // url/../node_modules, then url/../../node_modules, etc Path path(url); path.addPath(QStringLiteral("..")); const int maxPathSize = path.isLocalFile() ? 1 : 2; while (path.segments().size() > maxPathSize) { paths.append(path.cd(QStringLiteral("node_modules"))); path.addPath(QStringLiteral("..")); } return paths; } QString NodeJS::moduleFileName(const QString& moduleName, const QString& url) { QMutexLocker lock(&m_mutex); auto pair = qMakePair(moduleName, url); if (m_cachedModuleFileNames.contains(pair)) { return m_cachedModuleFileNames.value(pair); } QString& fileName = m_cachedModuleFileNames[pair]; // Absolue and relative URLs if (moduleName.startsWith(QLatin1Char('/')) || moduleName.startsWith(QLatin1Char('.'))) { // NOTE: This is not portable to Windows, but the Node.js documentation // only talks about module names that start with /, ./ and ../ . fileName = fileOrDirectoryPath(Path(url).cd(QStringLiteral("..")).cd(moduleName).toLocalFile()); return fileName; } // Try all the paths that might contain modules for (auto path : moduleDirectories(url)) { fileName = fileOrDirectoryPath(path.cd(moduleName).toLocalFile()); if (!fileName.isEmpty()) { break; } } return fileName; } QString NodeJS::fileOrDirectoryPath(const QString& baseName) { if (QFile::exists(baseName)) { return baseName; } else if (QFile::exists(baseName + QLatin1String(".js"))) { return baseName + QLatin1String(".js"); } else if (QFile::exists(baseName + QLatin1String("/index.js"))) { // TODO: package.json files currently not supported return baseName + QLatin1String("/index.js"); } return QString(); } } diff --git a/plugins/qmljs/duchain/parsesession.cpp b/plugins/qmljs/duchain/parsesession.cpp index 4538e75bec..8d6aa4d324 100644 --- a/plugins/qmljs/duchain/parsesession.cpp +++ b/plugins/qmljs/duchain/parsesession.cpp @@ -1,284 +1,286 @@ /************************************************************************************* * Copyright (C) 2012 by 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) 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 "parsesession.h" #include "debugvisitor.h" #include "cache.h" #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; IndexedString ParseSession::languageString() { static const IndexedString langString("QML/JS"); return langString; } bool isSorted(const QList& locations) { if (locations.size() <= 1) { return true; } for(int i = 1; i < locations.size(); ++i) { if (locations.at(i).begin() <= locations.at(i-1).begin()) { return false; } } return true; } QmlJS::Dialect ParseSession::guessLanguageFromSuffix(const QString& path) { if (path.endsWith(QLatin1String(".js"))) { return QmlJS::Dialect::JavaScript; } else if (path.endsWith(QLatin1String(".json"))) { return QmlJS::Dialect::Json; } else { return QmlJS::Dialect::Qml; } } ParseSession::ParseSession(const IndexedString& url, const QString& contents, int priority) : m_url(url), m_ownPriority(priority), m_allDependenciesSatisfied(true) { const QString path = m_url.str(); m_doc = QmlJS::Document::create(path, guessLanguageFromSuffix(path)); m_doc->setSource(contents); m_doc->parse(); Q_ASSERT(isSorted(m_doc->engine()->comments())); // Parse the module name and the version of url (this is used only when the file // is a QML module, but doesn't break for JavaScript files) m_baseName = QString::fromUtf8(m_url.byteArray()) .section('/', -1, -1) // Base name .section('.', 0, -2); // Without extension } bool ParseSession::isParsedCorrectly() const { return m_doc->isParsedCorrectly(); } QmlJS::AST::Node* ParseSession::ast() const { return m_doc->ast(); } IndexedString ParseSession::url() const { return m_url; } QString ParseSession::moduleName() const { return m_baseName; } void ParseSession::addProblem(QmlJS::AST::Node* node, const QString& message, IProblem::Severity severity) { ProblemPointer p(new Problem); p->setDescription(message); p->setSeverity(severity); p->setSource(IProblem::SemanticAnalysis); p->setFinalLocation(DocumentRange(m_url, editorFindRange(node, node).castToSimpleRange())); m_problems << p; } QList ParseSession::problems() const { QList problems = m_problems; - foreach (const QmlJS::DiagnosticMessage& msg, m_doc->diagnosticMessages()) { + const auto diagnosticMessages = m_doc->diagnosticMessages(); + problems.reserve(problems.size() + diagnosticMessages.size()); + for (const auto& msg : diagnosticMessages) { ProblemPointer p(new Problem); p->setDescription(msg.message); p->setSeverity(IProblem::Error); p->setSource(IProblem::Parser); p->setFinalLocation(DocumentRange(m_url, locationToRange(msg.loc).castToSimpleRange())); problems << p; } return problems; } QString ParseSession::symbolAt(const QmlJS::AST::SourceLocation& location) const { return m_doc->source().mid(location.offset, location.length); } QmlJS::Dialect ParseSession::language() const { return m_doc->language(); } bool compareSourceLocation(const QmlJS::AST::SourceLocation& l, const QmlJS::AST::SourceLocation& r) { return l.begin() < r.begin(); } QString ParseSession::commentForLocation(const QmlJS::AST::SourceLocation& location) const { // find most recent comment in sorted list of comments const QList< QmlJS::AST::SourceLocation >& comments = m_doc->engine()->comments(); auto it = std::lower_bound( comments.constBegin(), comments.constEnd(), location, compareSourceLocation ); if (it == comments.constBegin()) { return QString(); } // lower bound returns the place of insertion, // we want the comment before that it--; RangeInRevision input = locationToRange(location); RangeInRevision match = locationToRange(*it); if (match.end.line != input.start.line - 1 && match.end.line != input.start.line) { return QString(); } ///TODO: merge consecutive //-style comments? return formatComment(symbolAt(*it)); } RangeInRevision ParseSession::locationToRange(const QmlJS::AST::SourceLocation& location) const { const int linesInLocation = m_doc->source().midRef(location.offset, location.length).count('\n'); return RangeInRevision(location.startLine - 1, location.startColumn - 1, location.startLine - 1 + linesInLocation, location.startColumn - 1 + location.length); } RangeInRevision ParseSession::locationsToRange(const QmlJS::AST::SourceLocation& locationFrom, const QmlJS::AST::SourceLocation& locationTo) const { return RangeInRevision(locationToRange(locationFrom).start, locationToRange(locationTo).end); } RangeInRevision ParseSession::locationsToInnerRange(const QmlJS::AST::SourceLocation& locationFrom, const QmlJS::AST::SourceLocation& locationTo) const { return RangeInRevision(locationToRange(locationFrom).end, locationToRange(locationTo).start); } RangeInRevision ParseSession::editorFindRange(QmlJS::AST::Node* fromNode, QmlJS::AST::Node* toNode) const { return locationsToRange(fromNode->firstSourceLocation(), toNode->lastSourceLocation()); } void ParseSession::setContextOnNode(QmlJS::AST::Node* node, DUContext* context) { m_astToContext.insert(node, DUContextPointer(context)); } DUContext* ParseSession::contextFromNode(QmlJS::AST::Node* node) const { return m_astToContext.value(node, DUContextPointer()).data(); } bool ParseSession::allDependenciesSatisfied() const { return m_allDependenciesSatisfied; } ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName) { ReferencedTopDUContext res = contextOfFile(fileName, m_url, m_ownPriority); if (!res) { // The file was not yet present in the DUChain, store this information. // This will prevent the second parsing pass from running (it would be // useless as the file will be re-parsed when res will become available) m_allDependenciesSatisfied = false; } return res; } ReferencedTopDUContext ParseSession::contextOfFile(const QString& fileName, const KDevelop::IndexedString& url, int ownPriority) { if (fileName.isEmpty()) { return ReferencedTopDUContext(); } // Get the top context of this module file DUChainReadLocker lock; IndexedString moduleFileString(fileName); ReferencedTopDUContext moduleContext = DUChain::self()->chainForDocument(moduleFileString); lock.unlock(); QmlJS::Cache::instance().addDependency(url, moduleFileString); if (!moduleContext) { // Queue the file on which we depend with a lower priority than the one of this file scheduleForParsing(moduleFileString, ownPriority - 1); // Register a dependency between this file and the imported one return ReferencedTopDUContext(); } else { return moduleContext; } } void ParseSession::reparseImporters() { for (const KDevelop::IndexedString& file : QmlJS::Cache::instance().filesThatDependOn(m_url)) { scheduleForParsing(file, m_ownPriority); } } void ParseSession::scheduleForParsing(const IndexedString& url, int priority) { BackgroundParser* bgparser = KDevelop::ICore::self()->languageController()->backgroundParser(); TopDUContext::Features features = (TopDUContext::Features) (TopDUContext::ForceUpdate | TopDUContext::AllDeclarationsContextsAndUses); if (bgparser->isQueued(url)) { bgparser->removeDocument(url); } bgparser->addDocument(url, features, priority, nullptr, ParseJob::FullSequentialProcessing); } void ParseSession::dumpNode(QmlJS::AST::Node* node) const { DebugVisitor v(this); v.startVisiting(node); } diff --git a/plugins/qthelp/qthelpplugin.cpp b/plugins/qthelp/qthelpplugin.cpp index 02103b6a23..259ee9891d 100644 --- a/plugins/qthelp/qthelpplugin.cpp +++ b/plugins/qthelp/qthelpplugin.cpp @@ -1,180 +1,181 @@ /* This file is part of KDevelop Copyright 2009 Aleix Pol Copyright 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qthelpplugin.h" #include #include #include #include #include "qthelpprovider.h" #include "qthelpqtdoc.h" #include "qthelp_config_shared.h" #include "debug.h" #include "qthelpconfig.h" QtHelpPlugin *QtHelpPlugin::s_plugin = nullptr; K_PLUGIN_FACTORY_WITH_JSON(QtHelpPluginFactory, "kdevqthelp.json", registerPlugin(); ) QtHelpPlugin::QtHelpPlugin(QObject* parent, const QVariantList& args) : KDevelop::IPlugin(QStringLiteral("kdevqthelp"), parent) , m_qtHelpProviders() , m_qtDoc(new QtHelpQtDoc(this, QVariantList())) , m_loadSystemQtDoc(false) { Q_UNUSED(args); s_plugin = this; connect(this, &QtHelpPlugin::changedProvidersList, KDevelop::ICore::self()->documentationController(), &KDevelop::IDocumentationController::changedDocumentationProviders); QMetaObject::invokeMethod(this, "readConfig", Qt::QueuedConnection); } QtHelpPlugin::~QtHelpPlugin() { } void QtHelpPlugin::readConfig() { QStringList iconList, nameList, pathList, ghnsList; QString searchDir; qtHelpReadConfig(iconList, nameList, pathList, ghnsList, searchDir, m_loadSystemQtDoc); searchHelpDirectory(pathList, nameList, iconList, searchDir); loadQtHelpProvider(pathList, nameList, iconList); loadQtDocumentation(m_loadSystemQtDoc); emit changedProvidersList(); } void QtHelpPlugin::loadQtDocumentation(bool loadQtDoc) { if(!loadQtDoc){ m_qtDoc->unloadDocumentation(); } else if(loadQtDoc) { m_qtDoc->loadDocumentation(); } } void QtHelpPlugin::searchHelpDirectory(QStringList& pathList, QStringList& nameList, QStringList& iconList, const QString& searchDir) { if (searchDir.isEmpty()) { return; } qCDebug(QTHELP) << "Searching qch files in: " << searchDir; QDirIterator dirIt(searchDir, QStringList() << QStringLiteral("*.qch"), QDir::Files, QDirIterator::Subdirectories); const QString logo(QStringLiteral("qtlogo")); while(dirIt.hasNext() == true) { dirIt.next(); qCDebug(QTHELP) << "qch found: " << dirIt.filePath(); pathList.append(dirIt.filePath()); nameList.append(dirIt.fileInfo().baseName()); iconList.append(logo); } } void QtHelpPlugin::loadQtHelpProvider(const QStringList& pathList, const QStringList& nameList, const QStringList& iconList) { QList oldList(m_qtHelpProviders); m_qtHelpProviders.clear(); for(int i=0; i < pathList.length(); i++) { // check if provider already exist QString fileName = pathList.at(i); QString name = nameList.at(i); QString iconName = iconList.at(i); QString nameSpace = QHelpEngineCore::namespaceName(fileName); if(!nameSpace.isEmpty()){ QtHelpProvider *provider = nullptr; foreach(QtHelpProvider* oldProvider, oldList){ if(QHelpEngineCore::namespaceName(oldProvider->fileName()) == nameSpace){ provider = oldProvider; oldList.removeAll(provider); break; } } if(!provider){ provider = new QtHelpProvider(this, fileName, name, iconName, QVariantList()); }else{ provider->setName(name); provider->setIconName(iconName); } bool exist = false; foreach(QtHelpProvider* existingProvider, m_qtHelpProviders){ if(QHelpEngineCore::namespaceName(existingProvider->fileName()) == nameSpace){ exist = true; break; } } if(!exist){ m_qtHelpProviders.append(provider); } } } // delete unused providers qDeleteAll(oldList); } QList QtHelpPlugin::providers() { QList list; + list.reserve(m_qtHelpProviders.size() + (m_loadSystemQtDoc?1:0)); foreach(QtHelpProvider* provider, m_qtHelpProviders) { list.append(provider); } if(m_loadSystemQtDoc){ list.append(m_qtDoc); } return list; } QList QtHelpPlugin::qtHelpProviderLoaded() { return m_qtHelpProviders; } bool QtHelpPlugin::isQtHelpQtDocLoaded() const { return m_loadSystemQtDoc; } bool QtHelpPlugin::isQtHelpAvailable() const { return !m_qtDoc->qchFiles().isEmpty(); } KDevelop::ConfigPage* QtHelpPlugin::configPage(int number, QWidget* parent) { if (number == 0) { return new QtHelpConfig(this, parent); } return nullptr; } int QtHelpPlugin::configPages() const { return 1; } #include "qthelpplugin.moc" diff --git a/plugins/qthelp/qthelpqtdoc.cpp b/plugins/qthelp/qthelpqtdoc.cpp index 3d8621f5c4..6f19f3fdb4 100644 --- a/plugins/qthelp/qthelpqtdoc.cpp +++ b/plugins/qthelp/qthelpqtdoc.cpp @@ -1,146 +1,148 @@ /* This file is part of KDevelop Copyright 2009 Aleix Pol Copyright 2009 David Nolden Copyright 2010 Benjamin Port Copyright 2016 Andreas Cord-Landwehr This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qthelpqtdoc.h" #include "debug.h" #include #include #include #include #include #include namespace { QString qmakeCandidate() { // return the first qmake executable we can find const QStringList candidates = {"qmake", "qmake-qt4", "qmake-qt5"}; auto it = std::find_if(candidates.constBegin(), candidates.constEnd(), [](const QString& candidate) { return !QStandardPaths::findExecutable(candidate).isEmpty(); }); return it != candidates.constEnd() ? *it : QString(); } } QtHelpQtDoc::QtHelpQtDoc(QObject *parent, const QVariantList &args) : QtHelpProviderAbstract(parent, QStringLiteral("qthelpcollection.qhc"), args) , m_path(QString()) { Q_UNUSED(args); registerDocumentations(); } void QtHelpQtDoc::registerDocumentations() { const QString qmake = qmakeCandidate(); if (!qmake.isEmpty()) { QProcess *p = new QProcess; p->setProcessChannelMode(QProcess::MergedChannels); p->setProgram(qmake); p->setArguments({QLatin1String("-query"), QLatin1String("QT_INSTALL_DOCS")}); p->start(); connect(p, static_cast(&QProcess::finished), this, &QtHelpQtDoc::lookupDone); } } void QtHelpQtDoc::lookupDone(int code) { QProcess *p = qobject_cast(sender()); if(code == QProcess::NormalExit) { m_path = QDir::fromNativeSeparators(QString::fromLatin1(p->readAllStandardOutput().trimmed())); qCDebug(QTHELP) << "Detected doc path:" << m_path; } else { qCCritical(QTHELP) << "qmake query returned error:" << QString::fromLatin1(p->readAllStandardError()); qCDebug(QTHELP) << "last standard output was:" << QString::fromLatin1(p->readAllStandardOutput()); } sender()->deleteLater(); } void QtHelpQtDoc::loadDocumentation() { if(m_path.isEmpty()) { return; } QStringList files = qchFiles(); if(files.isEmpty()) { qCWarning(QTHELP) << "could not find QCH file in directory" << m_path; return; } foreach(const QString &fileName, files) { QString fileNamespace = QHelpEngineCore::namespaceName(fileName); if (!fileNamespace.isEmpty() && !m_engine.registeredDocumentations().contains(fileNamespace)) { qCDebug(QTHELP) << "loading doc" << fileName << fileNamespace; if(!m_engine.registerDocumentation(fileName)) qCCritical(QTHELP) << "error >> " << fileName << m_engine.error(); } } } void QtHelpQtDoc::unloadDocumentation() { foreach(const QString &fileName, qchFiles()) { QString fileNamespace = QHelpEngineCore::namespaceName(fileName); if(!fileName.isEmpty() && m_engine.registeredDocumentations().contains(fileNamespace)) { m_engine.unregisterDocumentation(fileName); } } } QStringList QtHelpQtDoc::qchFiles() const { QStringList files; - QVector paths; // test directories - paths << m_path << m_path+"/qch/"; + const QVector paths{ // test directories + m_path, + m_path + QLatin1String("/qch/"), + }; foreach (const auto &path, paths) { QDir d(path); if(path.isEmpty() || !d.exists()) { continue; } foreach(const auto& file, d.entryInfoList(QDir::Files)) { files << file.absoluteFilePath(); } } if (files.isEmpty()) { qCDebug(QTHELP) << "no QCH file found at all"; } return files; } QIcon QtHelpQtDoc::icon() const { return QIcon::fromTheme("qtlogo"); } QString QtHelpQtDoc::name() const { return i18n("QtHelp"); } diff --git a/plugins/quickopen/duchainitemquickopen.cpp b/plugins/quickopen/duchainitemquickopen.cpp index 9637aba1e0..25e28c2251 100644 --- a/plugins/quickopen/duchainitemquickopen.cpp +++ b/plugins/quickopen/duchainitemquickopen.cpp @@ -1,250 +1,251 @@ /* This file is part of the KDE libraries Copyright (C) 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectitemquickopen.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; DUChainItemData::DUChainItemData(const DUChainItem& file, bool openDefinition) : m_item(file) , m_openDefinition(openDefinition) { } QString DUChainItemData::text() const { - DUChainReadLocker lock;; + DUChainReadLocker lock; Declaration* decl = m_item.m_item.data(); if (!decl) { return i18n("Not available any more: %1", m_item.m_text); } if (FunctionDefinition* def = dynamic_cast(decl)) { if (def->declaration()) { decl = def->declaration(); } } QString text = decl->qualifiedIdentifier().toString(); if (!decl->abstractType()) { //With simplified representation, still mark functions as such by adding parens if (dynamic_cast(decl)) { text += QLatin1String("(...)"); } } else if (TypePtr function = decl->type()) { text += function->partToString(FunctionType::SignatureArguments); } return text; } QList DUChainItemData::highlighting() const { - DUChainReadLocker lock;; + DUChainReadLocker lock; Declaration* decl = m_item.m_item.data(); if (!decl) { return QList(); } if (FunctionDefinition* def = dynamic_cast(decl)) { if (def->declaration()) { decl = def->declaration(); } } QTextCharFormat boldFormat; boldFormat.setFontWeight(QFont::Bold); QTextCharFormat normalFormat; int prefixLength = 0; QString signature; TypePtr function = decl->type(); if (function) { signature = function->partToString(FunctionType::SignatureArguments); } //Only highlight the last part of the qualified identifier, so the scope doesn't distract too much QualifiedIdentifier id = decl->qualifiedIdentifier(); QString fullId = id.toString(); QString lastId; if (!id.isEmpty()) { lastId = id.last().toString(); } prefixLength += fullId.length() - lastId.length(); - QList ret; - ret << 0; - ret << prefixLength; - ret << QVariant(normalFormat); - ret << prefixLength; - ret << lastId.length(); - ret << QVariant(boldFormat); + QList ret{ + 0, + prefixLength, + QVariant(normalFormat), + prefixLength, + lastId.length(), + QVariant(boldFormat), + }; if (!signature.isEmpty()) { ret << prefixLength + lastId.length(); ret << signature.length(); ret << QVariant(normalFormat); } return ret; } QString DUChainItemData::htmlDescription() const { if (m_item.m_noHtmlDestription) { return QString(); } - DUChainReadLocker lock;; + DUChainReadLocker lock; Declaration* decl = m_item.m_item.data(); if (!decl) { return i18n("Not available any more"); } TypePtr function = decl->type(); QString text; if (function && function->returnType()) { text = i18nc("%1: function signature", "Return: %1", - function->partToString(FunctionType::SignatureReturn)) + QLatin1String(" "); + function->partToString(FunctionType::SignatureReturn)) + QLatin1Char(' '); } text += i18nc("%1: file path", "File: %1", ICore::self()->projectController()->prettyFileName(decl->url().toUrl())); QString ret = "" + text + ""; return ret; } bool DUChainItemData::execute(QString& /*filterText*/) { - DUChainReadLocker lock;; + DUChainReadLocker lock; Declaration* decl = m_item.m_item.data(); if (!decl) { return false; } if (m_openDefinition && FunctionDefinition::definition(decl)) { decl = FunctionDefinition::definition(decl); } QUrl url = decl->url().toUrl(); KTextEditor::Cursor cursor = decl->rangeInCurrentRevision().start(); DUContext* internal = decl->internalContext(); if (internal && (internal->type() == DUContext::Other || internal->type() == DUContext::Class)) { //Move into the body if (internal->range().end.line > internal->range().start.line) { cursor = KTextEditor::Cursor(internal->range().start.line + 1, 0); //Move into the body } } lock.unlock(); ICore::self()->documentController()->openDocument(url, cursor); return true; } bool DUChainItemData::isExpandable() const { return true; } QWidget* DUChainItemData::expandingWidget() const { - DUChainReadLocker lock;; + DUChainReadLocker lock; Declaration* decl = dynamic_cast(m_item.m_item.data()); if (!decl || !decl->context()) { return nullptr; } return decl->context()->createNavigationWidget(decl, decl->topContext(), QString(), QString(), AbstractNavigationWidget::EmbeddableWidget); } QIcon DUChainItemData::icon() const { return QIcon(); } Path DUChainItemData::projectPath() const { return m_item.m_projectPath; } DUChainItemDataProvider::DUChainItemDataProvider(IQuickOpen* quickopen, bool openDefinitions) : m_quickopen(quickopen) , m_openDefinitions(openDefinitions) { reset(); } void DUChainItemDataProvider::setFilterText(const QString& text) { Base::setFilter(text); } uint DUChainItemDataProvider::itemCount() const { return Base::filteredItems().count(); } uint DUChainItemDataProvider::unfilteredItemCount() const { return Base::items().count(); } QuickOpenDataPointer DUChainItemDataProvider::data(uint row) const { return KDevelop::QuickOpenDataPointer(createData(Base::filteredItems()[row])); } DUChainItemData* DUChainItemDataProvider::createData(const DUChainItem& item) const { return new DUChainItemData(item, m_openDefinitions); } QString DUChainItemDataProvider::itemText(const DUChainItem& data) const { return data.m_text; } void DUChainItemDataProvider::reset() { } diff --git a/plugins/quickopen/expandingtree/expandingtree.cpp b/plugins/quickopen/expandingtree/expandingtree.cpp index 7db1ed93c3..e41a222458 100644 --- a/plugins/quickopen/expandingtree/expandingtree.cpp +++ b/plugins/quickopen/expandingtree/expandingtree.cpp @@ -1,88 +1,88 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "expandingtree.h" #include #include #include #include "expandingwidgetmodel.h" #include #include using namespace KDevelop; ExpandingTree::ExpandingTree(QWidget* parent) : QTreeView(parent) { m_drawText.documentLayout()->setPaintDevice(this); setUniformRowHeights(false); } void ExpandingTree::setModel(QAbstractItemModel* model) { Q_ASSERT(!model || qobject_cast( qobject_cast(model)->sourceModel()) ); QTreeView::setModel(model); } void ExpandingTree::drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QTreeView::drawRow(painter, option, index); const ExpandingWidgetModel* eModel = qobject_cast(qobject_cast(model())->sourceModel()); Q_ASSERT(eModel); const QModelIndex sourceIndex = eModel->mapToSource(index); if (eModel->isPartiallyExpanded(sourceIndex) != ExpandingWidgetModel::ExpansionType::NotExpanded) { QRect rect = eModel->partialExpandRect(sourceIndex); if (rect.isValid()) { painter->fillRect(rect, QBrush(0xffffffff)); QAbstractTextDocumentLayout::PaintContext ctx; // since arbitrary HTML can be shown use a black on white color scheme here ctx.palette = QPalette(Qt::black, Qt::white); - ctx.clip = QRectF(0, 0, rect.width(), rect.height());; + ctx.clip = QRectF(0, 0, rect.width(), rect.height()); painter->setViewTransformEnabled(true); painter->translate(rect.left(), rect.top()); m_drawText.setHtml(eModel->partialExpandText(sourceIndex)); m_drawText.setPageSize(QSizeF(rect.width(), rect.height())); m_drawText.documentLayout()->draw(painter, ctx); painter->translate(-rect.left(), -rect.top()); } } } int ExpandingTree::sizeHintForColumn(int column) const { return columnWidth(column); } void ExpandingTree::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { const auto& path = index.data(ProjectPathRole).value(); if (path.isValid()) { const auto color = WidgetColorizer::colorForId(qHash(path), palette(), true); WidgetColorizer::drawBranches(this, painter, rect, index, color); } QTreeView::drawBranches(painter, rect, index); } diff --git a/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp b/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp index 0f64edc778..66846a1fed 100644 --- a/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp +++ b/plugins/quickopen/expandingtree/expandingwidgetmodel.cpp @@ -1,592 +1,594 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "expandingwidgetmodel.h" #include #include #include #include #include #include #include #include #include "expandingdelegate.h" #include QIcon ExpandingWidgetModel::m_expandedIcon; QIcon ExpandingWidgetModel::m_collapsedIcon; using namespace KTextEditor; inline QModelIndex firstColumn(const QModelIndex& index) { return index.sibling(index.row(), 0); } ExpandingWidgetModel::ExpandingWidgetModel(QWidget* parent) : QAbstractTableModel(parent) { } ExpandingWidgetModel::~ExpandingWidgetModel() { clearExpanding(); } static QColor doAlternate(const QColor& color) { QColor background = QApplication::palette().background().color(); return KColorUtils::mix(color, background, 0.15); } uint ExpandingWidgetModel::matchColor(const QModelIndex& index) const { int matchQuality = contextMatchQuality(index.sibling(index.row(), 0)); if (matchQuality > 0) { bool alternate = index.row() & 1; QColor badMatchColor(0xff00aa44); //Blue-ish green QColor goodMatchColor(0xff00ff00); //Green QColor background = treeView()->palette().light().color(); QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, (( float )matchQuality) / 10.0); if (alternate) { totalColor = doAlternate(totalColor); } const float dynamicTint = 0.2f; const float minimumTint = 0.2f; double tintStrength = (dynamicTint * matchQuality) / 10; if (tintStrength) { tintStrength += minimumTint; //Some minimum tinting strength, else it's not visible any more } return KColorUtils::tint(background, totalColor, tintStrength).rgb(); } else { return 0; } } QVariant ExpandingWidgetModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::BackgroundRole: { if (index.column() == 0) { //Highlight by match-quality uint color = matchColor(index); if (color) { return QBrush(color); } } //Use a special background-color for expanded items if (isExpanded(index)) { if (index.row() & 1) { return doAlternate(treeView()->palette().toolTipBase().color()); } else { return treeView()->palette().toolTipBase(); } } } } return QVariant(); } QModelIndex ExpandingWidgetModel::mapFromSource(const QModelIndex& index) const { const auto proxyModel = qobject_cast(treeView()->model()); Q_ASSERT(proxyModel); Q_ASSERT(!index.isValid() || index.model() == this); return proxyModel->mapFromSource(index); } QModelIndex ExpandingWidgetModel::mapToSource(const QModelIndex& index) const { const auto proxyModel = qobject_cast(treeView()->model()); Q_ASSERT(proxyModel); Q_ASSERT(!index.isValid() || index.model() == proxyModel); return proxyModel->mapToSource(index); } void ExpandingWidgetModel::clearMatchQualities() { m_contextMatchQualities.clear(); } QModelIndex ExpandingWidgetModel::partiallyExpandedRow() const { if (m_partiallyExpanded.isEmpty()) { return QModelIndex(); } else { return m_partiallyExpanded.constBegin().key(); } } void ExpandingWidgetModel::clearExpanding() { clearMatchQualities(); QMap oldExpandState = m_expandState; foreach (QPointer widget, m_expandingWidgets) { delete widget; } m_expandingWidgets.clear(); m_expandState.clear(); m_partiallyExpanded.clear(); for (QMap::const_iterator it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it) { if (it.value() == Expanded) { emit dataChanged(it.key(), it.key()); } } } ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex& index) const { if (m_partiallyExpanded.contains(firstColumn(index))) { return m_partiallyExpanded[firstColumn(index)]; } else { return NotExpanded; } } void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex& idx_) { QModelIndex index(firstColumn(idx_)); m_partiallyExpanded.remove(index); m_partiallyExpanded.remove(idx_); } int ExpandingWidgetModel::partiallyExpandWidgetHeight() const { return 60; ///@todo use font-metrics text-height*2 for 2 lines } void ExpandingWidgetModel::rowSelected(const QModelIndex& idx_) { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); if (!m_partiallyExpanded.contains(idx)) { QModelIndex oldIndex = partiallyExpandedRow(); //Unexpand the previous partially expanded row if (!m_partiallyExpanded.isEmpty()) { ///@todo allow multiple partially expanded rows while (!m_partiallyExpanded.isEmpty()) m_partiallyExpanded.erase(m_partiallyExpanded.begin()); //partiallyUnExpand( m_partiallyExpanded.begin().key() ); } //Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget. if (!idx.isValid()) { //All items have been unselected if (oldIndex.isValid()) { emit dataChanged(oldIndex, oldIndex); } } else { QVariant variant = data(idx, CodeCompletionModel::ItemSelected); if (!isExpanded(idx) && variant.type() == QVariant::String) { //Either expand upwards or downwards, choose in a way that //the visible fields of the new selected entry are not moved. if (oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()))) { m_partiallyExpanded.insert(idx, ExpandUpwards); } else { m_partiallyExpanded.insert(idx, ExpandDownwards); } //Say that one row above until one row below has changed, so no items will need to be moved(the space that is taken from one item is given to the other) if (oldIndex.isValid() && oldIndex < idx) { emit dataChanged(oldIndex, idx); if (treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem) { const QModelIndex viewIndex = mapFromSource(idx); //Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible, //so we do the scrolling by hand. QRect selectedRect = treeView()->visualRect(viewIndex); QRect frameRect = treeView()->frameRect(); if (selectedRect.bottom() > frameRect.bottom()) { int diff = selectedRect.bottom() - frameRect.bottom(); //We need to scroll down QModelIndex newTopIndex = viewIndex; QModelIndex nextTopIndex = viewIndex; QRect nextRect = treeView()->visualRect(nextTopIndex); while (nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff) { newTopIndex = nextTopIndex; nextTopIndex = treeView()->indexAbove(nextTopIndex); if (nextTopIndex.isValid()) { nextRect = treeView()->visualRect(nextTopIndex); } } treeView()->scrollTo(newTopIndex, QAbstractItemView::PositionAtTop); } } //This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible. //But we must make sure that it isn't too expensive. //We need to make sure that scrolling is efficient, and the whole content is not repainted. //Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature. //Since this also doesn't work smoothly, leave it for now //treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible ); } else if (oldIndex.isValid() && idx < oldIndex) { emit dataChanged(idx, oldIndex); //For consistency with the down-scrolling, we keep one additional line visible above the current visible. //Since this also doesn't work smoothly, leave it for now /* QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column()); if( prevLine.isValid() ) treeView()->scrollTo( prevLine );*/ } else { emit dataChanged(idx, idx); } } else if (oldIndex.isValid()) { //We are not partially expanding a new row, but we previously had a partially expanded row. So signalize that it has been unexpanded. emit dataChanged(oldIndex, oldIndex); } } } else { qCDebug(PLUGIN_QUICKOPEN) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded"; } } QString ExpandingWidgetModel::partialExpandText(const QModelIndex& idx) const { Q_ASSERT(idx.model() == this); if (!idx.isValid()) { return QString(); } return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString(); } QRect ExpandingWidgetModel::partialExpandRect(const QModelIndex& idx_) const { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); if (!idx.isValid()) { return QRect(); } ExpansionType expansion = ExpandDownwards; if (m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd()) { expansion = m_partiallyExpanded[idx]; } //Get the whole rectangle of the row: const QModelIndex viewIndex = mapFromSource(idx); QModelIndex rightMostIndex = viewIndex; QModelIndex tempIndex = viewIndex; while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid()) rightMostIndex = tempIndex; QRect rect = treeView()->visualRect(viewIndex); QRect rightMostRect = treeView()->visualRect(rightMostIndex); rect.setLeft(rect.left() + 20); rect.setRight(rightMostRect.right() - 5); //These offsets must match exactly those used in ExpandingDelegate::sizeHint() int top = rect.top() + 5; int bottom = rightMostRect.bottom() - 5; if (expansion == ExpandDownwards) { top += basicRowHeight(viewIndex); } else { bottom -= basicRowHeight(viewIndex); } rect.setTop(top); rect.setBottom(bottom); return rect; } bool ExpandingWidgetModel::isExpandable(const QModelIndex& idx_) const { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); if (!m_expandState.contains(idx)) { m_expandState.insert(idx, NotExpandable); QVariant v = data(idx, CodeCompletionModel::IsExpandable); if (v.canConvert() && v.toBool()) { m_expandState[idx] = Expandable; } } return m_expandState[idx] != NotExpandable; } bool ExpandingWidgetModel::isExpanded(const QModelIndex& idx_) const { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); return m_expandState.contains(idx) && m_expandState[idx] == Expanded; } void ExpandingWidgetModel::setExpanded(const QModelIndex& idx_, bool expanded) { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); qCDebug(PLUGIN_QUICKOPEN) << "Setting expand-state of row " << idx.row() << " to " << expanded; if (!idx.isValid()) { return; } if (isExpandable(idx)) { if (!expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx]) { m_expandingWidgets[idx]->hide(); } m_expandState[idx] = expanded ? Expanded : Expandable; if (expanded) { partiallyUnExpand(idx); } if (expanded && !m_expandingWidgets.contains(idx)) { QVariant v = data(idx, CodeCompletionModel::ExpandingWidget); if (v.canConvert()) { m_expandingWidgets[idx] = v.value(); } else if (v.canConvert()) { //Create a html widget that shows the given string KTextEdit* edit = new KTextEdit(v.toString()); edit->setReadOnly(true); edit->resize(200, 50); //Make the widget small so it embeds nicely. m_expandingWidgets[idx] = edit; } else { m_expandingWidgets[idx] = nullptr; } } //Eventually partially expand the row if (!expanded && firstColumn(mapToSource(treeView()->currentIndex())) == idx && (isPartiallyExpanded(idx) == ExpandingWidgetModel::ExpansionType::NotExpanded)) { rowSelected(idx); //Partially expand the row. } emit dataChanged(idx, idx); if (treeView()) { treeView()->scrollTo(mapFromSource(idx)); } } } int ExpandingWidgetModel::basicRowHeight(const QModelIndex& idx_) const { Q_ASSERT(idx_.model() == treeView()->model()); QModelIndex idx(firstColumn(idx_)); ExpandingDelegate* delegate = dynamic_cast(treeView()->itemDelegate(idx)); if (!delegate || !idx.isValid()) { qCDebug(PLUGIN_QUICKOPEN) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate"; return 15; } return delegate->basicSizeHint(idx).height(); } void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex& idx_) { Q_ASSERT(idx_.model() == this); QModelIndex idx(firstColumn(idx_)); QWidget* w = nullptr; if (m_expandingWidgets.contains(idx)) { w = m_expandingWidgets[idx]; } if (w && isExpanded(idx)) { if (!idx.isValid()) { return; } const QModelIndex viewIndex = mapFromSource(idx_); QRect rect = treeView()->visualRect(viewIndex); if (!rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height()) { //The item is currently not visible w->hide(); return; } QModelIndex rightMostIndex = viewIndex; QModelIndex tempIndex = viewIndex; while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid()) rightMostIndex = tempIndex; QRect rightMostRect = treeView()->visualRect(rightMostIndex); //Find out the basic height of the row rect.setLeft(rect.left() + 20); rect.setRight(rightMostRect.right() - 5); //These offsets must match exactly those used in KateCompletionDeleage::sizeHint() rect.setTop(rect.top() + basicRowHeight(viewIndex) + 5); rect.setHeight(w->height()); if (w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible()) { w->setParent(treeView()->viewport()); w->setGeometry(rect); w->show(); } } } void ExpandingWidgetModel::placeExpandingWidgets() { for (QMap >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) { placeExpandingWidget(it.key()); } } int ExpandingWidgetModel::expandingWidgetsHeight() const { int sum = 0; for (QMap >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) { if (isExpanded(it.key()) && (*it)) { sum += (*it)->height(); } } return sum; } QWidget* ExpandingWidgetModel::expandingWidget(const QModelIndex& idx_) const { QModelIndex idx(firstColumn(idx_)); if (m_expandingWidgets.contains(idx)) { return m_expandingWidgets[idx]; } else { return nullptr; } } void ExpandingWidgetModel::cacheIcons() const { if (m_expandedIcon.isNull()) { m_expandedIcon = QIcon::fromTheme(QStringLiteral("arrow-down")); } if (m_collapsedIcon.isNull()) { m_collapsedIcon = QIcon::fromTheme(QStringLiteral("arrow-right")); } } QList mergeCustomHighlighting(int leftSize, const QList& left, int rightSize, const QList& right) { QList ret = left; if (left.isEmpty()) { ret << QVariant(0); ret << QVariant(leftSize); ret << QTextFormat(QTextFormat::CharFormat); } if (right.isEmpty()) { ret << QVariant(leftSize); ret << QVariant(rightSize); ret << QTextFormat(QTextFormat::CharFormat); } else { QList::const_iterator it = right.constBegin(); while (it != right.constEnd()) { { QList::const_iterator testIt = it; for (int a = 0; a < 2; a++) { ++testIt; if (testIt == right.constEnd()) { qCWarning(PLUGIN_QUICKOPEN) << "Length of input is not multiple of 3"; break; } } } ret << QVariant((*it).toInt() + leftSize); ++it; ret << QVariant((*it).toInt()); ++it; ret << *it; if (!(*it).value().isValid()) { qCDebug(PLUGIN_QUICKOPEN) << "Text-format is invalid"; } ++it; } } return ret; } //It is assumed that between each two strings, one space is inserted QList mergeCustomHighlighting(QStringList strings, QList highlights, int grapBetweenStrings) { if (strings.isEmpty()) { qCWarning(PLUGIN_QUICKOPEN) << "List of strings is empty"; return QList(); } if (highlights.isEmpty()) { qCWarning(PLUGIN_QUICKOPEN) << "List of highlightings is empty"; return QList(); } if (strings.count() != highlights.count()) { qCWarning(PLUGIN_QUICKOPEN) << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same"; return QList(); } //Merge them together QString totalString = strings[0]; QVariantList totalHighlighting = highlights[0]; strings.pop_front(); highlights.pop_front(); while (!strings.isEmpty()) { - totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, strings[0].length(), highlights[0]); + const int stringLength = strings[0].length(); + totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, stringLength, highlights[0]); + totalString.reserve(totalString.size() + stringLength + grapBetweenStrings); totalString += strings[0]; for (int a = 0; a < grapBetweenStrings; a++) { totalString += ' '; } strings.pop_front(); highlights.pop_front(); } //Combine the custom-highlightings return totalHighlighting; } diff --git a/plugins/quickopen/projectfilequickopen.cpp b/plugins/quickopen/projectfilequickopen.cpp index adbecb7500..b9678ad1f9 100644 --- a/plugins/quickopen/projectfilequickopen.cpp +++ b/plugins/quickopen/projectfilequickopen.cpp @@ -1,367 +1,366 @@ /* This file is part of the KDE libraries Copyright (C) 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectfilequickopen.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../openwith/iopenwith.h" using namespace KDevelop; namespace { QSet openFiles() { QSet openFiles; const QList& docs = ICore::self()->documentController()->openDocuments(); openFiles.reserve(docs.size()); foreach (IDocument* doc, docs) { openFiles << IndexedString(doc->url()); } return openFiles; } QString iconNameForUrl(const IndexedString& url) { if (url.isEmpty()) { return QStringLiteral("tab-duplicate"); } ProjectBaseItem* item = ICore::self()->projectController()->projectModel()->itemForPath(url); if (item) { return item->iconName(); } return QStringLiteral("unknown"); } } ProjectFileData::ProjectFileData(const ProjectFile& file) : m_file(file) { } QString ProjectFileData::text() const { return m_file.projectPath.relativePath(m_file.path); } QString ProjectFileData::htmlDescription() const { return "" + i18nc("%1: project name", "Project %1", project()) + ""; } bool ProjectFileData::execute(QString& filterText) { const QUrl url = m_file.path.toUrl(); IOpenWith::openFiles(QList() << url); auto cursor = KTextEditorHelpers::extractCursor(filterText); if (cursor.isValid()) { IDocument* doc = ICore::self()->documentController()->documentForUrl(url); if (doc) { doc->setCursorPosition(cursor); } } return true; } bool ProjectFileData::isExpandable() const { return true; } QList ProjectFileData::highlighting() const { QTextCharFormat boldFormat; boldFormat.setFontWeight(QFont::Bold); QTextCharFormat normalFormat; QString txt = text(); - QList ret; - int fileNameLength = m_file.path.lastPathSegment().length(); - ret << 0; - ret << txt.length() - fileNameLength; - ret << QVariant(normalFormat); - ret << txt.length() - fileNameLength; - ret << fileNameLength; - ret << QVariant(boldFormat); - + const QList ret{ + 0, + txt.length() - fileNameLength, + QVariant(normalFormat), + txt.length() - fileNameLength, + fileNameLength, + QVariant(boldFormat), + }; return ret; } QWidget* ProjectFileData::expandingWidget() const { const QUrl url = m_file.path.toUrl(); DUChainReadLocker lock; ///Find a du-chain for the document QList contexts = DUChain::self()->chainsForDocument(url); ///Pick a non-proxy context TopDUContext* chosen = nullptr; foreach (TopDUContext* ctx, contexts) { if (!(ctx->parsingEnvironmentFile() && ctx->parsingEnvironmentFile()->isProxyContext())) { chosen = ctx; } } if (chosen) { return chosen->createNavigationWidget(nullptr, nullptr, "" + i18nc("%1: project name", "Project %1", project()) + "
    "); } else { QTextBrowser* ret = new QTextBrowser(); ret->resize(400, 100); ret->setText( "" + i18nc("%1: project name", "Project %1", project()) + "
    " + i18n("Not parsed yet") + "
    "); return ret; } return nullptr; } QIcon ProjectFileData::icon() const { const QString& iconName = iconNameForUrl(m_file.indexedPath); /** * FIXME: Move this cache into a more central place and reuse it elsewhere. * The project model e.g. could reuse this as well. * * Note: We cache here since otherwise displaying and esp. scrolling * in a large list of quickopen items becomes very slow. */ static QHash iconCache; QHash::const_iterator it = iconCache.constFind(iconName); if (it != iconCache.constEnd()) { return it.value(); } const QPixmap& pixmap = KIconLoader::global()->loadIcon(iconName, KIconLoader::Small); iconCache.insert(iconName, pixmap); return pixmap; } QString ProjectFileData::project() const { const IProject* project = ICore::self()->projectController()->findProjectForUrl(m_file.path.toUrl()); if (project) { return project->name(); } else { return i18n("none"); } } Path ProjectFileData::projectPath() const { return m_file.projectPath; } BaseFileDataProvider::BaseFileDataProvider() { } void BaseFileDataProvider::setFilterText(const QString& text) { int pathLength; KTextEditorHelpers::extractCursor(text, &pathLength); QString path(text.mid(0, pathLength)); if (path.startsWith(QLatin1String("./")) || path.startsWith(QLatin1String("../"))) { // assume we want to filter relative to active document's url IDocument* doc = ICore::self()->documentController()->activeDocument(); if (doc) { path = Path(Path(doc->url()).parent(), path).pathOrUrl(); } } setFilter(path.split('/', QString::SkipEmptyParts)); } uint BaseFileDataProvider::itemCount() const { return filteredItems().count(); } uint BaseFileDataProvider::unfilteredItemCount() const { return items().count(); } QuickOpenDataPointer BaseFileDataProvider::data(uint row) const { return QuickOpenDataPointer(new ProjectFileData(filteredItems().at(row))); } ProjectFileDataProvider::ProjectFileDataProvider() { auto projectController = ICore::self()->projectController(); connect(projectController, &IProjectController::projectClosing, this, &ProjectFileDataProvider::projectClosing); connect(projectController, &IProjectController::projectOpened, this, &ProjectFileDataProvider::projectOpened); foreach (const auto project, projectController->projects()) { projectOpened(project); } } void ProjectFileDataProvider::projectClosing(IProject* project) { foreach (ProjectFileItem* file, KDevelop::allFiles(project->projectItem())) { fileRemovedFromSet(file); } } void ProjectFileDataProvider::projectOpened(IProject* project) { const int processAfter = 1000; int processed = 0; foreach (ProjectFileItem* file, KDevelop::allFiles(project->projectItem())) { fileAddedToSet(file); if (++processed == processAfter) { // prevent UI-lockup when a huge project was imported QApplication::processEvents(); processed = 0; } } connect(project, &IProject::fileAddedToSet, this, &ProjectFileDataProvider::fileAddedToSet); connect(project, &IProject::fileRemovedFromSet, this, &ProjectFileDataProvider::fileRemovedFromSet); } void ProjectFileDataProvider::fileAddedToSet(ProjectFileItem* file) { ProjectFile f; f.projectPath = file->project()->path(); f.path = file->path(); f.indexedPath = file->indexedPath(); f.outsideOfProject = !f.projectPath.isParentOf(f.path); auto it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), f); if (it == m_projectFiles.end() || it->path != f.path) { m_projectFiles.insert(it, f); } } void ProjectFileDataProvider::fileRemovedFromSet(ProjectFileItem* file) { ProjectFile item; item.path = file->path(); // fast-path for non-generated files // NOTE: figuring out whether something is generated is expensive... and since // generated files are rare we apply this two-step algorithm here auto it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), item); if (it != m_projectFiles.end() && !(item < *it)) { m_projectFiles.erase(it); return; } // last try: maybe it was generated item.outsideOfProject = true; it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), item); if (it != m_projectFiles.end() && !(item < *it)) { m_projectFiles.erase(it); return; } } void ProjectFileDataProvider::reset() { clearFilter(); QVector projectFiles = m_projectFiles; const auto& open = openFiles(); for (QVector::iterator it = projectFiles.begin(); it != projectFiles.end(); ) { if (open.contains(it->indexedPath)) { it = projectFiles.erase(it); } else { ++it; } } setItems(projectFiles); } QSet ProjectFileDataProvider::files() const { QSet ret; foreach (IProject* project, ICore::self()->projectController()->projects()) { ret += project->fileSet(); } return ret - openFiles(); } void OpenFilesDataProvider::reset() { clearFilter(); IProjectController* projCtrl = ICore::self()->projectController(); IDocumentController* docCtrl = ICore::self()->documentController(); const QList& docs = docCtrl->openDocuments(); QVector currentFiles; currentFiles.reserve(docs.size()); foreach (IDocument* doc, docs) { ProjectFile f; f.path = Path(doc->url()); IProject* project = projCtrl->findProjectForUrl(doc->url()); if (project) { f.projectPath = project->path(); } currentFiles << f; } std::sort(currentFiles.begin(), currentFiles.end()); setItems(currentFiles); } QSet OpenFilesDataProvider::files() const { return openFiles(); } diff --git a/plugins/quickopen/projectitemquickopen.cpp b/plugins/quickopen/projectitemquickopen.cpp index 2aaea81cee..3eb65df2f8 100644 --- a/plugins/quickopen/projectitemquickopen.cpp +++ b/plugins/quickopen/projectitemquickopen.cpp @@ -1,374 +1,376 @@ /* This file is part of the KDE libraries Copyright (C) 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectitemquickopen.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; namespace { struct SubstringCache { explicit SubstringCache(const QString& string = QString()) : substring(string) { } inline int containedIn(const Identifier& id) const { int index = id.index(); QHash::const_iterator it = cache.constFind(index); if (it != cache.constEnd()) { return *it; } const QString idStr = id.identifier().str(); int result = idStr.lastIndexOf(substring, -1, Qt::CaseInsensitive); if (result < 0 && !idStr.isEmpty() && !substring.isEmpty()) { // no match; try abbreviations result = matchesAbbreviation(idStr.midRef(0), substring) ? 0 : -1; } //here we shift the values if the matched string is bigger than the substring, //so closer matches will appear first if (result >= 0) { result = result + (idStr.size() - substring.size()); } cache[index] = result; return result; } QString substring; mutable QHash cache; }; struct ClosestMatchToText { explicit ClosestMatchToText(const QHash& _cache) : cache(_cache) { } /** @brief Calculates the distance to two pre-filtered match items * * @param a The CodeModelView witch represents the first item to be tested * @param b The CodeModelView witch represents the second item to be tested * * @b */ inline bool operator()(const CodeModelViewItem& a, const CodeModelViewItem& b) const { const int height_a = cache.value(a.m_id.index(), -1); const int height_b = cache.value(b.m_id.index(), -1); Q_ASSERT(height_a != -1); Q_ASSERT(height_b != -1); if (height_a == height_b) { // stable sorting for equal items based on index // TODO: fast string-based sorting in such cases? return a.m_id.index() < b.m_id.index(); } return height_a < height_b; } private: const QHash& cache; }; Path findProjectForForPath(const IndexedString& path) { const auto model = ICore::self()->projectController()->projectModel(); const auto item = model->itemForPath(path); return item ? item->project()->path() : Path(); } uint addedItems(const AddedItems& items) { uint add = 0; for(auto it = items.constBegin(); it != items.constEnd(); ++it) { add += it.value().count(); } return add; } } ProjectItemDataProvider::ProjectItemDataProvider(KDevelop::IQuickOpen* quickopen) : m_itemTypes(NoItems) , m_quickopen(quickopen) , m_addedItemsCountCache([this]() { return addedItems(m_addedItems); }) { } void ProjectItemDataProvider::setFilterText(const QString& text) { m_addedItems.clear(); m_addedItemsCountCache.markDirty(); QStringList search(text.split(QStringLiteral("::"), QString::SkipEmptyParts)); for (int a = 0; a < search.count(); ++a) { if (search[a].endsWith(':')) { //Don't get confused while the :: is being typed search[a] = search[a].left(search[a].length() - 1); } } if (!search.isEmpty() && search.back().endsWith('(')) { search.back().chop(1); } if (text.isEmpty() || search.isEmpty()) { m_filteredItems = m_currentItems; return; } KDevVarLengthArray cache; foreach (const QString& searchPart, search) { cache.append(SubstringCache(searchPart)); } if (!text.startsWith(m_currentFilter)) { m_filteredItems = m_currentItems; } m_currentFilter = text; QVector oldFiltered = m_filteredItems; QHash heights; m_filteredItems.clear(); foreach (const CodeModelViewItem& item, oldFiltered) { const QualifiedIdentifier& currentId = item.m_id; int last_pos = currentId.count() - 1; int current_height = 0; int distance = 0; //iter over each search item from last to first //this makes easier to calculate the distance based on where we hit the result or nothing //Iterating from the last item to the first is more efficient, as we want to match the //class/function name, which is the last item on the search fields and on the identifier. for (int b = search.count() - 1; b >= 0; --b) { //iter over each id for the current identifier, from last to first for (; last_pos >= 0; --last_pos, distance++) { // the more distant we are from the class definition, the less priority it will have current_height += distance * 10000; int result; //if the current search item is contained on the current identifier if ((result = cache[b].containedIn(currentId.at(last_pos))) >= 0) { //when we find a hit, whe add the distance to the searched word. //so the closest item will be displayed first current_height += result; if (b == 0) { heights[currentId.index()] = current_height; m_filteredItems << item; } break; } } } } //then, for the last part, we use the already built cache to sort the items according with their distance std::sort(m_filteredItems.begin(), m_filteredItems.end(), ClosestMatchToText(heights)); } KDevelop::QuickOpenDataPointer ProjectItemDataProvider::data(uint pos) const { //Check whether this position falls into an appended item-list, else apply the offset uint filteredItemOffset = 0; for (AddedItems::const_iterator it = m_addedItems.constBegin(); it != m_addedItems.constEnd(); ++it) { int offsetInAppended = pos - (it.key() + 1); if (offsetInAppended >= 0 && offsetInAppended < it.value().count()) { return it.value()[offsetInAppended]; } if (it.key() >= pos) { break; } filteredItemOffset += it.value().count(); } const uint a = pos - filteredItemOffset; if (a > ( uint )m_filteredItems.size()) { return KDevelop::QuickOpenDataPointer(); } const auto& filteredItem = m_filteredItems[a]; QList ret; KDevelop::DUChainReadLocker lock(DUChain::lock()); TopDUContext* ctx = DUChainUtils::standardContextForUrl(filteredItem.m_file.toUrl()); if (ctx) { QList decls = ctx->findDeclarations(filteredItem.m_id, CursorInRevision::invalid(), AbstractType::Ptr(), nullptr, DUContext::DirectQualifiedLookup); //Filter out forward-declarations or duplicate imported declarations foreach (Declaration* decl, decls) { bool filter = false; if (decls.size() > 1 && decl->isForwardDeclaration()) { filter = true; } else if (decl->url() != filteredItem.m_file && m_files.contains(decl->url())) { filter = true; } if (filter) { decls.removeOne(decl); } } + ret.reserve(ret.size() + decls.size()); foreach (Declaration* decl, decls) { DUChainItem item; item.m_item = decl; item.m_text = decl->qualifiedIdentifier().toString(); item.m_projectPath = findProjectForForPath(filteredItem.m_file); ret << QuickOpenDataPointer(new DUChainItemData(item)); } if (decls.isEmpty()) { DUChainItem item; item.m_text = filteredItem.m_id.toString(); item.m_projectPath = findProjectForForPath(filteredItem.m_file); ret << QuickOpenDataPointer(new DUChainItemData(item)); } } else { qCDebug(PLUGIN_QUICKOPEN) << "Could not find standard-context"; } if (!ret.isEmpty()) { QList append = ret.mid(1); if (!append.isEmpty()) { m_addedItemsCountCache.markDirty(); AddedItems addMap; for (AddedItems::iterator it = m_addedItems.begin(); it != m_addedItems.end(); ) { if (it.key() == pos) { //There already is appended data stored, nothing to do return ret.first(); } else if (it.key() > pos) { addMap[it.key() + append.count()] = it.value(); it = m_addedItems.erase(it); } else { ++it; } } m_addedItems.insert(pos, append); for (AddedItems::const_iterator it = addMap.constBegin(); it != addMap.constEnd(); ++it) { m_addedItems.insert(it.key(), it.value()); } } return ret.first(); } else { return KDevelop::QuickOpenDataPointer(); } } void ProjectItemDataProvider::reset() { m_files = m_quickopen->fileSet(); m_currentItems.clear(); m_addedItems.clear(); m_addedItemsCountCache.markDirty(); KDevelop::DUChainReadLocker lock(DUChain::lock()); foreach (const IndexedString& u, m_files) { uint count; const KDevelop::CodeModelItem* items; CodeModel::self().items(u, count, items); for (uint a = 0; a < count; ++a) { if (!items[a].id.isValid() || items[a].kind & CodeModelItem::ForwardDeclaration) { continue; } if (((m_itemTypes & Classes) && (items[a].kind & CodeModelItem::Class)) || ((m_itemTypes & Functions) && (items[a].kind & CodeModelItem::Function))) { QualifiedIdentifier id = items[a].id.identifier(); if (id.isEmpty() || id.at(0).identifier().isEmpty()) { // id.isEmpty() not always hit when .toString() is actually empty... // anyhow, this makes sure that we don't show duchain items without // any name that could be searched for. This happens e.g. in the c++ // plugin for anonymous structs or sometimes for declarations in macro // expressions continue; } m_currentItems << CodeModelViewItem(u, id); } } } m_filteredItems = m_currentItems; m_currentFilter.clear(); } uint ProjectItemDataProvider::itemCount() const { return m_filteredItems.count() + m_addedItemsCountCache.cachedResult(); } uint ProjectItemDataProvider::unfilteredItemCount() const { return m_currentItems.count() + m_addedItemsCountCache.cachedResult(); } QStringList ProjectItemDataProvider::supportedItemTypes() { - QStringList ret; - ret << i18n("Classes"); - ret << i18n("Functions"); + const QStringList ret{ + i18n("Classes"), + i18n("Functions"), + }; return ret; } void ProjectItemDataProvider::enableData(const QStringList& items, const QStringList& scopes) { //FIXME: property support different scopes if (scopes.contains(i18n("Project"))) { m_itemTypes = NoItems; if (items.contains(i18n("Classes"))) { m_itemTypes = ( ItemTypes )(m_itemTypes | Classes); } if (items.contains(i18n("Functions"))) { m_itemTypes = ( ItemTypes )(m_itemTypes | Functions); } } else { m_itemTypes = NoItems; } } diff --git a/plugins/quickopen/quickopenplugin.cpp b/plugins/quickopen/quickopenplugin.cpp index be4ee9ee26..401145fecb 100644 --- a/plugins/quickopen/quickopenplugin.cpp +++ b/plugins/quickopen/quickopenplugin.cpp @@ -1,1168 +1,1172 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "quickopenplugin.h" #include "quickopenwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "quickopenmodel.h" #include "projectfilequickopen.h" #include "projectitemquickopen.h" #include "declarationlistquickopen.h" #include "documentationquickopenprovider.h" #include "actionsquickopenprovider.h" #include "debug.h" #include #include #include #include using namespace KDevelop; const bool noHtmlDestriptionInOutline = true; class QuickOpenWidgetCreator { public: virtual ~QuickOpenWidgetCreator() { } virtual QuickOpenWidget* createWidget() = 0; virtual QString objectNameForLine() = 0; virtual void widgetShown() { } }; class StandardQuickOpenWidgetCreator : public QuickOpenWidgetCreator { public: StandardQuickOpenWidgetCreator(const QStringList& items, const QStringList& scopes) : m_items(items) , m_scopes(scopes) { } QString objectNameForLine() override { return QStringLiteral("Quickopen"); } void setItems(const QStringList& scopes, const QStringList& items) { m_scopes = scopes; m_items = items; } QuickOpenWidget* createWidget() override { QStringList useItems = m_items; if (useItems.isEmpty()) { useItems = QuickOpenPlugin::self()->lastUsedItems; } QStringList useScopes = m_scopes; if (useScopes.isEmpty()) { useScopes = QuickOpenPlugin::self()->lastUsedScopes; } return new QuickOpenWidget(i18n("Quick Open"), QuickOpenPlugin::self()->m_model, QuickOpenPlugin::self()->lastUsedItems, useScopes, false, true); } QStringList m_items; QStringList m_scopes; }; class OutlineFilter : public DUChainUtils::DUChainItemFilter { public: enum OutlineMode { Functions, FunctionsAndClasses }; explicit OutlineFilter(QVector& _items, OutlineMode _mode = FunctionsAndClasses) : items(_items) , mode(_mode) { } bool accept(Declaration* decl) override { if (decl->range().isEmpty()) { return false; } bool collectable = mode == Functions ? decl->isFunctionDeclaration() : (decl->isFunctionDeclaration() || (decl->internalContext() && decl->internalContext()->type() == DUContext::Class)); if (collectable) { DUChainItem item; item.m_item = IndexedDeclaration(decl); item.m_text = decl->toString(); items << item; return true; } else { return false; } } bool accept(DUContext* ctx) override { if (ctx->type() == DUContext::Class || ctx->type() == DUContext::Namespace || ctx->type() == DUContext::Global || ctx->type() == DUContext::Other || ctx->type() == DUContext::Helper) { return true; } else { return false; } } QVector& items; OutlineMode mode; }; K_PLUGIN_FACTORY_WITH_JSON(KDevQuickOpenFactory, "kdevquickopen.json", registerPlugin(); ) Declaration * cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return nullptr; } KDevelop::DUChainReadLocker lock(DUChain::lock()); return DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor(view->cursorPosition())).declaration); } ///The first definition that belongs to a context that surrounds the current cursor Declaration* cursorContextDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return nullptr; } KDevelop::DUChainReadLocker lock(DUChain::lock()); TopDUContext* ctx = DUChainUtils::standardContextForUrl(view->document()->url()); if (!ctx) { return nullptr; } KTextEditor::Cursor cursor(view->cursorPosition()); DUContext* subCtx = ctx->findContext(ctx->transformToLocalRevision(cursor)); while (subCtx && !subCtx->owner()) subCtx = subCtx->parentContext(); Declaration* definition = nullptr; if (!subCtx || !subCtx->owner()) { definition = DUChainUtils::declarationInLine(cursor, ctx); } else { definition = subCtx->owner(); } if (!definition) { return nullptr; } return definition; } //Returns only the name, no template-parameters or scope QString cursorItemText() { KDevelop::DUChainReadLocker lock(DUChain::lock()); Declaration* decl = cursorDeclaration(); if (!decl) { return QString(); } IDocument* doc = ICore::self()->documentController()->activeDocument(); if (!doc) { return QString(); } TopDUContext* context = DUChainUtils::standardContextForUrl(doc->url()); if (!context) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return QString(); } AbstractType::Ptr t = decl->abstractType(); IdentifiedType* idType = dynamic_cast(t.data()); if (idType && idType->declaration(context)) { decl = idType->declaration(context); } if (!decl->qualifiedIdentifier().isEmpty()) { return decl->qualifiedIdentifier().last().identifier().str(); } return QString(); } QuickOpenLineEdit* QuickOpenPlugin::createQuickOpenLineWidget() { return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(QStringList(), QStringList())); } QuickOpenLineEdit* QuickOpenPlugin::quickOpenLine(const QString& name) { QList lines = ICore::self()->uiController()->activeMainWindow()->findChildren(name); foreach (QuickOpenLineEdit* line, lines) { if (line->isVisible()) { return line; } } return nullptr; } static QuickOpenPlugin* staticQuickOpenPlugin = nullptr; QuickOpenPlugin* QuickOpenPlugin::self() { return staticQuickOpenPlugin; } void QuickOpenPlugin::createActionsForMainWindow(Sublime::MainWindow* /*window*/, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevquickopen.rc"); QAction* quickOpen = actions.addAction(QStringLiteral("quick_open")); quickOpen->setText(i18n("&Quick Open")); quickOpen->setIcon(QIcon::fromTheme(QStringLiteral("quickopen"))); actions.setDefaultShortcut(quickOpen, Qt::CTRL | Qt::ALT | Qt::Key_Q); connect(quickOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpen); QAction* quickOpenFile = actions.addAction(QStringLiteral("quick_open_file")); quickOpenFile->setText(i18n("Quick Open &File")); quickOpenFile->setIcon(QIcon::fromTheme(QStringLiteral("quickopen-file"))); actions.setDefaultShortcut(quickOpenFile, Qt::CTRL | Qt::ALT | Qt::Key_O); connect(quickOpenFile, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFile); QAction* quickOpenClass = actions.addAction(QStringLiteral("quick_open_class")); quickOpenClass->setText(i18n("Quick Open &Class")); quickOpenClass->setIcon(QIcon::fromTheme(QStringLiteral("quickopen-class"))); actions.setDefaultShortcut(quickOpenClass, Qt::CTRL | Qt::ALT | Qt::Key_C); connect(quickOpenClass, &QAction::triggered, this, &QuickOpenPlugin::quickOpenClass); QAction* quickOpenFunction = actions.addAction(QStringLiteral("quick_open_function")); quickOpenFunction->setText(i18n("Quick Open &Function")); quickOpenFunction->setIcon(QIcon::fromTheme(QStringLiteral("quickopen-function"))); actions.setDefaultShortcut(quickOpenFunction, Qt::CTRL | Qt::ALT | Qt::Key_M); connect(quickOpenFunction, &QAction::triggered, this, &QuickOpenPlugin::quickOpenFunction); QAction* quickOpenAlreadyOpen = actions.addAction(QStringLiteral("quick_open_already_open")); quickOpenAlreadyOpen->setText(i18n("Quick Open &Already Open File")); quickOpenAlreadyOpen->setIcon(QIcon::fromTheme(QStringLiteral("quickopen-file"))); connect(quickOpenAlreadyOpen, &QAction::triggered, this, &QuickOpenPlugin::quickOpenOpenFile); QAction* quickOpenDocumentation = actions.addAction(QStringLiteral("quick_open_documentation")); quickOpenDocumentation->setText(i18n("Quick Open &Documentation")); quickOpenDocumentation->setIcon(QIcon::fromTheme(QStringLiteral("quickopen-documentation"))); actions.setDefaultShortcut(quickOpenDocumentation, Qt::CTRL | Qt::ALT | Qt::Key_D); connect(quickOpenDocumentation, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDocumentation); QAction* quickOpenActions = actions.addAction(QStringLiteral("quick_open_actions")); quickOpenActions->setText(i18n("Quick Open &Actions")); actions.setDefaultShortcut(quickOpenActions, Qt::CTRL | Qt::ALT | Qt::Key_A); connect(quickOpenActions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenActions); m_quickOpenDeclaration = actions.addAction(QStringLiteral("quick_open_jump_declaration")); m_quickOpenDeclaration->setText(i18n("Jump to Declaration")); m_quickOpenDeclaration->setIcon(QIcon::fromTheme(QStringLiteral("go-jump-declaration"))); actions.setDefaultShortcut(m_quickOpenDeclaration, Qt::CTRL | Qt::Key_Period); connect(m_quickOpenDeclaration, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDeclaration, Qt::QueuedConnection); m_quickOpenDefinition = actions.addAction(QStringLiteral("quick_open_jump_definition")); m_quickOpenDefinition->setText(i18n("Jump to Definition")); m_quickOpenDefinition->setIcon(QIcon::fromTheme(QStringLiteral("go-jump-definition"))); actions.setDefaultShortcut(m_quickOpenDefinition, Qt::CTRL | Qt::Key_Comma); connect(m_quickOpenDefinition, &QAction::triggered, this, &QuickOpenPlugin::quickOpenDefinition, Qt::QueuedConnection); QWidgetAction* quickOpenLine = new QWidgetAction(this); quickOpenLine->setText(i18n("Embedded Quick Open")); // actions.setDefaultShortcut( quickOpenLine, Qt::CTRL | Qt::ALT | Qt::Key_E ); // connect(quickOpenLine, SIGNAL(triggered(bool)), this, SLOT(quickOpenLine(bool))); quickOpenLine->setDefaultWidget(createQuickOpenLineWidget()); actions.addAction(QStringLiteral("quick_open_line"), quickOpenLine); QAction* quickOpenNextFunction = actions.addAction(QStringLiteral("quick_open_next_function")); quickOpenNextFunction->setText(i18n("Next Function")); actions.setDefaultShortcut(quickOpenNextFunction, Qt::CTRL | Qt::ALT | Qt::Key_PageDown); connect(quickOpenNextFunction, &QAction::triggered, this, &QuickOpenPlugin::nextFunction); QAction* quickOpenPrevFunction = actions.addAction(QStringLiteral("quick_open_prev_function")); quickOpenPrevFunction->setText(i18n("Previous Function")); actions.setDefaultShortcut(quickOpenPrevFunction, Qt::CTRL | Qt::ALT | Qt::Key_PageUp); connect(quickOpenPrevFunction, &QAction::triggered, this, &QuickOpenPlugin::previousFunction); QAction* quickOpenNavigateFunctions = actions.addAction(QStringLiteral("quick_open_outline")); quickOpenNavigateFunctions->setText(i18n("Outline")); actions.setDefaultShortcut(quickOpenNavigateFunctions, Qt::CTRL | Qt::ALT | Qt::Key_N); connect(quickOpenNavigateFunctions, &QAction::triggered, this, &QuickOpenPlugin::quickOpenNavigateFunctions); } QuickOpenPlugin::QuickOpenPlugin(QObject* parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevquickopen"), parent) { staticQuickOpenPlugin = this; m_model = new QuickOpenModel(nullptr); KConfigGroup quickopengrp = KSharedConfig::openConfig()->group("QuickOpen"); - lastUsedScopes = quickopengrp.readEntry("SelectedScopes", QStringList() << i18n("Project") << i18n("Includes") << i18n("Includers") << i18n("Currently Open")); + lastUsedScopes = quickopengrp.readEntry("SelectedScopes", QStringList{ + i18n("Project"), + i18n("Includes"), + i18n("Includers"), + i18n("Currently Open")}); lastUsedItems = quickopengrp.readEntry("SelectedItems", QStringList()); { m_openFilesData = new OpenFilesDataProvider(); QStringList scopes, items; scopes << i18n("Currently Open"); items << i18n("Files"); m_model->registerProvider(scopes, items, m_openFilesData); } { m_projectFileData = new ProjectFileDataProvider(); QStringList scopes, items; scopes << i18n("Project"); items << i18n("Files"); m_model->registerProvider(scopes, items, m_projectFileData); } { m_projectItemData = new ProjectItemDataProvider(this); QStringList scopes, items; scopes << i18n("Project"); items << ProjectItemDataProvider::supportedItemTypes(); m_model->registerProvider(scopes, items, m_projectItemData); } { m_documentationItemData = new DocumentationQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Documentation"); m_model->registerProvider(scopes, items, m_documentationItemData); } { m_actionsItemData = new ActionsQuickOpenProvider; QStringList scopes, items; scopes << i18n("Includes"); items << i18n("Actions"); m_model->registerProvider(scopes, items, m_actionsItemData); } } QuickOpenPlugin::~QuickOpenPlugin() { freeModel(); delete m_model; delete m_projectFileData; delete m_projectItemData; delete m_openFilesData; delete m_documentationItemData; delete m_actionsItemData; } void QuickOpenPlugin::unload() { } ContextMenuExtension QuickOpenPlugin::contextMenuExtension(Context* context, QWidget* parent) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension(context, parent); KDevelop::DeclarationContext* codeContext = dynamic_cast(context); if (!codeContext) { return menuExt; } DUChainReadLocker readLock; Declaration* decl(codeContext->declaration().data()); if (decl) { const bool isDef = FunctionDefinition::definition(decl); if (codeContext->use().isValid() || !isDef) { menuExt.addAction(KDevelop::ContextMenuExtension::NavigationGroup, m_quickOpenDeclaration); } if (isDef) { menuExt.addAction(KDevelop::ContextMenuExtension::NavigationGroup, m_quickOpenDefinition); } } return menuExt; } void QuickOpenPlugin::showQuickOpen(const QStringList& items) { if (!freeModel()) { return; } QStringList initialItems = items; QStringList useScopes = lastUsedScopes; if (!useScopes.contains(i18n("Currently Open"))) { useScopes << i18n("Currently Open"); } showQuickOpenWidget(initialItems, useScopes, false); } void QuickOpenPlugin::showQuickOpen(ModelTypes modes) { if (!freeModel()) { return; } QStringList initialItems; if (modes & Files || modes & OpenFiles) { initialItems << i18n("Files"); } if (modes & Functions) { initialItems << i18n("Functions"); } if (modes & Classes) { initialItems << i18n("Classes"); } QStringList useScopes; if (modes != OpenFiles) { useScopes = lastUsedScopes; } if ((modes & OpenFiles) && !useScopes.contains(i18n("Currently Open"))) { useScopes << i18n("Currently Open"); } bool preselectText = (!(modes & Files) || modes == QuickOpenPlugin::All); showQuickOpenWidget(initialItems, useScopes, preselectText); } void QuickOpenPlugin::showQuickOpenWidget(const QStringList& items, const QStringList& scopes, bool preselectText) { QuickOpenWidgetDialog* dialog = new QuickOpenWidgetDialog(i18n("Quick Open"), m_model, items, scopes); m_currentWidgetHandler = dialog; if (preselectText) { KDevelop::IDocument* currentDoc = core()->documentController()->activeDocument(); if (currentDoc && currentDoc->isTextDocument()) { QString preselected = currentDoc->textSelection().isEmpty() ? currentDoc->textWord() : currentDoc->textDocument()->text(currentDoc->textSelection()); dialog->widget()->setPreselectedText(preselected); } } connect(dialog->widget(), &QuickOpenWidget::scopesChanged, this, &QuickOpenPlugin::storeScopes); //Not connecting itemsChanged to storeItems, as showQuickOpen doesn't use lastUsedItems and so shouldn't store item changes //connect( dialog->widget(), SIGNAL(itemsChanged(QStringList)), this, SLOT(storeItems(QStringList)) ); dialog->widget()->ui.itemsButton->setEnabled(false); if (quickOpenLine()) { quickOpenLine()->showWithWidget(dialog->widget()); dialog->deleteLater(); } else { dialog->run(); } } void QuickOpenPlugin::storeScopes(const QStringList& scopes) { lastUsedScopes = scopes; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry("SelectedScopes", scopes); } void QuickOpenPlugin::storeItems(const QStringList& items) { lastUsedItems = items; KConfigGroup grp = KSharedConfig::openConfig()->group("QuickOpen"); grp.writeEntry("SelectedItems", items); } void QuickOpenPlugin::quickOpen() { if (quickOpenLine()) { //Same as clicking on Quick Open quickOpenLine()->setFocus(); } else { showQuickOpen(All); } } void QuickOpenPlugin::quickOpenFile() { showQuickOpen(( ModelTypes )(Files | OpenFiles)); } void QuickOpenPlugin::quickOpenFunction() { showQuickOpen(Functions); } void QuickOpenPlugin::quickOpenClass() { showQuickOpen(Classes); } void QuickOpenPlugin::quickOpenOpenFile() { showQuickOpen(OpenFiles); } void QuickOpenPlugin::quickOpenDocumentation() { showQuickOpenWidget(QStringList(i18n("Documentation")), QStringList(i18n("Includes")), true); } void QuickOpenPlugin::quickOpenActions() { showQuickOpenWidget(QStringList(i18n("Actions")), QStringList(i18n("Includes")), true); } QSet QuickOpenPlugin::fileSet() const { return m_model->fileSet(); } void QuickOpenPlugin::registerProvider(const QStringList& scope, const QStringList& type, KDevelop::QuickOpenDataProviderBase* provider) { m_model->registerProvider(scope, type, provider); } bool QuickOpenPlugin::removeProvider(KDevelop::QuickOpenDataProviderBase* provider) { m_model->removeProvider(provider); return true; } void QuickOpenPlugin::quickOpenDeclaration() { if (jumpToSpecialObject()) { return; } KDevelop::DUChainReadLocker lock(DUChain::lock()); Declaration* decl = cursorDeclaration(); if (!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } decl->activateSpecialization(); IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if (u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } QWidget* QuickOpenPlugin::specialObjectNavigationWidget() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return nullptr; } QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QWidget* w = language->specialLanguageObjectNavigationWidget(url, KTextEditor::Cursor(view->cursorPosition())); if (w) { return w; } } return nullptr; } QPair QuickOpenPlugin::specialObjectJumpPosition() const { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return qMakePair(QUrl(), KTextEditor::Cursor()); } QUrl url = ICore::self()->documentController()->activeDocument()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(url); foreach (const auto language, languages) { QPair pos = language->specialLanguageObjectJumpCursor(url, KTextEditor::Cursor(view->cursorPosition())); if (pos.second.isValid()) { return pos; } } return qMakePair(QUrl(), KTextEditor::Cursor::invalid()); } bool QuickOpenPlugin::jumpToSpecialObject() { QPair pos = specialObjectJumpPosition(); if (pos.second.isValid()) { if (pos.first.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for special language object"; return false; } ICore::self()->documentController()->openDocument(pos.first, pos.second); return true; } return false; } void QuickOpenPlugin::quickOpenDefinition() { if (jumpToSpecialObject()) { return; } KDevelop::DUChainReadLocker lock(DUChain::lock()); Declaration* decl = cursorDeclaration(); if (!decl) { qCDebug(PLUGIN_QUICKOPEN) << "Found no declaration for cursor, cannot jump"; return; } IndexedString u = decl->url(); KTextEditor::Cursor c = decl->rangeInCurrentRevision().start(); if (FunctionDefinition* def = FunctionDefinition::definition(decl)) { def->activateSpecialization(); u = def->url(); c = def->rangeInCurrentRevision().start(); } else { qCDebug(PLUGIN_QUICKOPEN) << "Found no definition for declaration"; decl->activateSpecialization(); } if (u.isEmpty()) { qCDebug(PLUGIN_QUICKOPEN) << "Got empty url for declaration" << decl->toString(); return; } lock.unlock(); core()->documentController()->openDocument(u.toUrl(), c); } bool QuickOpenPlugin::freeModel() { if (m_currentWidgetHandler) { delete m_currentWidgetHandler; } m_currentWidgetHandler = nullptr; return true; } void QuickOpenPlugin::nextFunction() { jumpToNearestFunction(NextFunction); } void QuickOpenPlugin::previousFunction() { jumpToNearestFunction(PreviousFunction); } void QuickOpenPlugin::jumpToNearestFunction(QuickOpenPlugin::FunctionJumpDirection direction) { IDocument* doc = ICore::self()->documentController()->activeDocument(); if (!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock(DUChain::lock()); TopDUContext* context = DUChainUtils::standardContextForUrl(doc->url()); if (!context) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } QVector items; OutlineFilter filter(items, OutlineFilter::Functions); DUChainUtils::collectItems(context, filter); CursorInRevision cursor = context->transformToLocalRevision(KTextEditor::Cursor(doc->cursorPosition())); if (!cursor.isValid()) { return; } Declaration* nearestDeclBefore = nullptr; int distanceBefore = INT_MIN; Declaration* nearestDeclAfter = nullptr; int distanceAfter = INT_MAX; for (int i = 0; i < items.count(); ++i) { Declaration* decl = items[i].m_item.data(); int distance = decl->range().start.line - cursor.line; if (distance < 0 && distance >= distanceBefore) { distanceBefore = distance; nearestDeclBefore = decl; } else if (distance > 0 && distance <= distanceAfter) { distanceAfter = distance; nearestDeclAfter = decl; } } CursorInRevision c = CursorInRevision::invalid(); if (direction == QuickOpenPlugin::NextFunction && nearestDeclAfter) { c = nearestDeclAfter->range().start; } else if (direction == QuickOpenPlugin::PreviousFunction && nearestDeclBefore) { c = nearestDeclBefore->range().start; } KTextEditor::Cursor textCursor = KTextEditor::Cursor::invalid(); if (c.isValid()) { textCursor = context->transformFromLocalRevision(c); } lock.unlock(); if (textCursor.isValid()) { core()->documentController()->openDocument(doc->url(), textCursor); } else { qCDebug(PLUGIN_QUICKOPEN) << "No declaration to jump to"; } } struct CreateOutlineDialog { CreateOutlineDialog() : dialog(nullptr) , cursorDecl(nullptr) , model(nullptr) { } void start() { if (!QuickOpenPlugin::self()->freeModel()) { return; } IDocument* doc = ICore::self()->documentController()->activeDocument(); if (!doc) { qCDebug(PLUGIN_QUICKOPEN) << "No active document"; return; } KDevelop::DUChainReadLocker lock(DUChain::lock()); TopDUContext* context = DUChainUtils::standardContextForUrl(doc->url()); if (!context) { qCDebug(PLUGIN_QUICKOPEN) << "Got no standard context"; return; } model = new QuickOpenModel(nullptr); OutlineFilter filter(items); DUChainUtils::collectItems(context, filter); if (noHtmlDestriptionInOutline) { for (int a = 0; a < items.size(); ++a) { items[a].m_noHtmlDestription = true; } } cursorDecl = cursorContextDeclaration(); model->registerProvider(QStringList(), QStringList(), new DeclarationListDataProvider(QuickOpenPlugin::self(), items, true)); dialog = new QuickOpenWidgetDialog(i18n("Outline"), model, QStringList(), QStringList(), true); dialog->widget()->setSortingEnabled(true); model->setParent(dialog->widget()); } void finish() { //Select the declaration that contains the cursor if (cursorDecl && dialog) { int num = 0; foreach (const DUChainItem& item, items) { if (item.m_item.data() == cursorDecl) { QModelIndex index(model->index(num, 0, QModelIndex())); // Need to invoke the scrolling later. If we did it now, then it wouldn't have any effect, // apparently because the widget internals aren't initialized yet properly (although we've // already called 'widget->show()'. auto list = dialog->widget()->ui.list; QMetaObject::invokeMethod(list, "setCurrentIndex", Qt::QueuedConnection, Q_ARG(QModelIndex, index)); QMetaObject::invokeMethod(list, "scrollTo", Qt::QueuedConnection, Q_ARG(QModelIndex, index), Q_ARG(QAbstractItemView::ScrollHint, QAbstractItemView::PositionAtCenter)); } ++num; } } } QPointer dialog; Declaration* cursorDecl; QVector items; QuickOpenModel* model; }; class OutlineQuickopenWidgetCreator : public QuickOpenWidgetCreator { public: OutlineQuickopenWidgetCreator(const QStringList& /*scopes*/, const QStringList& /*items*/) : m_creator(nullptr) { } ~OutlineQuickopenWidgetCreator() override { delete m_creator; } QuickOpenWidget* createWidget() override { delete m_creator; m_creator = new CreateOutlineDialog; m_creator->start(); if (!m_creator->dialog) { return nullptr; } m_creator->dialog->deleteLater(); return m_creator->dialog->widget(); } void widgetShown() override { if (m_creator) { m_creator->finish(); delete m_creator; m_creator = nullptr; } } QString objectNameForLine() override { return QStringLiteral("Outline"); } CreateOutlineDialog* m_creator; }; void QuickOpenPlugin::quickOpenNavigateFunctions() { CreateOutlineDialog create; create.start(); if (!create.dialog) { return; } m_currentWidgetHandler = create.dialog; QuickOpenLineEdit* line = quickOpenLine(QStringLiteral("Outline")); if (!line) { line = quickOpenLine(); } if (line) { line->showWithWidget(create.dialog->widget()); create.dialog->deleteLater(); } else { create.dialog->run(); } create.finish(); } QuickOpenLineEdit::QuickOpenLineEdit(QuickOpenWidgetCreator* creator) : m_widget(nullptr) , m_forceUpdate(false) , m_widgetCreator(creator) { setFont(qApp->font("QToolButton")); setMinimumWidth(200); setMaximumWidth(400); deactivate(); setDefaultText(i18n("Quick Open...")); setToolTip(i18n("Search for files, classes, functions and more," " allowing you to quickly navigate in your source code.")); setObjectName(m_widgetCreator->objectNameForLine()); setFocusPolicy(Qt::ClickFocus); } QuickOpenLineEdit::~QuickOpenLineEdit() { delete m_widget; delete m_widgetCreator; } bool QuickOpenLineEdit::insideThis(QObject* object) { while (object) { qCDebug(PLUGIN_QUICKOPEN) << object; if (object == this || object == m_widget) { return true; } object = object->parent(); } return false; } void QuickOpenLineEdit::widgetDestroyed(QObject* obj) { Q_UNUSED(obj); // need to use a queued connection here, because this function is called in ~QWidget! // => QuickOpenWidget instance is half-destructed => connections are not yet cleared // => clear() will trigger signals which will operate on the invalid QuickOpenWidget // So, just wait until properly destructed QMetaObject::invokeMethod(this, "deactivate", Qt::QueuedConnection); } void QuickOpenLineEdit::showWithWidget(QuickOpenWidget* widget) { connect(widget, &QuickOpenWidget::destroyed, this, &QuickOpenLineEdit::widgetDestroyed); qCDebug(PLUGIN_QUICKOPEN) << "storing widget" << widget; deactivate(); if (m_widget) { qCDebug(PLUGIN_QUICKOPEN) << "deleting" << m_widget; delete m_widget; } m_widget = widget; m_forceUpdate = true; setFocus(); } void QuickOpenLineEdit::focusInEvent(QFocusEvent* ev) { QLineEdit::focusInEvent(ev); // delete m_widget; qCDebug(PLUGIN_QUICKOPEN) << "got focus"; qCDebug(PLUGIN_QUICKOPEN) << "old widget" << m_widget << "force update:" << m_forceUpdate; if (m_widget && !m_forceUpdate) { return; } if (!m_forceUpdate && !QuickOpenPlugin::self()->freeModel()) { deactivate(); return; } m_forceUpdate = false; if (!m_widget) { m_widget = m_widgetCreator->createWidget(); if (!m_widget) { deactivate(); return; } } activate(); m_widget->showStandardButtons(false); m_widget->showSearchField(false); m_widget->setParent(nullptr, Qt::ToolTip); m_widget->setFocusPolicy(Qt::NoFocus); m_widget->setAlternativeSearchField(this); QuickOpenPlugin::self()->m_currentWidgetHandler = m_widget; connect(m_widget.data(), &QuickOpenWidget::ready, this, &QuickOpenLineEdit::deactivate); connect(m_widget.data(), &QuickOpenWidget::scopesChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeScopes); connect(m_widget.data(), &QuickOpenWidget::itemsChanged, QuickOpenPlugin::self(), &QuickOpenPlugin::storeItems); Q_ASSERT(m_widget->ui.searchLine == this); m_widget->prepareShow(); QRect widgetGeometry = QRect(mapToGlobal(QPoint(0, height())), mapToGlobal(QPoint(width(), height() + 400))); widgetGeometry.setWidth(700); ///@todo Waste less space QRect screenGeom = QApplication::desktop()->screenGeometry(this); if (widgetGeometry.right() > screenGeom.right()) { widgetGeometry.moveRight(screenGeom.right()); } if (widgetGeometry.bottom() > screenGeom.bottom()) { widgetGeometry.moveBottom(mapToGlobal(QPoint(0, 0)).y()); } m_widget->setGeometry(widgetGeometry); m_widget->show(); m_widgetCreator->widgetShown(); } void QuickOpenLineEdit::hideEvent(QHideEvent* ev) { QWidget::hideEvent(ev); if (m_widget) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); } // deactivate(); } bool QuickOpenLineEdit::eventFilter(QObject* obj, QEvent* e) { if (!m_widget) { return IQuickOpenLine::eventFilter(obj, e); } switch (e->type()) { case QEvent::KeyPress: case QEvent::ShortcutOverride: if (static_cast(e)->key() == Qt::Key_Escape) { deactivate(); e->accept(); return true; // eat event } break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); break; // handle bug 260657 - "Outline menu doesn't follow main window on its move" case QEvent::Move: { if (QWidget* widget = qobject_cast(obj)) { // close the outline menu in case a parent widget moved if (widget->isAncestorOf(this)) { qCDebug(PLUGIN_QUICKOPEN) << "closing because of parent widget move"; deactivate(); } } break; } case QEvent::FocusIn: if (dynamic_cast(obj)) { QFocusEvent* focusEvent = dynamic_cast(e); Q_ASSERT(focusEvent); //Eat the focus event, keep the focus qCDebug(PLUGIN_QUICKOPEN) << "focus change" << "inside this: " << insideThis(obj) << "this" << this << "obj" << obj; if (obj == this) { break; } qCDebug(PLUGIN_QUICKOPEN) << "reason" << focusEvent->reason(); if (focusEvent->reason() != Qt::MouseFocusReason && focusEvent->reason() != Qt::ActiveWindowFocusReason) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); break; } if (!insideThis(obj)) { deactivate(); } } else if (obj != this) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); } break; default: break; } return IQuickOpenLine::eventFilter(obj, e); } void QuickOpenLineEdit::activate() { qCDebug(PLUGIN_QUICKOPEN) << "activating"; setText(QString()); setStyleSheet(QString()); qApp->installEventFilter(this); } void QuickOpenLineEdit::deactivate() { qCDebug(PLUGIN_QUICKOPEN) << "deactivating"; clear(); if (m_widget || hasFocus()) { QMetaObject::invokeMethod(this, "checkFocus", Qt::QueuedConnection); } if (m_widget) { m_widget->deleteLater(); } m_widget = nullptr; qApp->removeEventFilter(this); } void QuickOpenLineEdit::checkFocus() { qCDebug(PLUGIN_QUICKOPEN) << "checking focus" << m_widget; if (m_widget) { QWidget* focusWidget = QApplication::focusWidget(); bool focusWidgetInsideThis = focusWidget ? insideThis(focusWidget) : false; if (QApplication::focusWindow() && isVisible() && !isHidden() && (!focusWidget || (focusWidget && focusWidgetInsideThis))) { qCDebug(PLUGIN_QUICKOPEN) << "setting focus to line edit"; activateWindow(); setFocus(); } else { qCDebug(PLUGIN_QUICKOPEN) << "deactivating because check failed, focusWidget" << focusWidget << "insideThis" << focusWidgetInsideThis; deactivate(); } } else { if (ICore::self()->documentController()->activeDocument()) { ICore::self()->documentController()->activateDocument(ICore::self()->documentController()->activeDocument()); } //Make sure the focus is somewehre else, even if there is no active document setEnabled(false); setEnabled(true); } } IQuickOpenLine* QuickOpenPlugin::createQuickOpenLine(const QStringList& scopes, const QStringList& type, IQuickOpen::QuickOpenType kind) { if (kind == Outline) { return new QuickOpenLineEdit(new OutlineQuickopenWidgetCreator(scopes, type)); } else { return new QuickOpenLineEdit(new StandardQuickOpenWidgetCreator(scopes, type)); } } #include "quickopenplugin.moc" diff --git a/plugins/standardoutputview/outputwidget.cpp b/plugins/standardoutputview/outputwidget.cpp index a1ba08a1b2..c256567cbe 100644 --- a/plugins/standardoutputview/outputwidget.cpp +++ b/plugins/standardoutputview/outputwidget.cpp @@ -1,686 +1,686 @@ /* This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * Copyright 2007 Dukju Ahn * * 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 "outputwidget.h" #include "standardoutputview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "outputmodel.h" #include "toolviewdata.h" #include Q_DECLARE_METATYPE(QTreeView*) OutputWidget::OutputWidget(QWidget* parent, const ToolViewData* tvdata) : QWidget( parent ) , m_tabwidget(nullptr) , m_stackwidget(nullptr) , data(tvdata) , m_closeButton(nullptr) , m_closeOthersAction(nullptr) , m_nextAction(nullptr) , m_previousAction(nullptr) , m_activateOnSelect(nullptr) , m_focusOnSelect(nullptr) , m_filterInput(nullptr) , m_filterAction(nullptr) { setWindowTitle(i18n("Output View")); setWindowIcon(tvdata->icon); QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); if( data->type & KDevelop::IOutputView::MultipleView ) { m_tabwidget = new QTabWidget(this); layout->addWidget( m_tabwidget ); m_closeButton = new QToolButton( this ); connect( m_closeButton, &QToolButton::clicked, this, &OutputWidget::closeActiveView ); m_closeButton->setIcon( QIcon::fromTheme( QStringLiteral( "tab-close") ) ); m_closeButton->setToolTip( i18n( "Close the currently active output view") ); m_closeButton->setAutoRaise(true); m_closeOthersAction = new QAction( this ); connect(m_closeOthersAction, &QAction::triggered, this, &OutputWidget::closeOtherViews); m_closeOthersAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-close-other"))); m_closeOthersAction->setToolTip( i18n( "Close all other output views" ) ); m_closeOthersAction->setText( m_closeOthersAction->toolTip() ); addAction(m_closeOthersAction); m_tabwidget->setCornerWidget(m_closeButton, Qt::TopRightCorner); m_tabwidget->setDocumentMode(true); } else if ( data->type == KDevelop::IOutputView::HistoryView ) { m_stackwidget = new QStackedWidget( this ); layout->addWidget( m_stackwidget ); m_previousAction = new QAction( QIcon::fromTheme( QStringLiteral( "arrow-left" ) ), i18n("Previous Output"), this ); connect(m_previousAction, &QAction::triggered, this, &OutputWidget::previousOutput); addAction(m_previousAction); m_nextAction = new QAction( QIcon::fromTheme( QStringLiteral( "arrow-right" ) ), i18n("Next Output"), this ); connect(m_nextAction, &QAction::triggered, this, &OutputWidget::nextOutput); addAction(m_nextAction); } addAction(dynamic_cast(data->plugin->actionCollection()->action(QStringLiteral("prev_error")))); addAction(dynamic_cast(data->plugin->actionCollection()->action(QStringLiteral("next_error")))); m_activateOnSelect = new KToggleAction( QIcon(), i18n("Select activated Item"), this ); m_activateOnSelect->setChecked( true ); m_focusOnSelect = new KToggleAction( QIcon(), i18n("Focus when selecting Item"), this ); m_focusOnSelect->setChecked( false ); if( data->option & KDevelop::IOutputView::ShowItemsButton ) { addAction(m_activateOnSelect); addAction(m_focusOnSelect); } QAction *separator = new QAction(this); separator->setSeparator(true); addAction(separator); QAction* action; action = new QAction(QIcon::fromTheme(QStringLiteral("go-first")), i18n("First Item"), this); connect(action, &QAction::triggered, this, &OutputWidget::selectFirstItem); addAction(action); action = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Previous Item"), this); connect(action, &QAction::triggered, this, &OutputWidget::selectPreviousItem); addAction(action); action = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Next Item"), this); connect(action, &QAction::triggered, this, &OutputWidget::selectNextItem); addAction(action); action = new QAction(QIcon::fromTheme(QStringLiteral("go-last")), i18n("Last Item"), this); connect(action, &QAction::triggered, this, &OutputWidget::selectLastItem); addAction(action); QAction* selectAllAction = KStandardAction::selectAll(this, SLOT(selectAll()), this); selectAllAction->setShortcut(QKeySequence()); //FIXME: why does CTRL-A conflict with Katepart (while CTRL-Cbelow doesn't) ? selectAllAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); addAction(selectAllAction); QAction* copyAction = KStandardAction::copy(this, SLOT(copySelection()), this); copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); addAction(copyAction); QAction *clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18n("Clear"), this); connect(clearAction, &QAction::triggered, this, &OutputWidget::clearModel); addAction(clearAction); if( data->option & KDevelop::IOutputView::AddFilterAction ) { QAction *separator = new QAction(this); separator->setSeparator(true); addAction(separator); m_filterInput = new KExpandableLineEdit(this); m_filterInput->setPlaceholderText(i18n("Search...")); m_filterInput->setClearButtonEnabled(true); m_filterInput->setToolTip(i18n("Enter a wild card string to filter the output view")); m_filterAction = new QWidgetAction(this); m_filterAction->setText(m_filterInput->placeholderText()); connect(m_filterAction, &QAction::triggered, this, [this]() {m_filterInput->setFocus();}); m_filterAction->setDefaultWidget(m_filterInput); addAction(m_filterAction); connect(m_filterInput, &QLineEdit::textEdited, this, &OutputWidget::outputFilter ); if( data->type & KDevelop::IOutputView::MultipleView ) { connect(m_tabwidget, &QTabWidget::currentChanged, this, &OutputWidget::updateFilter); } else if ( data->type == KDevelop::IOutputView::HistoryView ) { connect(m_stackwidget, &QStackedWidget::currentChanged, this, &OutputWidget::updateFilter); } } addActions(data->actionList); connect( data, &ToolViewData::outputAdded, this, &OutputWidget::addOutput ); connect( this, &OutputWidget::outputRemoved, data->plugin, &StandardOutputView::outputRemoved ); foreach( int id, data->outputdata.keys() ) { changeModel( id ); changeDelegate( id ); } enableActions(); } void OutputWidget::clearModel() { auto view = qobject_cast(currentWidget()); if( !view || !view->isVisible()) return; KDevelop::OutputModel *outputModel = nullptr; if (auto proxy = qobject_cast(view->model())) { outputModel = qobject_cast(proxy->sourceModel()); } else { outputModel = qobject_cast(view->model()); } outputModel->clear(); } void OutputWidget::addOutput( int id ) { QTreeView* listview = createListView(id); setCurrentWidget( listview ); connect( data->outputdata.value(id), &OutputData::modelChanged, this, &OutputWidget::changeModel); connect( data->outputdata.value(id), &OutputData::delegateChanged, this, &OutputWidget::changeDelegate); enableActions(); } void OutputWidget::setCurrentWidget( QTreeView* view ) { if( data->type & KDevelop::IOutputView::MultipleView ) { m_tabwidget->setCurrentWidget( view ); } else if( data->type & KDevelop::IOutputView::HistoryView ) { m_stackwidget->setCurrentWidget( view ); } } void OutputWidget::changeDelegate( int id ) { if( data->outputdata.contains( id ) && m_views.contains( id ) ) { m_views.value(id)->setItemDelegate(data->outputdata.value(id)->delegate); } else { addOutput(id); } } void OutputWidget::changeModel( int id ) { if( data->outputdata.contains( id ) && m_views.contains( id ) ) { OutputData* od = data->outputdata.value(id); m_views.value( id )->setModel(od->model); } else { addOutput( id ); } } void OutputWidget::removeOutput( int id ) { if( data->outputdata.contains( id ) && m_views.contains( id ) ) { QTreeView* view = m_views.value(id); if( data->type & KDevelop::IOutputView::MultipleView || data->type & KDevelop::IOutputView::HistoryView ) { if( data->type & KDevelop::IOutputView::MultipleView ) { int idx = m_tabwidget->indexOf( view ); if( idx != -1 ) { m_tabwidget->removeTab( idx ); if( m_proxyModels.contains( idx ) ) { delete m_proxyModels.take( idx ); m_filters.remove( idx ); } } } else { int idx = m_stackwidget->indexOf( view ); if( idx != -1 && m_proxyModels.contains( idx ) ) { delete m_proxyModels.take( idx ); m_filters.remove( idx ); } m_stackwidget->removeWidget( view ); } delete view; } else { m_views.value( id )->setModel( nullptr ); m_views.value( id )->setItemDelegate( nullptr ); if( m_proxyModels.contains( 0 ) ) { delete m_proxyModels.take( 0 ); m_filters.remove( 0 ); } } m_views.remove( id ); emit outputRemoved( data->toolViewId, id ); } enableActions(); } void OutputWidget::closeActiveView() { QWidget* widget = m_tabwidget->currentWidget(); if( !widget ) return; foreach( int id, m_views.keys() ) { if( m_views.value(id) == widget ) { OutputData* od = data->outputdata.value(id); if( od->behaviour & KDevelop::IOutputView::AllowUserClose ) { data->plugin->removeOutput( id ); } } } enableActions(); } void OutputWidget::closeOtherViews() { QWidget* widget = m_tabwidget->currentWidget(); if (!widget) return; foreach (int id, m_views.keys()) { if (m_views.value(id) == widget) { continue; // leave the active view open } OutputData* od = data->outputdata.value(id); if (od->behaviour & KDevelop::IOutputView::AllowUserClose) { data->plugin->removeOutput( id ); } } enableActions(); } QWidget* OutputWidget::currentWidget() const { QWidget* widget; if( data->type & KDevelop::IOutputView::MultipleView ) { widget = m_tabwidget->currentWidget(); } else if( data->type & KDevelop::IOutputView::HistoryView ) { widget = m_stackwidget->currentWidget(); } else { widget = m_views.begin().value(); } return widget; } KDevelop::IOutputViewModel *OutputWidget::outputViewModel() const { auto view = qobject_cast(currentWidget()); if( !view || !view->isVisible()) return nullptr; QAbstractItemModel *absmodel = view->model(); KDevelop::IOutputViewModel *iface = dynamic_cast(absmodel); if ( ! iface ) { // try if it's a proxy model? if ( QAbstractProxyModel* proxy = qobject_cast(absmodel) ) { iface = dynamic_cast(proxy->sourceModel()); } } return iface; } void OutputWidget::eventuallyDoFocus() { QWidget* widget = currentWidget(); if( m_focusOnSelect->isChecked() && !widget->hasFocus() ) { widget->setFocus( Qt::OtherFocusReason ); } } QAbstractItemView *OutputWidget::outputView() const { return qobject_cast(currentWidget()); } void OutputWidget::activateIndex(const QModelIndex &index, QAbstractItemView *view, KDevelop::IOutputViewModel *iface) { if( ! index.isValid() ) return; int tabIndex = currentOutputIndex(); QModelIndex sourceIndex = index; QModelIndex viewIndex = index; if( QAbstractProxyModel* proxy = m_proxyModels.value(tabIndex) ) { if ( index.model() == proxy ) { // index is from the proxy, map it to the source sourceIndex = proxy->mapToSource(index); } else if (proxy == view->model()) { // index is from the source, map it to the proxy viewIndex = proxy->mapFromSource(index); } } view->setCurrentIndex( viewIndex ); view->scrollTo( viewIndex ); if( m_activateOnSelect->isChecked() ) { iface->activate( sourceIndex ); } } void OutputWidget::selectFirstItem() { selectItem(First); } void OutputWidget::selectNextItem() { selectItem(Next); } void OutputWidget::selectPreviousItem() { selectItem(Previous); } void OutputWidget::selectLastItem() { selectItem(Last); } void OutputWidget::selectItem(SelectionMode selectionMode) { auto view = outputView(); auto iface = outputViewModel(); if ( ! view || ! iface ) return; eventuallyDoFocus(); auto index = view->currentIndex(); if (QAbstractProxyModel* proxy = m_proxyModels.value(currentOutputIndex())) { if ( index.model() == proxy ) { // index is from the proxy, map it to the source index = proxy->mapToSource(index); } } QModelIndex newIndex; switch (selectionMode) { case First: newIndex = iface->firstHighlightIndex(); break; case Next: newIndex = iface->nextHighlightIndex( index ); break; case Previous: newIndex = iface->previousHighlightIndex( index ); break; case Last: newIndex = iface->lastHighlightIndex(); break; } qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "old:" << index << "- new:" << newIndex; activateIndex(newIndex, view, iface); } void OutputWidget::activate(const QModelIndex& index) { auto iface = outputViewModel(); auto view = outputView(); if( ! view || ! iface ) return; activateIndex(index, view, iface); } QTreeView* OutputWidget::createListView(int id) { auto createHelper = [&]() -> QTreeView* { KDevelop::FocusedTreeView* listview = new KDevelop::FocusedTreeView(this); listview->setEditTriggers( QAbstractItemView::NoEditTriggers ); listview->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); //Always enable the scrollbar, so it doesn't flash around listview->setHeaderHidden(true); listview->setUniformRowHeights(true); listview->setRootIsDecorated(false); listview->setSelectionMode( QAbstractItemView::ContiguousSelection ); if (data->outputdata.value(id)->behaviour & KDevelop::IOutputView::AutoScroll) { listview->setAutoScrollAtEnd(true); } connect(listview, &QTreeView::activated, this, &OutputWidget::activate); connect(listview, &QTreeView::clicked, this, &OutputWidget::activate); return listview; }; QTreeView* listview = nullptr; if( !m_views.contains(id) ) { bool newView = true; if( data->type & KDevelop::IOutputView::MultipleView || data->type & KDevelop::IOutputView::HistoryView ) { qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "creating listview"; listview = createHelper(); if( data->type & KDevelop::IOutputView::MultipleView ) { m_tabwidget->addTab( listview, data->outputdata.value(id)->title ); } else { m_stackwidget->addWidget( listview ); m_stackwidget->setCurrentWidget( listview ); } } else { if( m_views.isEmpty() ) { listview = createHelper(); layout()->addWidget( listview ); } else { listview = m_views.begin().value(); newView = false; } } m_views[id] = listview; changeModel( id ); changeDelegate( id ); if (newView) listview->scrollToBottom(); } else { listview = m_views.value(id); } enableActions(); return listview; } void OutputWidget::raiseOutput(int id) { if( m_views.contains(id) ) { if( data->type & KDevelop::IOutputView::MultipleView ) { int idx = m_tabwidget->indexOf( m_views.value(id) ); if( idx >= 0 ) { m_tabwidget->setCurrentIndex( idx ); } } else if( data->type & KDevelop::IOutputView::HistoryView ) { int idx = m_stackwidget->indexOf( m_views.value(id) ); if( idx >= 0 ) { m_stackwidget->setCurrentIndex( idx ); } } } enableActions(); } void OutputWidget::nextOutput() { if( m_stackwidget && m_stackwidget->currentIndex() < m_stackwidget->count()-1 ) { m_stackwidget->setCurrentIndex( m_stackwidget->currentIndex()+1 ); } enableActions(); } void OutputWidget::previousOutput() { if( m_stackwidget && m_stackwidget->currentIndex() > 0 ) { m_stackwidget->setCurrentIndex( m_stackwidget->currentIndex()-1 ); } enableActions(); } void OutputWidget::enableActions() { if( data->type == KDevelop::IOutputView::HistoryView ) { Q_ASSERT(m_stackwidget); Q_ASSERT(m_nextAction); Q_ASSERT(m_previousAction); m_previousAction->setEnabled( ( m_stackwidget->currentIndex() > 0 ) ); m_nextAction->setEnabled( ( m_stackwidget->currentIndex() < m_stackwidget->count() - 1 ) ); } } void OutputWidget::scrollToIndex( const QModelIndex& idx ) { QWidget* w = currentWidget(); if( !w ) return; QAbstractItemView *view = static_cast(w); view->scrollTo( idx ); } void OutputWidget::copySelection() { QWidget* widget = currentWidget(); if( !widget ) return; QAbstractItemView *view = dynamic_cast(widget); if( !view ) return; QClipboard *cb = QApplication::clipboard(); QModelIndexList indexes = view->selectionModel()->selectedRows(); - QString content; + QStringList content; + content.reserve(indexes.size()); Q_FOREACH( const QModelIndex& index, indexes) { - content += index.data().toString() + '\n'; + content += index.data().toString(); } - content.chop(1); - cb->setText(content); + cb->setText(content.join(QLatin1Char('\n'))); } void OutputWidget::selectAll() { if (QAbstractItemView *view = qobject_cast(currentWidget())) view->selectAll(); } int OutputWidget::currentOutputIndex() { int index = 0; if( data->type & KDevelop::IOutputView::MultipleView ) { index = m_tabwidget->currentIndex(); } else if( data->type & KDevelop::IOutputView::HistoryView ) { index = m_stackwidget->currentIndex(); } return index; } void OutputWidget::outputFilter(const QString& filter) { QAbstractItemView *view = qobject_cast(currentWidget()); if( !view ) return; int index = currentOutputIndex(); auto proxyModel = qobject_cast(view->model()); if( !proxyModel ) { proxyModel = new QSortFilterProxyModel(view->model()); proxyModel->setDynamicSortFilter(true); proxyModel->setSourceModel(view->model()); m_proxyModels.insert(index, proxyModel); view->setModel(proxyModel); } QRegExp regExp(filter, Qt::CaseInsensitive); proxyModel->setFilterRegExp(regExp); m_filters[index] = filter; } void OutputWidget::updateFilter(int index) { if(m_filters.contains(index)) { m_filterInput->setText(m_filters[index]); } else { m_filterInput->clear(); } } void OutputWidget::setTitle(int outputId, const QString& title) { QTreeView* view = m_views.value(outputId, nullptr); if(view && (data->type & KDevelop::IOutputView::MultipleView)) { int idx = m_tabwidget->indexOf(view); if (idx >= 0) { m_tabwidget->setTabText(idx, title); } } } diff --git a/plugins/subversion/svninternaljobbase.cpp b/plugins/subversion/svninternaljobbase.cpp index 1d90f52492..140e52a79c 100644 --- a/plugins/subversion/svninternaljobbase.cpp +++ b/plugins/subversion/svninternaljobbase.cpp @@ -1,382 +1,382 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright 2007 Dukju Ahn * * * * 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 "svninternaljobbase.h" #include #include #include #include extern "C" { #include } #include #include #include "kdevsvncpp/context.hpp" #include "kdevsvncpp/apr.hpp" #include "kdevsvncpp/revision.hpp" SvnInternalJobBase::SvnInternalJobBase(SvnJobBase* parentJob) : m_ctxt( new svn::Context() ) , m_guiSemaphore( 0 ) , m_mutex() , m_killMutex() , m_parentJob(parentJob) { m_ctxt->setListener(this); } SvnInternalJobBase::~SvnInternalJobBase() { m_ctxt->setListener(nullptr); delete m_ctxt; m_ctxt = nullptr; } void SvnInternalJobBase::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) { emit started(); ThreadWeaver::Job::defaultBegin(self, thread); } void SvnInternalJobBase::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) { ThreadWeaver::Job::defaultEnd(self, thread); if (!self->success()) { emit failed(); } emit done(); // at this ppint this object cannot yet be deleted (e.g. as part of the parent job destruction, // ThreadWeaver logic still holds and uses a reference to finish the execution logic } bool SvnInternalJobBase::contextGetLogin( const std::string& realm, std::string& username, std::string& password, bool& maySave ) { emit needLogin( QString::fromUtf8( realm.c_str() ) ); m_guiSemaphore.acquire( 1 ); QMutexLocker l(&m_mutex); if( m_login_username.isEmpty() || m_login_password.isEmpty() ) return false; username = std::string( m_login_username.toUtf8() ); password = std::string( m_login_password.toUtf8() ); maySave = this->m_maySave; return true; } void SvnInternalJobBase::contextNotify( const char* path, svn_wc_notify_action_t action, svn_node_kind_t kind, const char* mimetype, svn_wc_notify_state_t contentState, svn_wc_notify_state_t propState, svn_revnum_t rev ) { QString notifyString; switch( action ){ case svn_wc_notify_add: notifyString = i18nc( "A file was marked to be added to svn", "Added %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_delete: notifyString = i18nc( "A file was marked for deletion from svn", "Deleted %1", QString::fromUtf8( path ) ); break; // various update notifications case svn_wc_notify_update_delete: notifyString = i18nc( "A file was deleted during an svn update operation", "Deleted %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_update_add: notifyString = i18nc( "A file was added during an svn update operation", "Added %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_update_update: /* If this is an inoperative dir change, do no notification. An inoperative dir change is when a directory gets closed without any props having been changed. */ if (! ((kind == svn_node_dir) && ((propState == svn_wc_notify_state_inapplicable) || (propState == svn_wc_notify_state_unknown) || (propState == svn_wc_notify_state_unchanged)))) { if (kind == svn_node_file) { if (contentState == svn_wc_notify_state_conflicted) notifyString = QStringLiteral("Conflict On File"); else if (contentState == svn_wc_notify_state_merged) notifyString = QStringLiteral("File Merged"); else if (contentState == svn_wc_notify_state_changed) notifyString = QStringLiteral("File Updated"); } if (propState == svn_wc_notify_state_conflicted) notifyString += QLatin1String(" Conflict On Property"); else if (propState == svn_wc_notify_state_merged) notifyString += QLatin1String(" Properties Merged"); else if (propState == svn_wc_notify_state_changed) notifyString += QLatin1String(" Properties Updated"); else notifyString += ' '; if (! ((contentState == svn_wc_notify_state_unchanged || contentState == svn_wc_notify_state_unknown) && (propState == svn_wc_notify_state_unchanged || propState == svn_wc_notify_state_unknown))) - notifyString += QStringLiteral( " " ) + QString::fromUtf8( path ); + notifyString += QLatin1Char(' ') + QString::fromUtf8(path); } break; case svn_wc_notify_update_completed: // The last notification in an update (including updates of externals). notifyString = i18n("Revision %1", rev ); break; case svn_wc_notify_update_external: notifyString = i18n("Updating externals: %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_status_completed: break; case svn_wc_notify_status_external: break; // various commit notifications case svn_wc_notify_commit_modified: notifyString = i18n( "Sending %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_commit_added: if( mimetype ){ notifyString = i18n( "Adding %1 using mimetype %2.", QString::fromUtf8( path ), mimetype ); } else { notifyString = i18n( "Adding %1.", QString::fromUtf8( path ) ); } break; case svn_wc_notify_commit_deleted: notifyString = i18n( "Deleting %1.", QString::fromUtf8( path ) ); break; case svn_wc_notify_commit_replaced: notifyString = i18n( "Replacing %1.", QString::fromUtf8( path ) ); break; case svn_wc_notify_commit_postfix_txdelta: if ( sendFirstDelta ) { sendFirstDelta = false; notifyString=i18n("Transmitting file data "); } else { notifyString='.'; } break; case svn_wc_notify_blame_revision: notifyString = i18n( "Blame finished for revision %1, path %2", rev, QString::fromUtf8( path ) ); break; case svn_wc_notify_revert: notifyString = i18n( "Reverted working copy %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_failed_revert: notifyString = i18n( "Reverting failed on working copy %1", QString::fromUtf8( path ) ); break; case svn_wc_notify_copy: notifyString = i18n( "Copied %1", QString::fromUtf8( path ) ); break; default: break; } emit showNotification( QString::fromUtf8( path ), notifyString ); } bool SvnInternalJobBase::contextCancel() { QMutexLocker lock( &m_killMutex ); return killed; } bool SvnInternalJobBase::contextGetLogMessage( std::string& msg ) { emit needCommitMessage(); m_guiSemaphore.acquire( 1 ); QMutexLocker l( &m_mutex ); QByteArray ba = m_commitMessage.toUtf8(); msg = std::string( ba.data() ); return true; } void SvnInternalJobBase::initBeforeRun() { connect( this, &SvnInternalJobBase::needCommitMessage, m_parentJob, &SvnJobBase::askForCommitMessage, Qt::QueuedConnection ); connect( this, &SvnInternalJobBase::needLogin, m_parentJob, &SvnJobBase::askForLogin, Qt::QueuedConnection ); connect( this, &SvnInternalJobBase::needSslServerTrust, m_parentJob, &SvnJobBase::askForSslServerTrust, Qt::QueuedConnection ); connect( this, &SvnInternalJobBase::showNotification, m_parentJob, &SvnJobBase::showNotification, Qt::QueuedConnection ); connect( this, &SvnInternalJobBase::needSslClientCert, m_parentJob, &SvnJobBase::askForSslClientCert, Qt::QueuedConnection ); connect( this, &SvnInternalJobBase::needSslClientCertPassword, m_parentJob, &SvnJobBase::askForSslClientCertPassword, Qt::QueuedConnection ); } svn::ContextListener::SslServerTrustAnswer SvnInternalJobBase::contextSslServerTrustPrompt( const svn::ContextListener::SslServerTrustData& data, apr_uint32_t& acceptedFailures ) { std::string host = data.hostname; std::string print = data.fingerprint; std::string from = data.validFrom; std::string until = data.validUntil; std::string issue = data.issuerDName; std::string realm = data.realm; acceptedFailures = data.failures; QStringList failures; if( data.failures & SVN_AUTH_SSL_NOTYETVALID ) { failures << i18n("Certificate is not yet valid."); } if( data.failures & SVN_AUTH_SSL_EXPIRED ) { failures << i18n("Certificate has expired."); } if( data.failures & SVN_AUTH_SSL_CNMISMATCH ) { failures << i18n("Certificate's CN (hostname) doesn't match the remote hostname."); } if( data.failures & SVN_AUTH_SSL_UNKNOWNCA ) { failures << i18n("Certificate authority is unknown."); } if( data.failures & SVN_AUTH_SSL_NOTYETVALID ) { failures << i18n("Other unknown error."); } emit needSslServerTrust( failures, QString::fromUtf8( host.c_str() ), QString::fromUtf8( print.c_str() ), QString::fromUtf8( from.c_str() ), QString::fromUtf8( until.c_str() ), QString::fromUtf8( issue.c_str() ), QString::fromUtf8( realm.c_str() ) ); m_guiSemaphore.acquire(1); QMutexLocker l(&m_mutex); return m_trustAnswer; } bool SvnInternalJobBase::contextSslClientCertPrompt( std::string& cert ) { emit needSslClientCert( QString::fromUtf8( cert.c_str() ) ); m_guiSemaphore.acquire( 1 ); return true; } bool SvnInternalJobBase::contextSslClientCertPwPrompt( std::string& pw, const std::string& realm, bool& maySave ) { Q_UNUSED(pw); Q_UNUSED(maySave); emit needSslClientCertPassword( QString::fromUtf8( realm.c_str() ) ); m_guiSemaphore.acquire( 1 ); return false; } bool SvnInternalJobBase::success() const { return m_success; } svn::Revision SvnInternalJobBase::createSvnCppRevisionFromVcsRevision( const KDevelop::VcsRevision& revision ) { svn::Revision rev; QVariant value = revision.revisionValue(); switch( revision.revisionType() ) { case KDevelop::VcsRevision::Special: { if( value.canConvert() ) { KDevelop::VcsRevision::RevisionSpecialType specialtype = value.value(); switch( specialtype ) { case KDevelop::VcsRevision::Head: rev = svn::Revision( svn::Revision::HEAD ); break; case KDevelop::VcsRevision::Working: rev = svn::Revision( svn::Revision::WORKING ); break; case KDevelop::VcsRevision::Base: rev = svn::Revision( svn::Revision::BASE ); break; case KDevelop::VcsRevision::Previous: rev = svn::Revision( svn_opt_revision_previous ); break; case KDevelop::VcsRevision::Start: rev = svn::Revision( svn::Revision::START ); break; default: break; } } break; } case KDevelop::VcsRevision::GlobalNumber: case KDevelop::VcsRevision::FileNumber: { bool ok; qlonglong number = value.toLongLong(&ok); if( ok ) { rev = svn::Revision( number ); } break; } case KDevelop::VcsRevision::Date: { QDateTime dt = value.toDateTime(); if( dt.isValid() ) { rev = svn::Revision( dt.toTime_t() ); } break; } default: break; } return rev; } void SvnInternalJobBase::setErrorMessage( const QString& msg ) { QMutexLocker lock( &m_mutex ); m_errorMessage = msg; } QString SvnInternalJobBase::errorMessage() const { QMutexLocker lock( &m_mutex ); return m_errorMessage; } void SvnInternalJobBase::kill() { QMutexLocker lock( &m_killMutex ); killed = true; } diff --git a/plugins/subversion/svnssldialog.cpp b/plugins/subversion/svnssldialog.cpp index d7cac3324f..c5d266e476 100644 --- a/plugins/subversion/svnssldialog.cpp +++ b/plugins/subversion/svnssldialog.cpp @@ -1,85 +1,85 @@ /*************************************************************************** * Copyright 2007 Dukju Ahn * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "svnssldialog.h" #include #include #include #include "ui_ssltrustdialog.h" class SvnSSLTrustDialogPrivate { public: Ui::SvnSSLTrustDialog ui; bool temporarily; }; SvnSSLTrustDialog::SvnSSLTrustDialog( QWidget *parent ) : QDialog( parent ), d( new SvnSSLTrustDialogPrivate ) { d->ui.setupUi( this ); d->temporarily = true; setWindowTitle( i18n( "Ssl Server Certificate" ) ); buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel); buttonBox->addButton(i18n("Trust Permanently"), QDialogButtonBox::YesRole); buttonBox->addButton(i18n("Trust Temporarily"), QDialogButtonBox::AcceptRole)->setDefault(true); auto layout = new QVBoxLayout(); setLayout(layout); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::clicked, this, &SvnSSLTrustDialog::buttonClicked); } SvnSSLTrustDialog::~SvnSSLTrustDialog() { delete d; } void SvnSSLTrustDialog::setCertInfos( const QString& hostname, const QString& fingerPrint, const QString& validfrom, const QString& validuntil, const QString& issuerName, const QString& realm, const QStringList& failures ) { - QString txt = QStringLiteral("

      "); - foreach( const QString &fail, failures ) - { - txt += "
    • "+fail+"
    • "; + if (!failures.isEmpty()) { + const QString txt = QLatin1String("
      • ") + + failures.join(QLatin1String("
      • ")) + + QLatin1String("
      "); + d->ui.reasons->setHtml( txt ); } - d->ui.reasons->setHtml( txt ); d->ui.hostname->setText( hostname ); d->ui.fingerprint->setText( fingerPrint ); d->ui.validUntil->setText( validuntil ); d->ui.validFrom->setText( validfrom ); d->ui.issuer->setText( issuerName ); setWindowTitle( i18n( "Ssl Server Certificate: %1", realm ) ); } bool SvnSSLTrustDialog::useTemporarily() { return d->temporarily; } void SvnSSLTrustDialog::buttonClicked(QAbstractButton *button) { if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { d->temporarily = true; } else { d->temporarily = false; } accept(); } diff --git a/plugins/vcschangesview/vcschangesview.cpp b/plugins/vcschangesview/vcschangesview.cpp index 12bfb0a196..e7833dd4b6 100644 --- a/plugins/vcschangesview/vcschangesview.cpp +++ b/plugins/vcschangesview/vcschangesview.cpp @@ -1,180 +1,181 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "vcschangesview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vcschangesviewplugin.h" using namespace KDevelop; VcsChangesView::VcsChangesView(VcsProjectIntegrationPlugin* plugin, QWidget* parent) : QTreeView(parent) { setRootIsDecorated(false); setEditTriggers(QAbstractItemView::NoEditTriggers); setSelectionMode(ContiguousSelection); setContextMenuPolicy(Qt::CustomContextMenu); setTextElideMode(Qt::ElideLeft); setWordWrap(true); setWindowIcon(QIcon::fromTheme(QStringLiteral("exchange-positions"), windowIcon())); connect(this, &VcsChangesView::customContextMenuRequested, this, &VcsChangesView::popupContextMenu); foreach(QAction* action, plugin->actionCollection()->actions()) addAction(action); QAction* action = plugin->actionCollection()->action(QStringLiteral("locate_document")); connect(action, &QAction::triggered, this, &VcsChangesView::selectCurrentDocument); connect(this, &VcsChangesView::doubleClicked, this, &VcsChangesView::openSelected); } static void appendActions(QMenu* menu, const QList& actions) { menu->addSeparator(); menu->addActions(actions); } void VcsChangesView::popupContextMenu( const QPoint &pos ) { QList urls; QList projects; QModelIndexList selectionIdxs = selectedIndexes(); if(selectionIdxs.isEmpty()) return; foreach(const QModelIndex& idx, selectionIdxs) { if(idx.column()==0) { if(idx.parent().isValid()) urls += idx.data(KDevelop::VcsFileChangesModel::VcsStatusInfoRole).value().url(); else { IProject* project = ICore::self()->projectController()->findProjectByName(idx.data(ProjectChangesModel::ProjectNameRole).toString()); if (project) { projects += project; } else { qWarning() << "Couldn't find a project for project: " << idx.data(ProjectChangesModel::ProjectNameRole).toString(); } } } } QPointer menu = new QMenu(this); QAction* refreshAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Refresh")); QList extensions; if(!urls.isEmpty()) { KDevelop::FileContext context(urls); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); } else { QList items; + items.reserve(projects.size()); foreach(IProject* p, projects) items += p->projectItem(); KDevelop::ProjectItemContextImpl context(items); extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu); } QList buildActions; QList vcsActions; QList extActions; QList projectActions; QList fileActions; QList runActions; foreach( const ContextMenuExtension& ext, extensions ) { buildActions += ext.actions(ContextMenuExtension::BuildGroup); fileActions += ext.actions(ContextMenuExtension::FileGroup); projectActions += ext.actions(ContextMenuExtension::ProjectGroup); vcsActions += ext.actions(ContextMenuExtension::VcsGroup); extActions += ext.actions(ContextMenuExtension::ExtensionGroup); runActions += ext.actions(ContextMenuExtension::RunGroup); } appendActions(menu, buildActions); appendActions(menu, runActions ); appendActions(menu, fileActions); appendActions(menu, vcsActions); appendActions(menu, extActions); appendActions(menu, projectActions); if ( !menu->isEmpty() ) { QAction* res = menu->exec(viewport()->mapToGlobal(pos)); if(res == refreshAction) { if(!urls.isEmpty()) emit reload(urls); else emit reload(projects); } } delete menu; } void VcsChangesView::selectCurrentDocument() { IDocument* doc = ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url = doc->url(); IProject* p = ICore::self()->projectController()->findProjectForUrl(url); QModelIndex idx = (p ? model()->match(model()->index(0, 0), ProjectChangesModel::UrlRole, url, 1, Qt::MatchExactly).value(0) : QModelIndex()); if(idx.isValid()) { expand(idx.parent()); setCurrentIndex(idx); } else collapseAll(); } void VcsChangesView::setModel(QAbstractItemModel* model) { connect(model, &QAbstractItemModel::rowsInserted, this, &VcsChangesView::expand); QTreeView::setModel(model); } void VcsChangesView::openSelected(const QModelIndex& index) { if(!index.parent().isValid()) //then it's a project return; QModelIndex idx = index.sibling(index.row(), 1); VcsStatusInfo info = idx.data(ProjectChangesModel::VcsStatusInfoRole).value(); QUrl url = info.url(); ICore::self()->documentController()->openDocument(url); } diff --git a/plugins/welcomepage/sessionsmodel.cpp b/plugins/welcomepage/sessionsmodel.cpp index 5b23a1b09e..c920a79707 100644 --- a/plugins/welcomepage/sessionsmodel.cpp +++ b/plugins/welcomepage/sessionsmodel.cpp @@ -1,92 +1,94 @@ /* This file is part of KDevelop Copyright 2010 Aleix Pol Gonzalez 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 "sessionsmodel.h" #include #include using namespace KDevelop; SessionsModel::SessionsModel(QObject* parent) : QAbstractListModel(parent) , m_sessions(KDevelop::SessionController::availableSessionInfos()) { connect(Core::self()->sessionController(), &SessionController::sessionDeleted, this, &SessionsModel::sessionDeleted); } QHash< int, QByteArray > SessionsModel::roleNames() const { QHash< int, QByteArray > roles = QAbstractListModel::roleNames(); roles.insert(Uuid, "uuid"); roles.insert(Projects, "projects"); roles.insert(ProjectNames, "projectNames"); roles.insert(VisibleIdentifier, "identifier"); return roles; } QVariant SessionsModel::data(const QModelIndex& index, int role) const { if(!index.isValid() || index.row()>m_sessions.count()) { return QVariant(); } switch(role) { case Qt::DisplayRole: return m_sessions[index.row()].name; case Qt::ToolTip: return m_sessions[index.row()].description; case Uuid: return m_sessions[index.row()].uuid.toString(); case Projects: return QVariant::fromValue(m_sessions[index.row()].projects); case VisibleIdentifier: { const KDevelop::SessionInfo& s = m_sessions[index.row()]; return s.name.isEmpty() && !s.projects.isEmpty() ? s.projects.first().fileName() : s.name; } case ProjectNames: { QVariantList ret; - foreach(const QUrl& project, m_sessions[index.row()].projects) { + const auto& projects = m_sessions[index.row()].projects; + ret.reserve(projects.size()); + for (const auto& project : projects) { ret += project.fileName(); } return ret; } } return QVariant(); } int SessionsModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_sessions.size(); } void SessionsModel::loadSession(const QString& nameOrId) const { KDevelop::Core::self()->sessionController()->loadSession(nameOrId); } void SessionsModel::sessionDeleted(const QString& id) { for(int i=0; i