diff --git a/autotests/src/variable_test.cpp b/autotests/src/variable_test.cpp index f00a3c2f..60369634 100644 --- a/autotests/src/variable_test.cpp +++ b/autotests/src/variable_test.cpp @@ -1,292 +1,302 @@ /* This file is part of the KDE project * * Copyright 2019 Dominik Haumann * * 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 "variable_test.h" #include "moc_variable_test.cpp" #include #include #include #include #include #include using namespace KTextEditor; QTEST_MAIN(VariableTest) VariableTest::VariableTest() : QObject() { KTextEditor::EditorPrivate::enableUnitTestMode(); } VariableTest::~VariableTest() { } void VariableTest::testReturnValues() { auto editor = KTextEditor::Editor::instance(); const QString name = QStringLiteral("Document:"); auto func = [](const QStringView &, KTextEditor::View *) { return QString(); }; // exact matches QVERIFY(!editor->unregisterVariableMatch(name)); QVERIFY(editor->registerVariableMatch(name, "Document Text", func)); QVERIFY(!editor->registerVariableMatch(name, "Document Text", func)); QVERIFY(editor->unregisterVariableMatch(name)); QVERIFY(!editor->unregisterVariableMatch(name)); // prefix matches QVERIFY(!editor->unregisterVariablePrefix(name)); QVERIFY(editor->registerVariablePrefix(name, "Document Text", func)); QVERIFY(!editor->registerVariablePrefix(name, "Document Text", func)); QVERIFY(editor->unregisterVariablePrefix(name)); QVERIFY(!editor->unregisterVariablePrefix(name)); } void VariableTest::testExactMatch_data() { QTest::addColumn("text"); QTest::addColumn("expected"); QTest::addColumn("expectedText"); QTest::newRow("World") << "World" << "World" << "World"; QTest::newRow("Smart World") << "Smart World" << "Smart World" << "Smart World"; } void VariableTest::testExactMatch() { QFETCH(QString, text); QFETCH(QString, expected); QFETCH(QString, expectedText); auto editor = KTextEditor::Editor::instance(); auto doc = editor->createDocument(nullptr); auto view = doc->createView(nullptr); doc->setText(text); const QString name = QStringLiteral("Doc:Text"); auto func = [](const QStringView &, KTextEditor::View *view) { return view->document()->text(); }; QVERIFY(editor->registerVariableMatch(name, "Document Text", func)); // expandVariable QString output; QVERIFY(editor->expandVariable(QStringLiteral("Doc:Text"), view, output)); QCOMPARE(output, expected); // expandText editor->expandText(QStringLiteral("Hello %{Doc:Text}!"), view, output); QCOMPARE(output, QStringLiteral("Hello ") + expectedText + QLatin1Char('!')); editor->expandText(QStringLiteral("Hello %{Doc:Text} %{Doc:Text}!"), view, output); QCOMPARE(output, QStringLiteral("Hello ") + expectedText + QLatin1Char(' ') + expectedText + QLatin1Char('!')); QVERIFY(editor->unregisterVariableMatch("Doc:Text")); delete doc; } void VariableTest::testPrefixMatch() { auto editor = KTextEditor::Editor::instance(); const QString prefix = QStringLiteral("Mirror:"); auto func = [](const QStringView &text, KTextEditor::View *) { QStringView rest = text.right(text.size() - 7); QString out; for (auto it = rest.rbegin(); it != rest.rend(); ++it) { out += *it; } return out; }; QVERIFY(editor->registerVariablePrefix(prefix, "Reverse text", func)); QString output; QVERIFY(editor->expandVariable(QStringLiteral("Mirror:12345"), nullptr, output)); QCOMPARE(output, QStringLiteral("54321")); editor->expandText(QStringLiteral("Countdown: %{Mirror:12345}"), nullptr, output); QCOMPARE(output, QStringLiteral("Countdown: 54321")); // Test recursive expansion editor->expandText(QStringLiteral("Countup: %{Mirror:%{Mirror:12345}}"), nullptr, output); QCOMPARE(output, QStringLiteral("Countup: 12345")); QVERIFY(editor->unregisterVariablePrefix(prefix)); } void VariableTest::testRecursiveMatch() { auto editor = KTextEditor::Editor::instance(); auto doc = editor->createDocument(nullptr); auto view = doc->createView(nullptr); doc->setText(QStringLiteral("Text")); const QString name = QStringLiteral("Doc:Text"); auto func = [](const QStringView &, KTextEditor::View *view) { return view->document()->text(); }; QVERIFY(editor->registerVariableMatch(name, "Document Text", func)); // Test recursive expansion doc->setText(QStringLiteral("Text")); QString output; editor->expandText(QStringLiteral("Hello %{Doc:%{Doc:Text}}!"), view, output); QCOMPARE(output, QStringLiteral("Hello Text!")); QVERIFY(editor->unregisterVariableMatch(name)); delete doc; } void VariableTest::testBuiltins() { auto editor = KTextEditor::Editor::instance(); auto doc = editor->createDocument(nullptr); doc->openUrl(QUrl::fromLocalFile(QDir::homePath() + QStringLiteral("/kate-v5.tar.gz"))); doc->setText(QStringLiteral("get an edge in editing\n:-)")); auto view = doc->createView(nullptr); view->setCursorPosition(KTextEditor::Cursor(1, 2)); view->show(); QString out; + // Test invalid ones: + editor->expandText(QStringLiteral("%{}"), view, out); + QCOMPARE(out, QStringLiteral("%{}")); + editor->expandText(QStringLiteral("%{"), view, out); + QCOMPARE(out, QStringLiteral("%{")); + editor->expandText(QStringLiteral("%{{}"), view, out); + QCOMPARE(out, QStringLiteral("%{{}")); + editor->expandText(QStringLiteral("%{{}}"), view, out); + QCOMPARE(out, QStringLiteral("%{{}}")); + // Document:FileBaseName editor->expandText(QStringLiteral("%{Document:FileBaseName}"), view, out); QCOMPARE(out, QStringLiteral("kate-v5")); // Document:FileExtension editor->expandText(QStringLiteral("%{Document:FileExtension}"), view, out); QCOMPARE(out, QStringLiteral("tar.gz")); // Document:FileName editor->expandText(QStringLiteral("%{Document:FileName}"), view, out); QCOMPARE(out, QStringLiteral("kate-v5.tar.gz")); // Document:FilePath editor->expandText(QStringLiteral("%{Document:FilePath}"), view, out); QCOMPARE(out, QFileInfo(view->document()->url().toLocalFile()).absoluteFilePath()); // Document:Text editor->expandText(QStringLiteral("%{Document:Text}"), view, out); QCOMPARE(out, QStringLiteral("get an edge in editing\n:-)")); // Document:Path editor->expandText(QStringLiteral("%{Document:Path}"), view, out); QCOMPARE(out, QFileInfo(doc->url().toLocalFile()).absolutePath()); // Document:NativeFilePath editor->expandText(QStringLiteral("%{Document:NativeFilePath}"), view, out); QCOMPARE(out, QDir::toNativeSeparators(QFileInfo(doc->url().toLocalFile()).absoluteFilePath())); // Document:NativePath editor->expandText(QStringLiteral("%{Document:NativePath}"), view, out); QCOMPARE(out, QDir::toNativeSeparators(QFileInfo(doc->url().toLocalFile()).absolutePath())); // Document:NativePath editor->expandText(QStringLiteral("%{Document:NativePath}"), view, out); QCOMPARE(out, QDir::toNativeSeparators(QFileInfo(doc->url().toLocalFile()).absolutePath())); // Document:Cursor:Line editor->expandText(QStringLiteral("%{Document:Cursor:Line}"), view, out); QCOMPARE(out, QStringLiteral("1")); // Document:Cursor:Column editor->expandText(QStringLiteral("%{Document:Cursor:Column}"), view, out); QCOMPARE(out, QStringLiteral("2")); // Document:Cursor:XPos editor->expandText(QStringLiteral("%{Document:Cursor:XPos}"), view, out); QVERIFY(out.toInt() > 0); // Document:Cursor:YPos editor->expandText(QStringLiteral("%{Document:Cursor:YPos}"), view, out); QVERIFY(out.toInt() > 0); view->setSelection(KTextEditor::Range(1, 0, 1, 3)); // Document:Selection:Text editor->expandText(QStringLiteral("%{Document:Selection:Text}"), view, out); QCOMPARE(out, QStringLiteral(":-)")); // Document:Selection:StartLine editor->expandText(QStringLiteral("%{Document:Selection:StartLine}"), view, out); QCOMPARE(out, QStringLiteral("1")); // Document:Selection:StartColumn editor->expandText(QStringLiteral("%{Document:Selection:StartColumn}"), view, out); QCOMPARE(out, QStringLiteral("0")); // Document:Selection:EndLine editor->expandText(QStringLiteral("%{Document:Selection:EndLine}"), view, out); QCOMPARE(out, QStringLiteral("1")); // Document:Selection:EndColumn editor->expandText(QStringLiteral("%{Document:Selection:EndColumn}"), view, out); QCOMPARE(out, QStringLiteral("3")); // Document:RowCount editor->expandText(QStringLiteral("%{Document:RowCount}"), view, out); QCOMPARE(out, QStringLiteral("2")); // Date:Locale editor->expandText(QStringLiteral("%{Date:Locale}"), view, out); QVERIFY(!out.isEmpty()); // Date:ISO editor->expandText(QStringLiteral("%{Date:ISO}"), view, out); QVERIFY(!out.isEmpty()); // Date:yyyy-MM-dd editor->expandText(QStringLiteral("%{Date:yyyy-MM-dd}"), view, out); QVERIFY(QDate::fromString(out, QStringLiteral("yyyy-MM-dd")).isValid()); // Time:Locale editor->expandText(QStringLiteral("%{Time:Locale}"), view, out); QVERIFY(!out.isEmpty()); // Time:ISO editor->expandText(QStringLiteral("%{Time:ISO}"), view, out); QVERIFY(!out.isEmpty()); // Time:hh-mm-ss editor->expandText(QStringLiteral("%{Time:hh-mm-ss}"), view, out); QVERIFY(QTime::fromString(out, QStringLiteral("hh-mm-ss")).isValid()); // ENV:KTE_ENV_VAR_TEST qputenv("KTE_ENV_VAR_TEST", "KTE_ENV_VAR_TEST_VALUE"); editor->expandText(QStringLiteral("%{ENV:KTE_ENV_VAR_TEST}"), view, out); QCOMPARE(out, QStringLiteral("KTE_ENV_VAR_TEST_VALUE")); // JS: editor->expandText(QStringLiteral("%{JS:3 + %{JS:2 + 1}}"), view, out); QCOMPARE(out, QStringLiteral("6")); // UUID editor->expandText(QStringLiteral("%{UUID}"), view, out); QCOMPARE(out.count(QLatin1Char('-')), 4); } // kate: indent-mode cstyle; indent-width 4; replace-tabs on; diff --git a/src/utils/katevariableexpansionhelpers.cpp b/src/utils/katevariableexpansionhelpers.cpp index 89aab3d4..5a992109 100644 --- a/src/utils/katevariableexpansionhelpers.cpp +++ b/src/utils/katevariableexpansionhelpers.cpp @@ -1,418 +1,418 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright 2019 Dominik Haumann 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 "katevariableexpansionhelpers.h" #include "kateglobal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Find closing bracket for @p str starting a position @p pos. */ static int findClosing(QStringView str, int pos = 0) { const int len = str.size(); int nesting = 0; while (pos < len) { - ++pos; const QChar c = str[pos]; if (c == QLatin1Char('}')) { if (nesting == 0) { return pos; } nesting--; } else if (c == QLatin1Char('{')) { nesting++; } + ++pos; } return -1; } namespace KateMacroExpander { QString expandMacro(const QString &input, KTextEditor::View *view) { QString output = input; QString oldStr; do { oldStr = output; const int startIndex = output.indexOf(QLatin1String("%{")); if (startIndex < 0) { break; } const int endIndex = findClosing(output, startIndex + 2); if (endIndex <= startIndex) { break; } const int varLen = endIndex - (startIndex + 2); QString variable = output.mid(startIndex + 2, varLen); variable = expandMacro(variable, view); if (KTextEditor::Editor::instance()->expandVariable(variable, view, variable)) { output.replace(startIndex, endIndex - startIndex + 1, variable); } } while (output != oldStr); // str comparison guards against infinite loop return output; } } class VariableItemModel : public QAbstractItemModel { public: VariableItemModel(QObject *parent = nullptr) : QAbstractItemModel(parent) { } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (parent.isValid() || row < 0 || row >= m_variables.size()) { return {}; } return createIndex(row, column); } QModelIndex parent(const QModelIndex &index) const override { Q_UNUSED(index) // flat list -> we never have parents return {}; } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_variables.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) return 3; // name | description | current value } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (!index.isValid()) { return {}; } const auto &var = m_variables[index.row()]; switch (role) { case Qt::DisplayRole: { const QString suffix = var.isPrefixMatch() ? i18n("") : QString(); return QString(var.name() + suffix); } case Qt::ToolTipRole: return var.description(); } return {}; } void setVariables(const QVector &variables) { beginResetModel(); m_variables = variables; endResetModel(); } private: QVector m_variables; }; class TextEditButton : public QToolButton { public: TextEditButton(QAction *showAction, QTextEdit *parent) : QToolButton(parent) { setAutoRaise(true); setDefaultAction(showAction); m_watched = parent->viewport(); m_watched->installEventFilter(this); show(); adjustPosition(m_watched->size()); } protected: void paintEvent(QPaintEvent *) override { // reimplement to have same behavior as actions in QLineEdits QStylePainter p(this); QStyleOptionToolButton opt; initStyleOption(&opt); opt.state = opt.state & ~QStyle::State_Raised; opt.state = opt.state & ~QStyle::State_MouseOver; opt.state = opt.state & ~QStyle::State_Sunken; p.drawComplexControl(QStyle::CC_ToolButton, opt); } public: bool eventFilter(QObject *watched, QEvent *event) override { if (watched == m_watched) { switch (event->type()) { case QEvent::Resize: { auto resizeEvent = static_cast(event); adjustPosition(resizeEvent->size()); } default: break; } } return QToolButton::eventFilter(watched, event); } private: void adjustPosition(const QSize &parentSize) { QStyleOption sopt; sopt.initFrom(parentWidget()); const int topMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutTopMargin, &sopt, parentWidget()); const int rightMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutRightMargin, &sopt, parentWidget()); if (isLeftToRight()) { move(parentSize.width() - width() - rightMargin, topMargin); } else { move(0, 0); } } private: QWidget *m_watched; }; KateVariableExpansionDialog::KateVariableExpansionDialog(QWidget *parent) : QDialog(parent, Qt::Tool) , m_showAction(new QAction(QIcon::fromTheme(QStringLiteral("code-context")), i18n("Insert variable"), this)) , m_variableModel(new VariableItemModel(this)) , m_listView(new QListView(this)) { setWindowTitle(i18n("Variables")); auto vbox = new QVBoxLayout(this); m_filterEdit = new QLineEdit(this); m_filterEdit->setPlaceholderText(i18n("Filter")); m_filterEdit->setFocus(); m_filterEdit->installEventFilter(this); vbox->addWidget(m_filterEdit); vbox->addWidget(m_listView); m_listView->setUniformItemSizes(true); m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setFilterRole(Qt::DisplayRole); m_filterModel->setSortRole(Qt::DisplayRole); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setFilterKeyColumn(0); m_filterModel->setSourceModel(m_variableModel); m_listView->setModel(m_filterModel); connect(m_filterEdit, &QLineEdit::textChanged, m_filterModel, &QSortFilterProxyModel::setFilterWildcard); auto lblDescription = new QLabel(i18n("Please select a variable."), this); auto lblCurrentValue = new QLabel(this); vbox->addWidget(lblDescription); vbox->addWidget(lblCurrentValue); // react to selection changes connect(m_listView->selectionModel(), &QItemSelectionModel::currentRowChanged, [this, lblDescription, lblCurrentValue](const QModelIndex ¤t, const QModelIndex &) { if (current.isValid()) { const auto &var = m_variables[m_filterModel->mapToSource(current).row()]; lblDescription->setText(var.description()); if (var.isPrefixMatch()) { lblCurrentValue->setText(i18n("Current value: %1", var.name())); } else { auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); const auto value = var.evaluate(var.name(), activeView); lblCurrentValue->setText(i18n("Current value: %1", value)); } } else { lblDescription->setText(i18n("Please select a variable.")); lblCurrentValue->clear(); } }); // insert text on activation connect(m_listView, &QAbstractItemView::activated, [this, lblDescription, lblCurrentValue](const QModelIndex &index) { if (index.isValid()) { const auto &var = m_variables[m_filterModel->mapToSource(index).row()]; // not auto, don't fall for string builder, see bug 413474 const QString name = QStringLiteral("%{") + var.name() + QLatin1Char('}'); if (parentWidget() && parentWidget()->window()) { auto currentWidget = parentWidget()->window()->focusWidget(); if (auto lineEdit = qobject_cast(currentWidget)) { lineEdit->insert(name); } else if (auto textEdit = qobject_cast(currentWidget)) { textEdit->insertPlainText(name); } } } }); // show dialog whenever the action is clicked connect(m_showAction, &QAction::triggered, [this]() { show(); activateWindow(); }); resize(400, 550); } KateVariableExpansionDialog::~KateVariableExpansionDialog() { for (auto it = m_textEditButtons.begin(); it != m_textEditButtons.end(); ++it) { if (it.value()) { delete it.value(); } } m_textEditButtons.clear(); } void KateVariableExpansionDialog::addVariable(const KTextEditor::Variable &variable) { Q_ASSERT(variable.isValid()); m_variables.push_back(variable); m_variableModel->setVariables(m_variables); } int KateVariableExpansionDialog::isEmpty() const { return m_variables.isEmpty(); } void KateVariableExpansionDialog::addWidget(QWidget *widget) { m_widgets.push_back(widget); widget->installEventFilter(this); connect(widget, &QObject::destroyed, this, &KateVariableExpansionDialog::onObjectDeleted); } void KateVariableExpansionDialog::onObjectDeleted(QObject *object) { m_widgets.removeAll(object); if (m_widgets.isEmpty()) { deleteLater(); } } bool KateVariableExpansionDialog::eventFilter(QObject *watched, QEvent *event) { // filter line edit if (watched == m_filterEdit) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp) || (keyEvent->key() == Qt::Key_PageDown) || (keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return); if (forward2list) { QCoreApplication::sendEvent(m_listView, event); return true; } } return QDialog::eventFilter(watched, event); } // tracked widgets (tooltips, adding/removing the showAction) switch (event->type()) { case QEvent::FocusIn: { if (auto lineEdit = qobject_cast(watched)) { lineEdit->addAction(m_showAction, QLineEdit::TrailingPosition); } else if (auto textEdit = qobject_cast(watched)) { if (!m_textEditButtons.contains(textEdit)) { m_textEditButtons[textEdit] = new TextEditButton(m_showAction, textEdit); } m_textEditButtons[textEdit]->raise(); m_textEditButtons[textEdit]->show(); } break; } case QEvent::FocusOut: { if (auto lineEdit = qobject_cast(watched)) { lineEdit->removeAction(m_showAction); } else if (auto textEdit = qobject_cast(watched)) { if (m_textEditButtons.contains(textEdit)) { delete m_textEditButtons[textEdit]; m_textEditButtons.remove(textEdit); } } break; } case QEvent::ToolTip: { QString inputText; if (auto lineEdit = qobject_cast(watched)) { inputText = lineEdit->text(); } QString toolTip; if (!inputText.isEmpty()) { auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); KTextEditor::Editor::instance()->expandText(inputText, activeView, toolTip); } if (!toolTip.isEmpty()) { auto helpEvent = static_cast(event); QToolTip::showText(helpEvent->globalPos(), toolTip, qobject_cast(watched)); event->accept(); return true; } break; } default: break; } // auto-hide on focus change auto parentWindow = parentWidget()->window(); const bool keepVisible = isActiveWindow() || m_widgets.contains(parentWindow->focusWidget()); if (!keepVisible) { hide(); } return QDialog::eventFilter(watched, event); } // kate: space-indent on; indent-width 4; replace-tabs on;