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,164 @@ +/* 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("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; +} + +// 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,11 @@ class Application; class Command; class Document; +class View; class EditorPrivate; class ConfigPage; + /** * \brief Accessor interface for the KTextEditor framework. * @@ -246,6 +248,79 @@ */ 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. + * + * @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. + * + * @since 5.57 + */ + void expandText(const QString& text, KTextEditor::View* view, QString& output) 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,44 @@ +/* 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 + +namespace KTextEditor +{ + class View; +} + +/** + * Helper for macro expansion. + */ +namespace KateMacroExpander +{ + /** + * Expands the @p input text based on the @p view. + * @return the expanded text. + */ + QString expandMacro(const QString& input, KTextEditor::View* 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,73 @@ +/* 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 + +/** + * 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++; + } + } + return -1; +} + +QString KateMacroExpander::expandMacro(const QString& input, KTextEditor::View* view) +{ + QString output = input; + QString oldStr; + do { + oldStr = output; + const int startIndex = output.indexOf(QStringLiteral("%{")); + 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; +} + +// 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,80 @@ 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; +} + +void Editor::expandText(const QString& text, KTextEditor::View* view, QString& output) const +{ + output = KateMacroExpander::expandMacro(text, view); +} + 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(); +} + +}