diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -143,6 +143,7 @@ src/revision_test.cpp src/modificationsystem_test.cpp src/inlinenote_test.cpp + src/variable_test.cpp src/templatehandler_test.cpp src/katefoldingtest.cpp src/bug286887.cpp diff --git a/autotests/src/variable_test.h b/autotests/src/variable_test.h new file mode 100644 --- /dev/null +++ b/autotests/src/variable_test.h @@ -0,0 +1,41 @@ +/* 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. + */ +#ifndef KTEXTEDITOR_VARIABLE_TEST_H +#define KTEXTEDITOR_VARIABLE_TEST_H + +#include + +class VariableTest : public QObject +{ + Q_OBJECT + +public: + VariableTest(); + ~VariableTest(); + +private Q_SLOTS: + void testReturnValues(); + void testExactMatch_data(); + void testExactMatch(); + void testPrefixMatch(); + void testRecursiveMatch(); +}; + +#endif // KTEXTEDITOR_VARIABLE_TEST_H diff --git a/autotests/src/variable_test.cpp b/autotests/src/variable_test.cpp new file mode 100644 --- /dev/null +++ b/autotests/src/variable_test.cpp @@ -0,0 +1,176 @@ +/* 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 + +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("quoteWhenNeeded"); + QTest::addColumn("expected"); + QTest::addColumn("expectedText"); + + QTest::newRow("World1") << "World" << false << "World" << "World"; + QTest::newRow("World2") << "World" << true << "World" << "World"; + QTest::newRow("Smart World1") << "Smart World" << false << "Smart World" << "Smart World"; + QTest::newRow("Smart World2") << "Smart World" << true << "Smart World" << "'Smart World'"; +} + +void VariableTest::testExactMatch() +{ + QFETCH(QString, text); + QFETCH(bool, quoteWhenNeeded); + QFETCH(QString, expected); + QFETCH(QString, expectedText); + const QuotationMode quotationMode = quoteWhenNeeded ? QuotationMode::WhenNeeded : QuotationMode::Never; + + auto editor = KTextEditor::Editor::instance(); + auto doc = editor->createDocument(nullptr); + auto view = doc->createView(nullptr); + doc->setText(text); + + const QString name = QStringLiteral("Document:Text"); + auto func = [](const QStringView&, KTextEditor::View* view) { + return view->document()->text(); + }; + + QVERIFY(editor->registerVariableMatch(name, "Document Text", func)); + + // expandVariable never quotes + QString output; + bool ok = editor->expandVariable(QStringLiteral("Document:Text"), view, output); + QVERIFY(ok); + QCOMPARE(output, expected); + + // expandText quotes depending on quotationMode + ok = editor->expandText(QStringLiteral("Hello %{Document:Text}!"), view, output, quotationMode); + QVERIFY(ok); + QCOMPARE(output, QStringLiteral("Hello ") + expectedText + QLatin1Char('!')); + + ok = editor->expandText(QStringLiteral("Hello %{Document:Text} %{Document:Text}!"), view, output, quotationMode); + QVERIFY(ok); + QCOMPARE(output, QStringLiteral("Hello ") + expectedText + QLatin1Char(' ') + expectedText + QLatin1Char('!')); + + QVERIFY(editor->unregisterVariableMatch("Document: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; + bool ok = editor->expandVariable(QStringLiteral("Mirror:12345"), nullptr, output); + QVERIFY(ok); + QCOMPARE(output, QStringLiteral("54321")); + + ok = editor->expandText(QStringLiteral("Countdown: %{Mirror:12345}"), nullptr, output); + QVERIFY(ok); + QCOMPARE(output, QStringLiteral("Countdown: 54321")); + + ok = editor->expandText(QStringLiteral("Countup: %{Mirror:%{Mirror:12345}}"), nullptr, output); + QVERIFY(ok); + // NOTE: Recursive expansion currently not supported + QCOMPARE(output, QStringLiteral("Countup: %{Mirror:54321}")); + + 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("Document:Text"); + auto func = [](const QStringView&, KTextEditor::View* view) { + return view->document()->text(); + }; + QVERIFY(editor->registerVariableMatch(name, "Document Text", func)); + + doc->setText(QStringLiteral("Text")); + QString output; + const bool ok = editor->expandText(QStringLiteral("Hello %{Document:%{Document:Text}}!"), view, output); + QVERIFY(ok); + // NOTE: Recursive expansion currently not supported + QEXPECT_FAIL("", "Recursive replacements do not seem to work", Continue); + QCOMPARE(output, QStringLiteral("Hello Text!")); + + delete doc; +} + +// kate: indent-mode cstyle; indent-width 4; replace-tabs on; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -195,6 +195,8 @@ utils/katedefaultcolors.cpp utils/katecommandrangeexpressionparser.cpp utils/katesedcmd.cpp +utils/katemacroexpander.cpp +utils/variable.cpp # schema schema/kateschema.cpp diff --git a/src/include/ktexteditor/editor.h b/src/include/ktexteditor/editor.h --- a/src/include/ktexteditor/editor.h +++ b/src/include/ktexteditor/editor.h @@ -41,9 +41,33 @@ class Application; class Command; class Document; +class View; class EditorPrivate; class ConfigPage; + +/** + * Enum that defines whether replaced variables in expandVariable() and + * Editor::expandText() are quoted in case the replacement text contains + * spaces. + * + * QuotationMode::WhenNeeded is typically used when passing the replaced + * text as arguments to a child process. + * + * @since 5.57 + */ +enum class QuotationMode +{ + /** + * Never quote. + */ + Never, + /** + * Quote when the replacement text contains spaces. + */ + WhenNeeded +}; + /** * \brief Accessor interface for the KTextEditor framework. * @@ -246,6 +270,83 @@ */ virtual QStringList commandList() const = 0; +public: + /** + * Function that is called to expand a variable in @p text. + */ + using ExpandFunction = QString (*)(const QStringView& text, KTextEditor::View* view); + + /** + * Registers a variable called @p name for exact matches. + * For instance, a variable called "CurrentDocument:Path" could be + * registered which then expands to the path the current document. + * + * @return true on success, false if the variable could not be registered, + * e.g. because it already was registered previously. + * + * @since 5.57 + */ + bool registerVariableMatch(const QString& name, const QString& description, ExpandFunction expansionFunc); + + /** + * Registers a variable for arbitrary text that matches the specified + * prefix. For instance, a variable called "ENV:" could be registered + * which then expands arbitrary environment variables, e.g. ENV:HOME + * would expand to the user's home directory. + * + * @note A colon ':' is used as separator for the prefix and the text + * after the colon that should be evaluated. + * + * @return true on success, false if a prefix could not be registered, + * e.g. because it already was registered previously. + * + * @since 5.57 + */ + bool registerVariablePrefix(const QString& prefix, const QString& description, ExpandFunction expansionFunc); + + /** + * Unregisters a variable that was previously registered with + * registerVariableMatch(). + * + * @return true if the variable was successfully unregistered, and + * false if the variable did not exist. + * + * @since 5.57 + */ + bool unregisterVariableMatch(const QString& variable); + + /** + * Unregisters a prefix of variable that was previously registered with + * registerVariableMatch(). + * + * @return true if the variable was successfully unregistered, and + * false if the variable did not exist. + * + * @since 5.57 + */ + bool unregisterVariablePrefix(const QString& variable); + + /** + * Expands a single @p variable, writing the expanded value to @p output. + * The expanded text in @p output is never quoted. + * + * @return true on success, otherwise false. + * + * @since 5.57 + */ + bool expandVariable(const QString& variable, KTextEditor::View* view, QString& output) const; + + /** + * Expands arbitrary @p text that may contain arbitrary many variables. + * On success, the expanded text is written to @p output. Depending on + * the @p quotationMode, the @p output is quoted when needed. + * + * @return true on success, otherwise false. + * + * @since 5.57 + */ + bool expandText(const QString& text, KTextEditor::View* view, QString& output, QuotationMode quotationMode = QuotationMode::Never) const; + private: /** * private d-pointer, pointing to the internal implementation diff --git a/src/utils/kateglobal.h b/src/utils/kateglobal.h --- a/src/utils/kateglobal.h +++ b/src/utils/kateglobal.h @@ -24,6 +24,7 @@ #include #include "katescript.h" +#include "variable.h" #include #include "ktexteditor/view.h" @@ -554,6 +555,16 @@ */ QStringListModel *m_searchHistoryModel; QStringListModel *m_replaceHistoryModel; + + /** + * Contains a lookup from the variable to the Variable instance. + */ + QHash m_variableExactMatches; + + /** + * Contains a lookup from the variable prefix to the Variable instance. + */ + QHash m_variablePrefixMatches; }; } diff --git a/src/utils/katemacroexpander.h b/src/utils/katemacroexpander.h new file mode 100644 --- /dev/null +++ b/src/utils/katemacroexpander.h @@ -0,0 +1,49 @@ +/* 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. + */ +#ifndef KTEXTEDITOR_MACRO_EXPANDER_H +#define KTEXTEDITOR_MACRO_EXPANDER_H + +#include + +#include + +namespace KTextEditor +{ + class View; +} + +/** + * Helper class for macro expansion. + */ +class KateMacroExpander : public KWordMacroExpander +{ +public: + KateMacroExpander(KTextEditor::View* view); + +protected: + bool expandMacro(const QString& str, QStringList& ret) override; + +private: + KTextEditor::View* m_view; +}; + +#endif // KTEXTEDITOR_MACRO_EXPANDER_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/utils/katemacroexpander.cpp b/src/utils/katemacroexpander.cpp new file mode 100644 --- /dev/null +++ b/src/utils/katemacroexpander.cpp @@ -0,0 +1,46 @@ +/* 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 "katemacroexpander.h" + +#include + +KateMacroExpander::KateMacroExpander(KTextEditor::View* view) + : KWordMacroExpander() + , m_view(view) +{ +} + +bool KateMacroExpander::expandMacro(const QString& str, QStringList& ret) +{ + QString variable = str; + const int macroIndex = str.indexOf(QLatin1Char('%')); + if (macroIndex >= 0) { + KTextEditor::Editor::instance()->expandVariable(str, m_view, variable); + } + + QString output; + const bool success = KTextEditor::Editor::instance()->expandVariable(variable, m_view, output); + if (success) { + ret << output; + } + return success; +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/utils/ktexteditor.cpp b/src/utils/ktexteditor.cpp --- a/src/utils/ktexteditor.cpp +++ b/src/utils/ktexteditor.cpp @@ -40,13 +40,15 @@ #include "modificationinterface.h" #include "sessionconfiginterface.h" #include "texthintinterface.h" +#include "variable.h" #include "annotationinterface.h" #include "abstractannotationitemdelegate.h" #include "kateglobal.h" #include "kateconfig.h" #include "katecmd.h" +#include "katemacroexpander.h" using namespace KTextEditor; @@ -103,6 +105,90 @@ return d->documentConfig()->encoding (); } +bool Editor::registerVariableMatch(const QString& name, const QString& description, ExpandFunction expansionFunc) +{ + if (name.isEmpty() || expansionFunc == nullptr) + return false; + + if (d->m_variableExactMatches.contains(name)) + return false; + + d->m_variableExactMatches.insert(name, Variable(name, description, expansionFunc)); + return true; +} + +bool Editor::registerVariablePrefix(const QString& prefix, const QString& description, ExpandFunction expansionFunc) +{ + if (prefix.isEmpty() || expansionFunc == nullptr) + return false; + + if (d->m_variablePrefixMatches.contains(prefix)) + return false; + + if (!prefix.contains(QLatin1Char(':'))) + return false; + + d->m_variablePrefixMatches.insert(prefix, Variable(prefix, description, expansionFunc)); + return true; +} + +bool Editor::unregisterVariableMatch(const QString& variable) +{ + auto it = d->m_variableExactMatches.find(variable); + if (it != d->m_variableExactMatches.end()) { + d->m_variableExactMatches.erase(it); + return true; + } + return false; +} + +bool Editor::unregisterVariablePrefix(const QString& variable) +{ + auto it = d->m_variablePrefixMatches.find(variable); + if (it != d->m_variablePrefixMatches.end()) { + d->m_variablePrefixMatches.erase(it); + return true; + } + return false; +} + +bool Editor::expandVariable(const QString& variable, KTextEditor::View* view, QString& output) const +{ + // first try exact matches + const auto it = d->m_variableExactMatches.find(variable); + if (it != d->m_variableExactMatches.end()) { + output = it->evaluate(variable, view); + return true; + } + + // try prefix matching + const int colonIndex = variable.indexOf(QLatin1Char(':')); + if (colonIndex >= 0) { + const QString prefix = variable.left(colonIndex + 1); + const auto itPrefix = d->m_variablePrefixMatches.find(prefix); + if (itPrefix != d->m_variablePrefixMatches.end()) { + output = itPrefix->evaluate(variable, view); + return true; + } + } + return false; +} + +bool Editor::expandText(const QString& text, KTextEditor::View* view, QString& output, QuotationMode quotationMode) const +{ + output = text; + + KateMacroExpander macroExpander(view); + bool success = false; + if (quotationMode == QuotationMode::Never) { + macroExpander.expandMacros(output); + success = true; + } else { + success = macroExpander.expandMacrosShellQuote(output); + } + return success; +} + bool View::insertText(const QString &text) { KTextEditor::Document *doc = document(); diff --git a/src/utils/variable.h b/src/utils/variable.h new file mode 100644 --- /dev/null +++ b/src/utils/variable.h @@ -0,0 +1,107 @@ +/* This file is part of the KDE project + Copyright (C) 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. +*/ + +#ifndef KTEXTEDITOR_VARIABLE_H +#define KTEXTEDITOR_VARIABLE_H + +#include +#include + +namespace KTextEditor +{ + class View; + +/** + * @brief Variable for variable expansion. + * + * @section variable_intro Introduction + * + * A Variable is used by the KTextEditor::Editor to expand variables, also + * know as expanding macros. A Variable itself is defined by the variable + * name() as well as a description() and a function that replaces the variable + * by its value. + * + * To register a Variable in the Editor use either Editor::registerVariableMatch() + * or Editor::registerPrefixMatch(). + * + * @see KTextEditor::Editor, KTextEditor::Editor::registerVariableMatch(), + * KTextEditor::Editor::registerPrefixMatch() + * @author Dominik Haumann \ + */ +class Variable +{ +public: + /** + * Function that is called to expand a variable in @p text. + * @param text + */ + using ExpandFunction = QString (*)(const QStringView& text, KTextEditor::View* view); + + /** + * Constructor defining a Variable by its @p name, its @p description, and + * its function @p expansionFunc to expand a variable to its corresponding + * value. + * + * @note The @p name should @e not be translated. + */ + Variable(const QString& name, const QString& description, ExpandFunction expansionFunc); + + /** + * Returns true, if the name is non-empty and the function provided in the + * constructor is not a nullptr. + */ + bool isValid() const; + + /** + * Returns the @p name that was provided in the constructor. + * Depending on where the Variable is registered, this name is used to + * identify an exact match or a prefix match. + */ + QString name() const; + + /** + * Returns the description that was provided in the constructor. + */ + QString description() const; + + /** + * Expands the Variable to its value. + * + * As example for an exact match, a variable "CurerntDocument:Cursor:Line" + * uses the @p view to return the current line of the text cursor. In this + * case @p prefix equals the text of the variable itself, i.e. + * "CurerntDocument:Cursor:Line". + * + * As example of a prefix match, a variable "ENV:value" expands the + * environment value @e value, e.g. "ENV:HOME". In this case, the @p prefix + * equals the text "ENV:HOME" and @p view would be unused. + * + * @return the expanded variable. + */ + QString evaluate(const QStringView& prefix, KTextEditor::View * view) const; + +private: + QString m_name; + QString m_description; + ExpandFunction m_function; +}; + +} + +#endif diff --git a/src/utils/variable.cpp b/src/utils/variable.cpp new file mode 100644 --- /dev/null +++ b/src/utils/variable.cpp @@ -0,0 +1,50 @@ +/* This file is part of the KDE project + Copyright (C) 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.h" + +namespace KTextEditor +{ + +Variable::Variable(const QString& name, const QString& description, Variable::ExpandFunction func) + : m_name(name) + , m_description(description) + , m_function(func) +{} + +bool Variable::isValid() const +{ + return (!m_name.isEmpty()) && (m_function != nullptr); +} + +QString Variable::name() const +{ + return m_name; +} + +QString Variable::description() const +{ + return m_description; +} + +QString Variable::evaluate(const QStringView& prefix, KTextEditor::View * view) const +{ + return isValid() ? m_function(prefix, view) : QString(); +} + +}