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.
This commit is contained in:
silverqx
2024-04-02 13:59:49 +02:00
parent 07937a4476
commit dffdd8852c
8 changed files with 249 additions and 7 deletions
+4
View File
@@ -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:
---------------------
+4
View File
@@ -65,6 +65,10 @@ namespace Orm::Utils
/*! Split a string by the given width (not in the middle of a word). */
static std::vector<QString>
splitStringByWidth(const QString &string, int width);
/*! Split a string at the first given character. */
static QList<QStringView>
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,
+33
View File
@@ -405,6 +405,39 @@ std::vector<QString> String::splitStringByWidth(const QString &string, const int
return lines;
}
QList<QStringView> 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)
{
@@ -47,6 +47,10 @@ namespace Tom::Commands
static std::optional<QString>
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 &currentCommandSplitted,
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<QString> &currentCommand,
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);
@@ -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)
+170 -5
View File
@@ -10,6 +10,7 @@
#include <range/v3/view/transform.hpp>
#include <orm/constants.hpp>
#include <orm/macros/likely.hpp>
#include <orm/utils/string.hpp>
#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 &currentCommandSplitted,
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<std::shared_ptr<Command>> &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<QStringView> 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<QList<QStringView>>(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([&sectionNamesSplitted]
(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<QList<QStringView>>();
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<QString> &currentCommand, 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));
+10
View File
@@ -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
+5 -1
View File
@@ -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)