From 1682f0fb3690f61a3539625d75fa5fefb2f95453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 26 Dec 2021 14:21:52 +0100 Subject: [PATCH] Refactor Tab completion for Twitch commands (#3144) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/common/CompletionModel.cpp | 67 +++++++++++++------ src/common/CompletionModel.hpp | 4 +- .../commands/CommandController.cpp | 65 ++++-------------- .../commands/CommandController.hpp | 6 +- src/providers/twitch/TwitchCommon.hpp | 37 ++++++++++ src/util/Helpers.cpp | 11 +++ src/util/Helpers.hpp | 7 ++ src/widgets/settingspages/CommandPage.cpp | 4 +- 9 files changed, 125 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dced6a99..d0c745c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - Minor: Show picked outcome in prediction badges. (#3357) - Minor: Add support for Emoji in IRC (#3354) - Minor: Moved `/live` logs to its own subdirectory. (Logs from before this change will still be available in `Channels -> live`). (#3393) +- Minor: Added autocompletion for default Twitch commands starting with the dot (e.g. `.mods` which does the same as `/mods`). (#3144) - Minor: Sorted usernames in `Users joined/parted` messages alphabetically. (#3421) - Minor: Mod list, VIP list, and Users joined/parted messages are now searchable. (#3426) - Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362) diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index ac96b85b4..5658e204c 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -7,6 +7,7 @@ #include "controllers/commands/CommandController.hpp" #include "debug/Benchmark.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" @@ -85,21 +86,40 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) // Twitch channel auto tc = dynamic_cast(&this->channel_); - std::function addString; - if (getSettings()->prefixOnlyEmoteCompletion) - { - addString = [=](const QString &str, TaggedString::Type type) { - if (str.startsWith(prefix, Qt::CaseInsensitive)) - this->items_.emplace(str + " ", type); - }; - } - else - { - addString = [=](const QString &str, TaggedString::Type type) { - if (str.contains(prefix, Qt::CaseInsensitive)) - this->items_.emplace(str + " ", type); - }; - } + auto addString = [=](const QString &str, TaggedString::Type type) { + // Special case for handling default Twitch commands + if (type == TaggedString::TwitchCommand) + { + if (prefix.size() < 2) + { + return; + } + + auto prefixChar = prefix.at(0); + + static std::set validPrefixChars{'/', '.'}; + + if (validPrefixChars.find(prefixChar) == validPrefixChars.end()) + { + return; + } + + if (startsWithOrContains((prefixChar + str), prefix, + Qt::CaseInsensitive, + getSettings()->prefixOnlyEmoteCompletion)) + { + this->items_.emplace((prefixChar + str + " "), type); + } + + return; + } + + if (startsWithOrContains(str, prefix, Qt::CaseInsensitive, + getSettings()->prefixOnlyEmoteCompletion)) + { + this->items_.emplace(str + " ", type); + } + }; if (auto account = getApp()->accounts->twitch.getCurrent()) { @@ -190,15 +210,22 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); } - // Commands - for (auto &command : getApp()->commands->items_) + // Custom Chatterino commands + for (auto &command : getApp()->commands->items) { - addString(command.name, TaggedString::Command); + addString(command.name, TaggedString::CustomCommand); } - for (auto &command : getApp()->commands->getDefaultTwitchCommandList()) + // Default Chatterino commands + for (auto &command : getApp()->commands->getDefaultChatterinoCommandList()) { - addString(command, TaggedString::Command); + addString(command, TaggedString::ChatterinoCommand); + } + + // Default Twitch commands + for (auto &command : TWITCH_DEFAULT_COMMANDS) + { + addString(command, TaggedString::TwitchCommand); } } diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index a05c98407..ee810bbe6 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -29,7 +29,9 @@ class CompletionModel : public QAbstractListModel EmoteEnd, // end emotes - Command, + CustomCommand, + ChatterinoCommand, + TwitchCommand, }; TaggedString(const QString &string, Type type); diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 337c325c0..32a15c252 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -8,6 +8,7 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" @@ -34,42 +35,6 @@ namespace { using namespace chatterino; -static const QStringList twitchDefaultCommands{ - "/help", - "/w", - "/me", - "/disconnect", - "/mods", - "/vips", - "/color", - "/commercial", - "/mod", - "/unmod", - "/vip", - "/unvip", - "/ban", - "/unban", - "/timeout", - "/untimeout", - "/slow", - "/slowoff", - "/r9kbeta", - "/r9kbetaoff", - "/emoteonly", - "/emoteonlyoff", - "/clear", - "/subscribers", - "/subscribersoff", - "/followers", - "/followersoff", - "/host", - "/unhost", - "/raid", - "/unraid", -}; - -static const QStringList whisperCommands{"/w", ".w"}; - // stripUserName removes any @ prefix or , suffix to make it more suitable for command use void stripUserName(QString &userName) { @@ -217,7 +182,7 @@ bool appendWhisperMessageStringLocally(const QString &textNoEmoji) QString commandName = words[0]; - if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) + if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive)) { if (words.length() > 2) { @@ -298,13 +263,11 @@ namespace chatterino { void CommandController::initialize(Settings &, Paths &paths) { - this->commandAutoCompletions_ = twitchDefaultCommands; - // Update commands map when the vector of commands has been updated auto addFirstMatchToMap = [this](auto args) { this->userCommands_.remove(args.item.name); - for (const Command &cmd : this->items_) + for (const Command &cmd : this->items) { if (cmd.name == args.item.name) { @@ -315,7 +278,7 @@ void CommandController::initialize(Settings &, Paths &paths) int maxSpaces = 0; - for (const Command &cmd : this->items_) + for (const Command &cmd : this->items) { auto localMaxSpaces = cmd.name.count(' '); if (localMaxSpaces > maxSpaces) @@ -326,8 +289,8 @@ void CommandController::initialize(Settings &, Paths &paths) this->maxSpaces_ = maxSpaces; }; - this->items_.itemInserted.connect(addFirstMatchToMap); - this->items_.itemRemoved.connect(addFirstMatchToMap); + this->items.itemInserted.connect(addFirstMatchToMap); + this->items.itemRemoved.connect(addFirstMatchToMap); // Initialize setting manager for commands.json auto path = combinePath(paths.settingsDirectory, "commands.json"); @@ -343,8 +306,8 @@ void CommandController::initialize(Settings &, Paths &paths) // Update the setting when the vector of commands has been updated (most // likely from the settings dialog) - this->items_.delayedItemsChanged.connect([this] { - this->commandsSetting_->setValue(this->items_.raw()); + this->items.delayedItemsChanged.connect([this] { + this->commandsSetting_->setValue(this->items.raw()); }); // Load commands from commands.json @@ -354,7 +317,7 @@ void CommandController::initialize(Settings &, Paths &paths) // of commands) for (const auto &command : this->commandsSetting_->getValue()) { - this->items_.append(command); + this->items.append(command); } /// Deprecated commands @@ -918,7 +881,7 @@ void CommandController::save() CommandModel *CommandController::createModel(QObject *parent) { CommandModel *model = new CommandModel(parent); - model->initialize(&this->items_); + model->initialize(&this->items); return model; } @@ -939,7 +902,7 @@ QString CommandController::execCommand(const QString &textNoEmoji, // works in a valid Twitch channel and /whispers, etc... if (!dryRun && channel->isTwitchChannel()) { - if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) + if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive)) { if (words.length() > 2) { @@ -1003,7 +966,7 @@ void CommandController::registerCommand(QString commandName, this->commands_[commandName] = commandFunction; - this->commandAutoCompletions_.append(commandName); + this->defaultChatterinoCommandAutoCompletions_.append(commandName); } QString CommandController::execCustomCommand(const QStringList &words, @@ -1114,9 +1077,9 @@ QString CommandController::execCustomCommand(const QStringList &words, } } -QStringList CommandController::getDefaultTwitchCommandList() +QStringList CommandController::getDefaultChatterinoCommandList() { - return this->commandAutoCompletions_; + return this->defaultChatterinoCommandAutoCompletions_; } } // namespace chatterino diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index 427c146e8..b27610109 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -23,11 +23,11 @@ class CommandModel; class CommandController final : public Singleton { public: - SignalVector items_; + SignalVector items; QString execCommand(const QString &text, std::shared_ptr channel, bool dryRun); - QStringList getDefaultTwitchCommandList(); + QStringList getDefaultChatterinoCommandList(); virtual void initialize(Settings &, Paths &paths) override; virtual void save() override; @@ -61,7 +61,7 @@ private: std::unique_ptr>> commandsSetting_; - QStringList commandAutoCompletions_; + QStringList defaultChatterinoCommandAutoCompletions_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchCommon.hpp b/src/providers/twitch/TwitchCommon.hpp index 6c41dbdc4..aaa78a8f5 100644 --- a/src/providers/twitch/TwitchCommon.hpp +++ b/src/providers/twitch/TwitchCommon.hpp @@ -38,4 +38,41 @@ static const std::vector TWITCH_USERNAME_COLORS = { {0, 255, 127}, // SpringGreen }; +static const QStringList TWITCH_DEFAULT_COMMANDS{ + "help", + "w", + "me", + "disconnect", + "mods", + "vips", + "color", + "commercial", + "mod", + "unmod", + "vip", + "unvip", + "ban", + "unban", + "timeout", + "untimeout", + "slow", + "slowoff", + "r9kbeta", + "r9kbetaoff", + "emoteonly", + "emoteonlyoff", + "clear", + "subscribers", + "subscribersoff", + "followers", + "followersoff", + "host", + "unhost", + "raid", + "unraid", + "delete", +}; + +static const QStringList TWITCH_WHISPER_COMMANDS{"/w", ".w"}; + } // namespace chatterino diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index 2f4d98db9..784465840 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -7,6 +7,17 @@ namespace chatterino { +bool startsWithOrContains(const QString &str1, const QString &str2, + Qt::CaseSensitivity caseSensitivity, bool startsWith) +{ + if (startsWith) + { + return str1.startsWith(str2, caseSensitivity); + } + + return str1.contains(str2, caseSensitivity); +} + QString generateUuid() { auto uuid = QUuid::createUuid(); diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 13247f010..10137c9b1 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -5,6 +5,13 @@ namespace chatterino { +/** + * @brief startsWithOrContains is a wrapper for checking + * whether str1 starts with or contains str2 within itself + **/ +bool startsWithOrContains(const QString &str1, const QString &str2, + Qt::CaseSensitivity caseSensitivity, bool startsWith); + QString generateUuid(); QString formatRichLink(const QString &url, bool file = false); diff --git a/src/widgets/settingspages/CommandPage.cpp b/src/widgets/settingspages/CommandPage.cpp index 7dc72c55f..9533489d1 100644 --- a/src/widgets/settingspages/CommandPage.cpp +++ b/src/widgets/settingspages/CommandPage.cpp @@ -46,7 +46,7 @@ CommandPage::CommandPage() view->setTitles({"Trigger", "Command"}); view->getTableView()->horizontalHeader()->setStretchLastSection(true); view->addButtonPressed.connect([] { - getApp()->commands->items_.append( + getApp()->commands->items.append( Command{"/command", "I made a new command HeyGuys"}); }); @@ -65,7 +65,7 @@ CommandPage::CommandPage() { if (int index = line.indexOf(' '); index != -1) { - getApp()->commands->items_.insert( + getApp()->commands->items.insert( Command(line.mid(0, index), line.mid(index + 1))); } }