From dffdd8852c80f12629309173f3d7d0560a4d8d94 Mon Sep 17 00:00:00 2001 From: silverqx Date: Tue, 2 Apr 2024 13:59:49 +0200 Subject: [PATCH] tom added completion for about --only= sections Added support for pwsh, zsh, and bash shells. Contains workaround for Register-ArgumentCompleter for array option values, currently, the wordArg is empty, the workaround fix fills the wordArg with the last option value. --- NOTES.txt | 4 + include/orm/utils/string.hpp | 4 + src/orm/utils/string.cpp | 33 ++++ tom/include/tom/commands/completecommand.hpp | 8 + .../tom/commands/stubs/integratestubs.hpp | 16 +- tom/src/tom/commands/completecommand.cpp | 175 +++++++++++++++++- tools/completions/tom.bash | 10 + tools/completions/tom.zsh | 6 +- 8 files changed, 249 insertions(+), 7 deletions(-) diff --git a/NOTES.txt b/NOTES.txt index dcd3c5fcf..bba3fc026 100644 --- a/NOTES.txt +++ b/NOTES.txt @@ -969,6 +969,10 @@ tom complete --word="mi" --commandline="tom mi" --position=6 tom complete --word="--" --commandline="tom migrate --" --position=14 tom complete --word="--p" --commandline="tom migrate --p" --position=15 +tom complete --word="--only=env" --commandline="tom about --only=env" --position=20 +tom complete --word="" --commandline="tom about --only=environment," --position=29 +tom complete --word="m" --commandline="tom about --only=environment,m" --position=30 + cmake build commands: --------------------- diff --git a/include/orm/utils/string.hpp b/include/orm/utils/string.hpp index abf0e57c9..686325be2 100644 --- a/include/orm/utils/string.hpp +++ b/include/orm/utils/string.hpp @@ -65,6 +65,10 @@ namespace Orm::Utils /*! Split a string by the given width (not in the middle of a word). */ static std::vector splitStringByWidth(const QString &string, int width); + /*! Split a string at the first given character. */ + static QList + splitAtFirst(QStringView string, QChar separator, + Qt::SplitBehavior behavior = Qt::KeepEmptyParts); /*! Count number of the given character before the given position. */ static QString::size_type countBefore(QString string, QChar character, diff --git a/src/orm/utils/string.cpp b/src/orm/utils/string.cpp index a0f0e0b30..d99cf5775 100644 --- a/src/orm/utils/string.cpp +++ b/src/orm/utils/string.cpp @@ -405,6 +405,39 @@ std::vector String::splitStringByWidth(const QString &string, const int return lines; } +QList String::splitAtFirst(const QStringView string, const QChar separator, + const Qt::SplitBehavior behavior) +{ + // Nothing to do + if (string.isEmpty()) + return {}; + + const auto index = string.indexOf(separator); + + // Nothing to do, separator was not found + if (index == -1) + return {string}; + + const auto *const begin = string.constBegin(); + const auto *const end = string.constEnd(); + const auto *const itSeparator = string.constBegin() + index; + const auto *const itAfterSeparator = string.constBegin() + index + 1; // +1 to skip the separator + + // Currently, a value before the separator must contain at least one character + Q_ASSERT(begin < itSeparator); + // Standard development check, therefore is separated from the above + Q_ASSERT(itAfterSeparator <= end); + + if (behavior == Qt::SkipEmptyParts && itAfterSeparator == end) + return {{begin, itSeparator}}; + + /* This is correct in all cases, overflow can't happen if there is nothing after + the separator, eg. key=, in this case the beginIndex will point to the constEnd(), + so the result will be like {string.constEnd(), string.constEnd()} what is an empty + string view. */ + return {{begin, itSeparator}, {itAfterSeparator, end}}; +} + QString::size_type String::countBefore(QString string, const QChar character, const QString::size_type position) { diff --git a/tom/include/tom/commands/completecommand.hpp b/tom/include/tom/commands/completecommand.hpp index bdff2c196..5793b1532 100644 --- a/tom/include/tom/commands/completecommand.hpp +++ b/tom/include/tom/commands/completecommand.hpp @@ -47,6 +47,10 @@ namespace Tom::Commands static std::optional getCurrentTomCommand(const QString &commandlineArg, QString::size_type cword); #endif + /*! Get the command-line option value for --word= option (workaround for pwsh). */ + QString getWordOptionValue( + const QStringList ¤tCommandSplitted, + QString::size_type positionArg, QString::size_type commandlineArgSize); /*! Print all guessed commands. */ int printGuessedCommands( @@ -59,6 +63,8 @@ namespace Tom::Commands int printAndGuessConnectionNames(const QString &connectionName) const; /*! Print all or guessed environment names for the --env= option. */ int printEnvironmentNames(const QString &environmentName) const; + /*! Print all section names for the about command --only= option. */ + int printSectionNamesForAbout(QStringView sectionNamesValue) const; /*! Print all or guessed long option parameter names. */ int printGuessedLongOptions(const std::optional ¤tCommand, const QString &word) const; @@ -89,6 +95,8 @@ namespace Tom::Commands inline static bool isLongOption(const QString &wordArg); /*! Determine whether the given word is a short option argument. */ inline static bool isShortOption(const QString &wordArg); + /*! Determine if the given word is a long option argument with an array value. */ + inline static bool isLongOptionWithArrayValue(const QString &wordArg); /*! Get the command-line option value (eg. --database=value). */ static QString getOptionValue(const QString &wordArg); diff --git a/tom/include/tom/commands/stubs/integratestubs.hpp b/tom/include/tom/commands/stubs/integratestubs.hpp index aff1661b6..e6a6a1c74 100644 --- a/tom/include/tom/commands/stubs/integratestubs.hpp +++ b/tom/include/tom/commands/stubs/integratestubs.hpp @@ -77,6 +77,10 @@ __tom_environments() { echo 'dev development local prod production test testing staging' } +__tom_about_sections() { + echo 'environment macros versions connections' +} + _tom() { local cur prev words cword split @@ -123,6 +127,12 @@ _tom() return fi + # Complete section names for about command --only= option + if [[ -v tom_command ]] && [[ $tom_command == 'about' ]] && [[ $prev == '--only' ]]; then + COMPREPLY=($(compgen -W "$(__tom_about_sections)" -- "$cur")) + return + fi + # Accurate completion using the tom complete command if _have tom; then # Completion for positional arguments and for long and short options @@ -234,6 +244,10 @@ __tom_namespaces() { _values namespace 'global' 'db' 'make' 'migrate' 'namespaced' 'all' } +__tom_about_sections() { + _values -s , section 'connections' 'environment' 'macros' 'versions' +} + # Try to infer database connection names if a user is in the right folder and have tagged # connection names with '// shell:connection' comment __tom_connections() { @@ -332,7 +346,7 @@ _tom() { $common_options \ '--json[Output the information as JSON]' \ '--pretty[Enable JSON human readable output]' \ - '--only=[Sections to display (partial match)]:section names' + '--only=[Sections to display (partial match)]:section names:__tom_about_sections' ;; (env|inspire) diff --git a/tom/src/tom/commands/completecommand.cpp b/tom/src/tom/commands/completecommand.cpp index 20c78968d..6e254d9f4 100644 --- a/tom/src/tom/commands/completecommand.cpp +++ b/tom/src/tom/commands/completecommand.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include "tom/application.hpp" @@ -24,7 +25,9 @@ TINYORM_BEGIN_COMMON_NAMESPACE namespace fs = std::filesystem; +using Orm::Constants::COMMA_C; using Orm::Constants::DASH; +using Orm::Constants::EMPTY; using Orm::Constants::EQ_C; using Orm::Constants::NEWLINE; using Orm::Constants::NOSPACE; @@ -33,6 +36,7 @@ using Orm::Constants::database_; using StringUtils = Orm::Utils::String; +using Tom::Constants::About; using Tom::Constants::DoubleDash; using Tom::Constants::Env; using Tom::Constants::Help; @@ -42,6 +46,7 @@ using Tom::Constants::LongOption; using Tom::Constants::ShPwsh; using Tom::Constants::commandline; using Tom::Constants::commandline_up; +using Tom::Constants::only_; using Tom::Constants::word_; using Tom::Constants::word_up; @@ -90,7 +95,6 @@ int CompleteCommand::run() // NOLINT(readability-function-cognitive-complexity) Command::run(); /* Initialization section */ - const auto wordArg = value(word_); const auto commandlineArg = value(commandline); #ifdef _MSC_VER @@ -100,15 +104,20 @@ int CompleteCommand::run() // NOLINT(readability-function-cognitive-complexity) const auto positionArg = value(position_).toInt(); # endif - const auto commandlineArgSize = commandlineArg.size(); - // Currently processed tom command const auto currentCommandSplitted = commandlineArg.split(SPACE); Q_ASSERT(!currentCommandSplitted.isEmpty()); - const auto currentCommandArg = getCurrentTomCommand(currentCommandSplitted); - const auto tomCommandSize = currentCommandSplitted.constFirst().size(); + const auto currentCommandArg = getCurrentTomCommand(currentCommandSplitted); + const auto tomCommandSize = currentCommandSplitted.constFirst().size(); + + // Register-ArgumentCompleter --word with workaround for arrays + const auto commandlineArgSize = commandlineArg.size(); + const auto wordArg = getWordOptionValue(currentCommandSplitted, positionArg, + commandlineArgSize); #else + const auto wordArg = value(word_); + # if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const auto cwordArg = value(cword_).toLongLong(); # else @@ -178,6 +187,16 @@ int CompleteCommand::run() // NOLINT(readability-function-cognitive-complexity) #endif return printGuessedShells(wordArg); + /* Print all or guessed section names for the about command --only= option + --- */ + // Bash has it's own guess logic in the tom.bash complete file +#ifdef _MSC_VER + if (currentCommandArg == About && wordArg.startsWith(LongOption.arg(only_)) && + positionArg >= commandlineArgSize + ) + return printSectionNamesForAbout(getOptionValue(wordArg)); +#endif + /* Print inferred database connection names for the --database= option --- */ // Bash has it's own guess logic in the tom.bash complete file @@ -259,6 +278,49 @@ CompleteCommand::getCurrentTomCommand(const QString &commandlineArg, } #endif +QString CompleteCommand::getWordOptionValue( + const QStringList ¤tCommandSplitted, + const QString::size_type positionArg, const QString::size_type commandlineArgSize) +{ + /* This method contains a special handling (alternative to getOptionValue() method) + with workaround for the --word= pwsh option from the Register-ArgumentCompleter. + It fixes cases when the option value on the command-line to complete contains + array value like --only=version,| in this case, pwsh provides/fills + the $wordToComplete in the Register-ArgumentCompleter with an empty string so + there is no way how to correctly complete these values. + This method workarounds/fixes this behavior and instead of an empty string provides + the correct value you would expect like --only=version,| or --only=version,ma| + which enables to complete the given partial array values. */ + + auto wordArg = value(word_); + + /* Nothing to do, cursor is already after an option, eg: --only=env | or --only=env, | + or somewhere before. */ + if (positionArg != commandlineArgSize) + return wordArg; + + const auto &lastArg = currentCommandSplitted.constLast(); + + /* This condition can't be true with the current Register-ArgumentCompleter + implementation, it ensures that our completion will work correctly if this will be + by any chance fixed in future pwsh versions. 🙃 */ + if (wordArg == lastArg) + return wordArg; + + const auto isLongOption = CompleteCommand::isLongOption(lastArg); + const auto isWordArgEmpty = wordArg.isEmpty(); + + // Targets --only=macros,| and returns --only=macros, + if ((isLongOption && isWordArgEmpty && lastArg.endsWith(COMMA_C)) || + // Targets --only=macros,versions,en| and returns --only=macros,versions,en + isLongOptionWithArrayValue(lastArg) + ) T_UNLIKELY + return lastArg; + + else T_LIKELY + return wordArg; +} + int CompleteCommand::printGuessedCommands( const std::vector> &commands) const { @@ -400,6 +462,96 @@ int CompleteCommand::printEnvironmentNames(const QString &environmentName) const return EXIT_SUCCESS; } +namespace +{ + /*! Return type for the initializePrintSectionNamesForAbout() function. */ + struct PrintSectionNamesForAboutType + { + /*! Section name to complete/find (passed on command-line). */ + QString sectionArg; + /*! All section names for completion (excluding already printed section names). */ + QList allSectionNamesFiltered; + /*! Determine whether completing the first section name (need by pwsh). */ + bool isFirstValue; + /*! Print all section names (if the section name input is empty). */ + bool printAllSectionNames; + }; + + /*! Initialize local variables for the printSectionNamesForAbout() method. */ + PrintSectionNamesForAboutType + initializePrintSectionNamesForAbout(const QStringView sectionNamesValue, + const QStringList &allSectionNames) + { + // Nothing to do, the wordArg is empty, return right away as we know the resut + if (sectionNamesValue.isEmpty()) + return {EMPTY, ranges::to>(allSectionNames), true, true}; + + // Current wordArg, section names already displayed on the command-line + auto sectionNamesSplitted = sectionNamesValue.split(COMMA_C, Qt::KeepEmptyParts); + // Needed for pwsh, determines an output format + const auto isFirstValue = sectionNamesSplitted.size() == 1; + /* Currently completed section name, we need to take it out so that this section + name is not filtered out in the ranges::views::filter() algorithm below. */ + const auto sectionArg = sectionNamesSplitted.takeLast(); + const auto printAllSectionNames = sectionArg.isEmpty(); + + // Remove all empty and null strings (it would print all section names w/o this) + sectionNamesSplitted.removeAll({}); + + // Filter out section names that are already displayed on the command-line + auto allSectionNamesFiltered = + allSectionNames | ranges::views::filter([§ionNamesSplitted] + (const QString &allSectionName) + { + // Include all of section names that aren't already on the command-line + return std::ranges::all_of(sectionNamesSplitted, + [&allSectionName](const QStringView sectionName) + { + return !allSectionName.startsWith(sectionName); + }); + }) + | ranges::to>(); + + return {sectionArg.toString(), std::move(allSectionNamesFiltered), isFirstValue, + printAllSectionNames}; + } +} // namespace + +int CompleteCommand::printSectionNamesForAbout(const QStringView sectionNamesValue) const +{ + static const QStringList allSectionNames { + sl("environment"), sl("macros"), sl("versions"), sl("connections"), + }; + + // Initialize local variables + auto [sectionArg, allSectionNamesFiltered, isFirstValue, printAllSectionNames] = + initializePrintSectionNamesForAbout(sectionNamesValue, allSectionNames); + + QStringList sectionNames; + sectionNames.reserve(allSectionNamesFiltered.size()); + + for (const auto section : allSectionNamesFiltered) + /* It also evaluates to true if the given environmentName is an empty string "", + so it prints all environment names in this case. + Also --env= has to be prepended because pwsh overwrites whole option. */ + if (printAllSectionNames || section.startsWith(sectionArg)) + sectionNames << sl("%1;%2").arg( + /* This is weird, but for the first section name we must + print also --only= and for the next section we don't, + reason is that for subsequent sections the wordArg is + empty so pwsh doesn't rewrite the whole --only= option + text so we must print the section name only. */ + isFirstValue + ? NOSPACE.arg(LongOption.arg(only_).append(EQ_C), section) + : section, + section); + + // Print + note(sectionNames.join(NEWLINE)); + + return EXIT_SUCCESS; +} + int CompleteCommand::printGuessedLongOptions( const std::optional ¤tCommand, const QString &word) const { @@ -565,6 +717,19 @@ bool CompleteCommand::isOptionArgument(const QString &wordArg, const OptionType Q_UNREACHABLE(); } +bool CompleteCommand::isLongOptionWithArrayValue(const QString &wordArg) +{ + // Nothing to check, not a long option + if (!isLongOption(wordArg)) + return false; + + const auto wordArgSplitted = StringUtils::splitAtFirst(wordArg, EQ_C, + Qt::KeepEmptyParts); + + // Checks --only=macros, or --only=macros,en + return wordArgSplitted.size() == 2 && wordArgSplitted.constLast().contains(COMMA_C); +} + QString CompleteCommand::getOptionValue(const QString &wordArg) { Q_ASSERT(wordArg.contains(EQ_C)); diff --git a/tools/completions/tom.bash b/tools/completions/tom.bash index dd4512958..08cf02af7 100644 --- a/tools/completions/tom.bash +++ b/tools/completions/tom.bash @@ -36,6 +36,10 @@ __tom_environments() { echo 'dev development local prod production test testing staging' } +__tom_about_sections() { + echo 'environment macros versions connections' +} + _tom() { local cur prev words cword split @@ -82,6 +86,12 @@ _tom() return fi + # Complete section names for about command --only= option + if [[ -v tom_command ]] && [[ $tom_command == 'about' ]] && [[ $prev == '--only' ]]; then + COMPREPLY=($(compgen -W "$(__tom_about_sections)" -- "$cur")) + return + fi + # Accurate completion using the tom complete command if _have tom; then # Completion for positional arguments and for long and short options diff --git a/tools/completions/tom.zsh b/tools/completions/tom.zsh index 1cdce0993..c250e516a 100644 --- a/tools/completions/tom.zsh +++ b/tools/completions/tom.zsh @@ -36,6 +36,10 @@ __tom_namespaces() { _values namespace 'global' 'db' 'make' 'migrate' 'namespaced' 'all' } +__tom_about_sections() { + _values -s , section 'connections' 'environment' 'macros' 'versions' +} + # Try to infer database connection names if a user is in the right folder and have tagged # connection names with '// shell:connection' comment __tom_connections() { @@ -134,7 +138,7 @@ _tom() { $common_options \ '--json[Output the information as JSON]' \ '--pretty[Enable JSON human readable output]' \ - '--only=[Sections to display (partial match)]:section names' + '--only=[Sections to display (partial match)]:section names:__tom_about_sections' ;; (env|inspire)