diff --git a/kdevplatform/sublime/idealbuttonbarwidget.cpp b/kdevplatform/sublime/idealbuttonbarwidget.cpp index 20c589ad1b..3dd46efc7c 100644 --- a/kdevplatform/sublime/idealbuttonbarwidget.cpp +++ b/kdevplatform/sublime/idealbuttonbarwidget.cpp @@ -1,365 +1,363 @@ /* 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 "idealbuttonbarwidget.h" #include "mainwindow.h" #include "idealdockwidget.h" #include "ideallayout.h" #include "idealtoolbutton.h" #include "document.h" #include "view.h" #include #include #include #include #include using namespace Sublime; class ToolViewAction : public QAction { Q_OBJECT public: ToolViewAction(IdealDockWidget *dock, QObject* parent) : QAction(parent), m_dock(dock) { setCheckable(true); const QString title = dock->view()->document()->title(); setIcon(dock->windowIcon()); setToolTip(i18n("Toggle '%1' tool view.", title)); setText(title); //restore toolview shortcut config KConfigGroup config = KSharedConfig::openConfig()->group("UI"); QStringList shortcutStrings = config.readEntry(QStringLiteral("Shortcut for %1").arg(title), QStringList()); setShortcuts({ QKeySequence::fromString(shortcutStrings.value(0)), QKeySequence::fromString(shortcutStrings.value(1)) }); dock->setWindowTitle(title); dock->view()->widget()->installEventFilter(this); refreshText(); } IdealDockWidget *dockWidget() const { Q_ASSERT(m_dock); return m_dock; } IdealToolButton* button() { return m_button; } void setButton(IdealToolButton* button) { m_button = button; refreshText(); } QString id() { return m_dock->view()->document()->documentSpecifier(); } private: bool eventFilter(QObject * watched, QEvent * event) override { if (watched == m_dock->view()->widget() && event->type() == QEvent::EnabledChange) { refreshText(); } return QObject::eventFilter(watched, event); } void refreshText() { const auto widget = m_dock->view()->widget(); const QString title = m_dock->view()->document()->title(); setText(widget->isEnabled() ? title : QStringLiteral("(%1)").arg(title)); } QPointer m_dock; QPointer m_button; }; IdealButtonBarWidget::IdealButtonBarWidget(Qt::DockWidgetArea area, IdealController *controller, Sublime::MainWindow *parent) : QWidget(parent) , m_area(area) , m_controller(controller) , m_corner(nullptr) , m_showState(false) { setContextMenuPolicy(Qt::CustomContextMenu); setToolTip(i18nc("@info:tooltip", "Right click to add new tool views.")); if (area == Qt::BottomDockWidgetArea) { QBoxLayout *statusLayout = new QBoxLayout(QBoxLayout::RightToLeft, this); statusLayout->setMargin(0); - statusLayout->setSpacing(IDEAL_LAYOUT_SPACING); - statusLayout->setContentsMargins(0, IDEAL_LAYOUT_MARGIN, 0, IDEAL_LAYOUT_MARGIN); IdealButtonBarLayout *l = new IdealButtonBarLayout(orientation()); statusLayout->addLayout(l); m_corner = new QWidget(this); QBoxLayout *cornerLayout = new QBoxLayout(QBoxLayout::LeftToRight, m_corner); cornerLayout->setMargin(0); cornerLayout->setSpacing(0); statusLayout->addWidget(m_corner); statusLayout->addStretch(1); } else (void) new IdealButtonBarLayout(orientation(), this); } QAction* IdealButtonBarWidget::addWidget(IdealDockWidget *dock, Area *area, View *view) { if (m_area == Qt::BottomDockWidgetArea || m_area == Qt::TopDockWidgetArea) dock->setFeatures( dock->features() | IdealDockWidget::DockWidgetVerticalTitleBar ); dock->setArea(area); dock->setView(view); dock->setDockWidgetArea(m_area); auto action = new ToolViewAction(dock, this); addAction(action); return action; } QWidget* IdealButtonBarWidget::corner() { return m_corner; } void IdealButtonBarWidget::addAction(QAction* qaction) { QWidget::addAction(qaction); auto action = dynamic_cast(qaction); if (!action || action->button()) { return; } bool wasEmpty = isEmpty(); IdealToolButton *button = new IdealToolButton(m_area); //apol: here we set the usual width of a button for the vertical toolbars as the minimumWidth //this is done because otherwise when we remove all the buttons and re-add new ones we get all //the screen flickering. This is solved by not defaulting to a smaller width when it's empty int w = button->sizeHint().width(); if (orientation() == Qt::Vertical && w > minimumWidth()) { setMinimumWidth(w); } action->setButton(button); button->setDefaultAction(action); Q_ASSERT(action->dockWidget()); connect(action, &QAction::toggled, this, static_cast(&IdealButtonBarWidget::showWidget)); connect(button, &IdealToolButton::customContextMenuRequested, action->dockWidget(), &IdealDockWidget::contextMenuRequested); addButtonToOrder(button); applyOrderToLayout(); if (wasEmpty) { emit emptyChanged(); } } void IdealButtonBarWidget::removeAction(QAction* widgetAction) { QWidget::removeAction(widgetAction); auto action = static_cast(widgetAction); action->button()->deleteLater(); delete action; if (layout()->isEmpty()) { emit emptyChanged(); } } bool IdealButtonBarWidget::isEmpty() { return actions().isEmpty(); } bool IdealButtonBarWidget::isShown() { const auto actions = this->actions(); return std::any_of(actions.cbegin(), actions.cend(), [](const QAction* action){ return action->isChecked(); }); } void IdealButtonBarWidget::saveShowState() { m_showState = isShown(); } bool IdealButtonBarWidget::lastShowState() { return m_showState; } QString IdealButtonBarWidget::id(const IdealToolButton* button) const { foreach (QAction* a, actions()) { auto tva = dynamic_cast(a); if (tva && tva->button() == button) { return tva->id(); } } return QString(); } IdealToolButton* IdealButtonBarWidget::button(const QString& id) const { foreach (QAction* a, actions()) { auto tva = dynamic_cast(a); if (tva && tva->id() == id) { return tva->button(); } } return nullptr; } void IdealButtonBarWidget::addButtonToOrder(const IdealToolButton* button) { QString buttonId = id(button); if (!m_buttonsOrder.contains(buttonId)) { if (m_area == Qt::BottomDockWidgetArea) { m_buttonsOrder.push_front(buttonId); } else { m_buttonsOrder.push_back(buttonId); } } } void IdealButtonBarWidget::loadOrderSettings(const KConfigGroup& configGroup) { m_buttonsOrder = configGroup.readEntry(QStringLiteral("(%1) Tool Views Order").arg(m_area), QStringList()); applyOrderToLayout(); } void IdealButtonBarWidget::saveOrderSettings(KConfigGroup& configGroup) { takeOrderFromLayout(); configGroup.writeEntry(QStringLiteral("(%1) Tool Views Order").arg(m_area), m_buttonsOrder); } bool IdealButtonBarWidget::isLocked() const { KConfigGroup config = KSharedConfig::openConfig()->group("UI"); return config.readEntry(QStringLiteral("Toolview Bar (%1) Is Locked").arg(m_area), false); } void IdealButtonBarWidget::applyOrderToLayout() { // If widget already have some buttons in the layout then calling loadOrderSettings() may leads // to situations when loaded order does not contains all existing buttons. Therefore we should // fix this with using addToOrder() method. for (int i = 0; i < layout()->count(); ++i) { if (auto button = dynamic_cast(layout()->itemAt(i)->widget())) { addButtonToOrder(button); layout()->removeWidget(button); } } foreach(const QString& id, m_buttonsOrder) { if (auto b = button(id)) { layout()->addWidget(b); } } } void IdealButtonBarWidget::takeOrderFromLayout() { m_buttonsOrder.clear(); for (int i = 0; i < layout()->count(); ++i) { if (auto button = dynamic_cast(layout()->itemAt(i)->widget())) { m_buttonsOrder += id(button); } } } Qt::Orientation IdealButtonBarWidget::orientation() const { if (m_area == Qt::LeftDockWidgetArea || m_area == Qt::RightDockWidgetArea) return Qt::Vertical; return Qt::Horizontal; } Qt::DockWidgetArea IdealButtonBarWidget::area() const { return m_area; } void IdealButtonBarWidget::showWidget(bool checked) { Q_ASSERT(parentWidget() != nullptr); QAction *action = qobject_cast(sender()); Q_ASSERT(action); showWidget(action, checked); } void IdealButtonBarWidget::showWidget(QAction *action, bool checked) { auto widgetAction = static_cast(action); IdealToolButton* button = widgetAction->button(); Q_ASSERT(button); if (checked) { if ( !QApplication::keyboardModifiers().testFlag(Qt::ControlModifier) ) { // Make sure only one widget is visible at any time. // The alternative to use a QActionCollection and setting that to "exclusive" // has a big drawback: QActions in a collection that is exclusive cannot // be un-checked by the user, e.g. in the View -> Tool Views menu. foreach(QAction *otherAction, actions()) { if ( otherAction != widgetAction && otherAction->isChecked() ) otherAction->setChecked(false); } } m_controller->lastDockWidget[m_area] = widgetAction->dockWidget(); } m_controller->showDockWidget(widgetAction->dockWidget(), checked); widgetAction->setChecked(checked); button->setChecked(checked); } IdealDockWidget * IdealButtonBarWidget::widgetForAction(QAction *_action) const { return static_cast(_action)->dockWidget(); } #include "idealbuttonbarwidget.moc" diff --git a/kdevplatform/sublime/ideallayout.cpp b/kdevplatform/sublime/ideallayout.cpp index bfa94251dd..f85de89f60 100644 --- a/kdevplatform/sublime/ideallayout.cpp +++ b/kdevplatform/sublime/ideallayout.cpp @@ -1,247 +1,259 @@ /* Copyright 2007 Roberto Raggi Copyright 2007 Hamish Rodda Copyright 2008 Vladimir Prus 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 "ideallayout.h" +#include +#include + using namespace Sublime; IdealButtonBarLayout::IdealButtonBarLayout(Qt::Orientation orientation, QWidget *parent) : QLayout(parent) , _orientation(orientation) , _height(0) { - if (orientation == Qt::Vertical) - setContentsMargins(IDEAL_LAYOUT_MARGIN, 0, IDEAL_LAYOUT_MARGIN, 0); - else - setContentsMargins(0, IDEAL_LAYOUT_MARGIN, 0, IDEAL_LAYOUT_MARGIN); - setSpacing(IDEAL_LAYOUT_SPACING); + setContentsMargins(0, 0, 0, 0); invalidate(); } void IdealButtonBarLayout::invalidate() { m_minSizeDirty = true; m_sizeHintDirty = true; m_layoutDirty = true; QLayout::invalidate(); } IdealButtonBarLayout::~IdealButtonBarLayout() { qDeleteAll(_items); } void IdealButtonBarLayout::setHeight(int height) { Q_ASSERT(orientation() == Qt::Vertical); _height = height; (void) invalidate(); } Qt::Orientation IdealButtonBarLayout::orientation() const { return _orientation; } Qt::Orientations IdealButtonBarLayout::expandingDirections() const { return orientation(); } QSize IdealButtonBarLayout::minimumSize() const { // The code below appears to be completely wrong -- // it will return the maximum size of a single button, not any // estimate as to how much space is necessary to draw all buttons // in a minimally acceptable way. if (m_minSizeDirty) { if (orientation() == Qt::Vertical) { const int width = doVerticalLayout(QRect(0, 0, 0, _height), false); return QSize(width, 0); } m_min = QSize(0, 0); foreach (QLayoutItem *item, _items) m_min = m_min.expandedTo(item->minimumSize()); m_minSizeDirty = false; } return m_min; } QSize IdealButtonBarLayout::sizeHint() const { if (m_sizeHintDirty) { + const int buttonSpacing = this->buttonSpacing(); + int orientationSize = 0; int crossSize = 0; bool first = true; foreach (QLayoutItem *item, _items) { QSize hint = item->sizeHint(); int orientationSizeHere; int crossSizeHere; if (orientation() == Qt::Vertical) { orientationSizeHere = hint.height(); crossSizeHere = hint.width(); } else { orientationSizeHere = hint.width(); crossSizeHere = hint.height(); } if (first) { crossSize = crossSizeHere; } else { - orientationSize += spacing(); + orientationSize += buttonSpacing; } orientationSize += orientationSizeHere; first = false; } if (orientation() == Qt::Vertical) m_hint = QSize(crossSize, orientationSize); else m_hint = QSize(orientationSize, crossSize); if (!_items.empty()) { /* If we have no items, just use (0, 0) as hint, don't append any margins. */ int l, t, r, b; getContentsMargins(&l, &t, &r, &b); m_hint += QSize(l+r, t+b); } m_sizeHintDirty = false; } return m_hint; } void IdealButtonBarLayout::setGeometry(const QRect &rect) { if (m_layoutDirty || rect != geometry()) { if (orientation() == Qt::Vertical) doVerticalLayout(rect); else doHorizontalLayout(rect); } } void IdealButtonBarLayout::addItem(QLayoutItem *item) { _items.append(item); invalidate(); } QLayoutItem* IdealButtonBarLayout::itemAt(int index) const { return _items.value(index, nullptr); } QLayoutItem* IdealButtonBarLayout::takeAt(int index) { if (index >= 0 && index < _items.count()) return _items.takeAt(index); invalidate(); return nullptr; } int IdealButtonBarLayout::count() const { return _items.count(); } +int IdealButtonBarLayout::buttonSpacing() const +{ + auto pw = parentWidget(); + return pw ? pw->style()->pixelMetric(QStyle::PM_ToolBarItemSpacing) : 0; +} + + int IdealButtonBarLayout::doVerticalLayout(const QRect &rect, bool updateGeometry) const { + const int buttonSpacing = this->buttonSpacing(); + int l, t, r, b; getContentsMargins(&l, &t, &r, &b); int x = rect.x() + l; int y = rect.y() + t; int currentLineWidth = 0; foreach (QLayoutItem *item, _items) { const QSize itemSizeHint = item->sizeHint(); if (y + itemSizeHint.height() + b > rect.height()) { - int newX = x + currentLineWidth + spacing(); + int newX = x + currentLineWidth + buttonSpacing; if (newX + itemSizeHint.width() + r <= rect.width()) { - x += currentLineWidth + spacing(); + x += currentLineWidth + buttonSpacing; y = rect.y() + t; } } if (updateGeometry) item->setGeometry(QRect(x, y, itemSizeHint.width(), itemSizeHint.height())); currentLineWidth = qMax(currentLineWidth, itemSizeHint.width()); - y += itemSizeHint.height() + spacing(); + y += itemSizeHint.height() + buttonSpacing; } m_layoutDirty = updateGeometry; return x + currentLineWidth + r; } int IdealButtonBarLayout::doHorizontalLayout(const QRect &rect, bool updateGeometry) const { + const int buttonSpacing = this->buttonSpacing(); + int l, t, r, b; getContentsMargins(&l, &t, &r, &b); int x = rect.x() + l; int y = rect.y() + t; int currentLineHeight = 0; foreach (QLayoutItem *item, _items) { QSize itemSizeHint = item->sizeHint(); if (x + itemSizeHint.width() + r > rect.width()) { // Run out of horizontal space. Try to move button to another // row. - int newY = y + currentLineHeight + spacing(); + int newY = y + currentLineHeight + buttonSpacing; if (newY + itemSizeHint.height() + b <= rect.height()) { y = newY; x = rect.x() + l; currentLineHeight = 0; } } if (updateGeometry) item->setGeometry(QRect(x, y, itemSizeHint.width(), itemSizeHint.height())); currentLineHeight = qMax(currentLineHeight, itemSizeHint.height()); - x += itemSizeHint.width() + spacing(); + x += itemSizeHint.width() + buttonSpacing; } m_layoutDirty = updateGeometry; return y + currentLineHeight + b; } diff --git a/kdevplatform/sublime/ideallayout.h b/kdevplatform/sublime/ideallayout.h index 806608a391..16e156cc04 100644 --- a/kdevplatform/sublime/ideallayout.h +++ b/kdevplatform/sublime/ideallayout.h @@ -1,83 +1,82 @@ /* Copyright 2007 Roberto Raggi Copyright 2007 Hamish Rodda 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. */ #ifndef KDEVPLATFORM_SUBLIME_IDEALLAYOUT_H #define KDEVPLATFORM_SUBLIME_IDEALLAYOUT_H #include #include "sublimedefs.h" -#define IDEAL_LAYOUT_MARGIN 0 -#define IDEAL_LAYOUT_SPACING 2 - namespace Sublime { class IdealButtonBarLayout: public QLayout { Q_OBJECT public: explicit IdealButtonBarLayout(Qt::Orientation orientation, QWidget *parent = nullptr); ~IdealButtonBarLayout() override; void setHeight(int height); inline Qt::Orientation orientation() const; Qt::Orientations expandingDirections() const override; QSize minimumSize() const override; QSize sizeHint() const override; void setGeometry(const QRect &rect) override; void addItem(QLayoutItem *item) override; QLayoutItem* itemAt(int index) const override; QLayoutItem* takeAt(int index) override; int count() const override; void invalidate() override; protected: int doVerticalLayout(const QRect &rect, bool updateGeometry = true) const; int doHorizontalLayout(const QRect &rect, bool updateGeometry = true) const; + int buttonSpacing() const; + private: QList _items; Qt::Orientation _orientation; int _height; mutable bool m_minSizeDirty : 1; mutable bool m_sizeHintDirty : 1; mutable bool m_layoutDirty : 1; mutable QSize m_min; mutable QSize m_hint; }; } #endif diff --git a/plugins/astyle/CMakeLists.txt b/plugins/astyle/CMakeLists.txt index 6d144ec453..84eecca606 100644 --- a/plugins/astyle/CMakeLists.txt +++ b/plugins/astyle/CMakeLists.txt @@ -1,38 +1,36 @@ -include_directories(lib) - add_definitions(-DTRANSLATION_DOMAIN=\"kdevastyle\") ecm_qt_declare_logging_category(kdevastyle_LOG_SRCS HEADER debug.h IDENTIFIER KDEV_ASTYLE CATEGORY_NAME "kdevelop.formatters.astyle" ) set(kdevastyle_PART_SRCS astyle_plugin.cpp astyle_preferences.cpp astyle_formatter.cpp astyle_stringiterator.cpp ${kdevastyle_LOG_SRCS} ) set(astyle_preferences_UI astyle_preferences.ui ) ki18n_wrap_ui(kdevastyle_PART_SRCS ${astyle_preferences_UI} ) kdevplatform_add_plugin(kdevastyle JSON kdevastyle.json SOURCES ${kdevastyle_PART_SRCS}) target_link_libraries(kdevastyle astylelib KF5::Parts KF5::KIOWidgets KF5::TextEditor KDev::Interfaces KDev::Util) add_subdirectory(3rdparty/libastyle) if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/plugins/clang/codecompletion/context.cpp b/plugins/clang/codecompletion/context.cpp index 9bdd847c3d..be74d24b42 100644 --- a/plugins/clang/codecompletion/context.cpp +++ b/plugins/clang/codecompletion/context.cpp @@ -1,1284 +1,1286 @@ /* * 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()) { 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); auto f = m_declaration->type(); if (f && f->indexedArgumentsSize() && didAddParentheses) { view->setCursorPosition(word.start() + KTextEditor::Cursor(0, repl.size() - 1)); } auto returnTypeIntegral = f->returnType().cast(); - if ( !funcptr && returnTypeIntegral && returnTypeIntegral->dataType() == IntegralType::TypeVoid ) { - // function returns void -- nothing can be done with the result + 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 ( f && f->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, KDevelop::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; return highlighting; } if (role == CodeCompletionModel::HighlightingMethod && index.column() == CodeCompletionModel::Arguments && argumentHintDepth()) { return QVariant(CodeCompletionModel::CustomHighlighting); } if (index.column() == CodeCompletionModel::Arguments && !m_declaration) { 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 == KDevelop::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(); 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("->"); 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; QSet handled; 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]; 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()) { #if CINDEX_VERSION_MINOR >= 30 // TODO: When parent context for CXCursor_OverloadCandidate is fixed remove this check if (result.CursorKind != CXCursor_OverloadCandidate) { break; } #else break; #endif } 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 CINDEX_VERSION_MINOR >= 30 if (result.CursorKind == CXCursor_OverloadCandidate) { typed += string; } #endif break; case CXCompletionChunk_CurrentParameter: argumentRange.start = arguments.size(); argumentRange.end = string.size(); break; default: break; } if (signatureState == Inside) { arguments += string; } } }; processChunks(result.CompletionString); #if CINDEX_VERSION_MINOR >= 30 // TODO: No closing paren if default parameters present if (result.CursorKind == CXCursor_OverloadCandidate && !arguments.endsWith(QLatin1Char(')'))) { arguments += QLatin1Char(')'); } #endif // 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; } auto found = findDeclaration(qid, ctx, m_position, 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; } } auto 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(); } #if CINDEX_VERSION_MINOR >= 30 if (result.CursorKind == CXCursor_OverloadCandidate) { declarationItem->setArgumentHintDepth(1); } #endif item = declarationItem; } else { #if CINDEX_VERSION_MINOR >= 30 if (result.CursorKind == CXCursor_OverloadCandidate) { // 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 { #endif // 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 CINDEX_VERSION_MINOR >= 30 } #endif } 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(QLatin1String("_")) ) { 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; } KDevelop::CompletionCustomGroupNode* node = new KDevelop::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; 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; 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/welcomepage/CMakeLists.txt b/plugins/welcomepage/CMakeLists.txt index 49faeee4e0..0b4af856a9 100644 --- a/plugins/welcomepage/CMakeLists.txt +++ b/plugins/welcomepage/CMakeLists.txt @@ -1,25 +1,30 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevwelcomepage\") add_subdirectory(declarative) set(welcomepage_SRCS welcomepageplugin.cpp sessionsmodel.cpp welcomepageview.cpp uihelper.cpp) qt5_add_resources(welcomepage_SRCS welcomepage.qrc) kdevplatform_add_plugin(kdevwelcomepage JSON kdevwelcomepage.json SOURCES ${welcomepage_SRCS}) +option(WELCOMEPAGE_ENABLE_QMLJSDEBUGGING "Enable the QML debugging infrastructure for the welcomepage plugin" OFF) +if (WELCOMEPAGE_ENABLE_QMLJSDEBUGGING) + # cf. http://doc.qt.io/qt-5/qtquick-debugging.html#enabling-the-infrastructure + target_compile_definitions(kdevwelcomepage PRIVATE QT_QML_DEBUG=1) +endif() target_link_libraries(kdevwelcomepage KDev::Interfaces KDev::Sublime KDev::Shell KDev::Project Qt5::QuickWidgets KF5::Declarative ) # see https://bugs.launchpad.net/ubuntu/+source/gcc-5/+bug/1568899 if (UNIX AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") message(WARNING "Applying workaround for https://bugs.launchpad.net/ubuntu/+source/gcc-5/+bug/1568899") target_link_libraries(kdevwelcomepage gcc_s gcc) endif() diff --git a/plugins/welcomepage/qml/NewsFeed.qml b/plugins/welcomepage/qml/NewsFeed.qml index 62cf0cd473..d7f3ea4c8a 100644 --- a/plugins/welcomepage/qml/NewsFeed.qml +++ b/plugins/welcomepage/qml/NewsFeed.qml @@ -1,212 +1,207 @@ /* KDevelop * * Copyright 2017 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) 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. */ import QtQuick 2.0 import QtQuick.Controls 1.3 import QtQuick.Layouts 1.2 import QtQuick.XmlListModel 2.0 import "storage.js" as Storage ListView { id: root - /// Update interval (in minutes) in which the news feed is polled - property int updateInterval: 24 * 60 // 24 hours - /// Max age (in minutes) of a news entry so it is shown in the list view + /// Update interval (in seconds) in which the news feed is polled + property int updateInterval: 24 * 3600 // 24 hours + /// Max age (in seconds) of a news entry so it is shown in the list view /// TODO: Implement me - property int maxNewsAge: 3 * 30 * 24 * 60 // 3 months - /// Max age (in minutes) of a news entry so it is considered 'new' (thus highlighted with a bold font) - property int maxHighlightedNewsAge: 30 * 24 * 60 // a month + property int maxNewsAge: 3 * 30 * 24 * 3600 // 3 months + /// Max age (in seconds) of a news entry so it is considered 'new' (thus highlighted with a bold font) + property int maxHighlightedNewsAge: 30 * 24 * 3600 // a month readonly property string feedUrl: "https://www.kdevelop.org/news/feed" readonly property bool loading: newsFeedSyncModel.status === XmlListModel.Loading /// Returns a date parsed from the pubDate function parsePubDate(pubDate) { // We need to modify the pubDate read from the RSS feed // so the JavaScript Date object can interpret it var d = pubDate.replace(',','').split(' '); if (d.length != 6) return new Date(NaN); return new Date([d[0], d[2], d[1], d[3], d[4], 'GMT' + d[5]].join(' ')); } // there's no builtin function for this(?) function toMap(obj) { var map = {}; for (var k in obj) { map[k] = obj[k]; } return map; } - function minutesSince(date) { - return !isNaN(date) ? Math.floor(Number((new Date() - date)) / 60000) : -1; + function secondsSince(date) { + return !isNaN(date) ? Math.floor(Number((new Date() - date)) / 1000) : -1; } function loadEntriesFromCache() { newsFeedOfflineModel.clear() var data = Storage.get("newsFeedOfflineModelData", null); if (data) { var newsEntries = JSON.parse(data); for (var i = 0; i < newsEntries.length; ++i) { newsFeedOfflineModel.append(newsEntries[i]); } } root.positionViewAtBeginning() } function saveEntriesToCache() { var newsEntries = []; for (var i = 0; i < newsFeedSyncModel.count; ++i) { var entry = newsFeedSyncModel.get(i); newsEntries.push(toMap(entry)); } Storage.set("newsFeedOfflineModelData", JSON.stringify(newsEntries)); } spacing: 10 // Note: this model is *not* attached to the the view -- it's merely used for fetching the RSS feed XmlListModel { id: newsFeedSyncModel property bool active: false source: active ? feedUrl : "" query: "/rss/channel/item" XmlRole { name: "title"; query: "title/string()" } XmlRole { name: "link"; query: "link/string()" } XmlRole { name: "pubDate"; query: "pubDate/string()" } onStatusChanged: { if (status == XmlListModel.Ready) { saveEntriesToCache(); loadEntriesFromCache(); Storage.set("newsFeedLastFetchDate", JSON.stringify(new Date())); } else if (status == XmlListModel.Error) { console.log("Failed to fetch news feed: " + errorString()); } } } ListModel { id: newsFeedOfflineModel } // detach from model while fetching feed to make space for the busy indicator model: root.loading ? undefined : newsFeedOfflineModel delegate: Column { id: feedDelegate readonly property date publicationDate: parsePubDate(model.pubDate) - readonly property int ageInMinutes: minutesSince(publicationDate) - readonly property bool isNew: ageInMinutes != -1 && ageInMinutes < maxHighlightedNewsAge + /// in seconds + readonly property int age: secondsSince(publicationDate) + readonly property bool isNew: age != -1 && age < maxHighlightedNewsAge readonly property string dateString: isNaN(publicationDate.getDate()) ? model.pubDate : publicationDate.toLocaleDateString() x: 10 width: parent.width - 2*x Link { width: parent.width text: model.title onClicked: Qt.openUrlExternally(model.link) } Label { width: parent.width font.bold: isNew font.pointSize: 8 color: disabledPalette.windowText text: isNew ? i18nc("Example: Tue, 03 Jan 2017 10:00:00 (new)", "%1 (new)", dateString) : dateString } } - BusyIndicator { - id: busyIndicator - - height: newsHeading.height - - running: root.loading - } - Label { id: placeHolderLabel x: 10 width: parent.width - 2*x - text: i18n("No recent news") + text: root.loading ? + i18n("Fetching feeds...") : + i18n("No recent news") color: disabledPalette.windowText - visible: root.count === 0 && !root.loading + visible: root.count === 0 Behavior on opacity { NumberAnimation {} } } SystemPalette { id: disabledPalette colorGroup: SystemPalette.Disabled } function fetchFeed() { console.log("Fetching news feed") newsFeedSyncModel.active = true newsFeedSyncModel.reload() } Timer { id: delayedStartupTimer // delay loading a bit so it has no effect on the KDevelop startup interval: 3000 running: true onTriggered: { // only fetch feed if items are out of date var lastFetchDate = new Date(JSON.parse(Storage.get("newsFeedLastFetchDate", null))); - if (minutesSince(lastFetchDate) > root.updateInterval) { - console.log("Last fetch of news feed was on " + lastFetchDate + ", updating now"); + console.log("Last fetch of news feed was on " + lastFetchDate); + if (secondsSince(lastFetchDate) > root.updateInterval) { root.fetchFeed(); } } } Timer { id: reloadFeedTimer interval: root.updateInterval * 1000 running: true repeat: true onTriggered: root.fetchFeed() } Component.onCompleted: loadEntriesFromCache() }