Refactor Tab completion for Twitch commands (#3144)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Paweł
2021-12-26 14:21:52 +01:00
committed by GitHub
parent 94f7f09e73
commit 1682f0fb36
9 changed files with 125 additions and 77 deletions
+1
View File
@@ -38,6 +38,7 @@
- Minor: Show picked outcome in prediction badges. (#3357) - Minor: Show picked outcome in prediction badges. (#3357)
- Minor: Add support for Emoji in IRC (#3354) - 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: 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: Sorted usernames in `Users joined/parted` messages alphabetically. (#3421)
- Minor: Mod list, VIP list, and Users joined/parted messages are now searchable. (#3426) - 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) - Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362)
+47 -20
View File
@@ -7,6 +7,7 @@
#include "controllers/commands/CommandController.hpp" #include "controllers/commands/CommandController.hpp"
#include "debug/Benchmark.hpp" #include "debug/Benchmark.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@@ -85,21 +86,40 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
// Twitch channel // Twitch channel
auto tc = dynamic_cast<TwitchChannel *>(&this->channel_); auto tc = dynamic_cast<TwitchChannel *>(&this->channel_);
std::function<void(const QString &str, TaggedString::Type type)> addString; auto addString = [=](const QString &str, TaggedString::Type type) {
if (getSettings()->prefixOnlyEmoteCompletion) // Special case for handling default Twitch commands
{ if (type == TaggedString::TwitchCommand)
addString = [=](const QString &str, TaggedString::Type type) { {
if (str.startsWith(prefix, Qt::CaseInsensitive)) if (prefix.size() < 2)
this->items_.emplace(str + " ", type); {
}; return;
} }
else
{ auto prefixChar = prefix.at(0);
addString = [=](const QString &str, TaggedString::Type type) {
if (str.contains(prefix, Qt::CaseInsensitive)) static std::set<QChar> validPrefixChars{'/', '.'};
this->items_.emplace(str + " ", type);
}; 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()) 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); addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
} }
// Commands // Custom Chatterino commands
for (auto &command : getApp()->commands->items_) 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);
} }
} }
+3 -1
View File
@@ -29,7 +29,9 @@ class CompletionModel : public QAbstractListModel
EmoteEnd, EmoteEnd,
// end emotes // end emotes
Command, CustomCommand,
ChatterinoCommand,
TwitchCommand,
}; };
TaggedString(const QString &string, Type type); TaggedString(const QString &string, Type type);
+14 -51
View File
@@ -8,6 +8,7 @@
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
@@ -34,42 +35,6 @@
namespace { namespace {
using namespace chatterino; 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 // stripUserName removes any @ prefix or , suffix to make it more suitable for command use
void stripUserName(QString &userName) void stripUserName(QString &userName)
{ {
@@ -217,7 +182,7 @@ bool appendWhisperMessageStringLocally(const QString &textNoEmoji)
QString commandName = words[0]; QString commandName = words[0];
if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive))
{ {
if (words.length() > 2) if (words.length() > 2)
{ {
@@ -298,13 +263,11 @@ namespace chatterino {
void CommandController::initialize(Settings &, Paths &paths) void CommandController::initialize(Settings &, Paths &paths)
{ {
this->commandAutoCompletions_ = twitchDefaultCommands;
// Update commands map when the vector of commands has been updated // Update commands map when the vector of commands has been updated
auto addFirstMatchToMap = [this](auto args) { auto addFirstMatchToMap = [this](auto args) {
this->userCommands_.remove(args.item.name); this->userCommands_.remove(args.item.name);
for (const Command &cmd : this->items_) for (const Command &cmd : this->items)
{ {
if (cmd.name == args.item.name) if (cmd.name == args.item.name)
{ {
@@ -315,7 +278,7 @@ void CommandController::initialize(Settings &, Paths &paths)
int maxSpaces = 0; int maxSpaces = 0;
for (const Command &cmd : this->items_) for (const Command &cmd : this->items)
{ {
auto localMaxSpaces = cmd.name.count(' '); auto localMaxSpaces = cmd.name.count(' ');
if (localMaxSpaces > maxSpaces) if (localMaxSpaces > maxSpaces)
@@ -326,8 +289,8 @@ void CommandController::initialize(Settings &, Paths &paths)
this->maxSpaces_ = maxSpaces; this->maxSpaces_ = maxSpaces;
}; };
this->items_.itemInserted.connect(addFirstMatchToMap); this->items.itemInserted.connect(addFirstMatchToMap);
this->items_.itemRemoved.connect(addFirstMatchToMap); this->items.itemRemoved.connect(addFirstMatchToMap);
// Initialize setting manager for commands.json // Initialize setting manager for commands.json
auto path = combinePath(paths.settingsDirectory, "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 // Update the setting when the vector of commands has been updated (most
// likely from the settings dialog) // likely from the settings dialog)
this->items_.delayedItemsChanged.connect([this] { this->items.delayedItemsChanged.connect([this] {
this->commandsSetting_->setValue(this->items_.raw()); this->commandsSetting_->setValue(this->items.raw());
}); });
// Load commands from commands.json // Load commands from commands.json
@@ -354,7 +317,7 @@ void CommandController::initialize(Settings &, Paths &paths)
// of commands) // of commands)
for (const auto &command : this->commandsSetting_->getValue()) for (const auto &command : this->commandsSetting_->getValue())
{ {
this->items_.append(command); this->items.append(command);
} }
/// Deprecated commands /// Deprecated commands
@@ -918,7 +881,7 @@ void CommandController::save()
CommandModel *CommandController::createModel(QObject *parent) CommandModel *CommandController::createModel(QObject *parent)
{ {
CommandModel *model = new CommandModel(parent); CommandModel *model = new CommandModel(parent);
model->initialize(&this->items_); model->initialize(&this->items);
return model; return model;
} }
@@ -939,7 +902,7 @@ QString CommandController::execCommand(const QString &textNoEmoji,
// works in a valid Twitch channel and /whispers, etc... // works in a valid Twitch channel and /whispers, etc...
if (!dryRun && channel->isTwitchChannel()) if (!dryRun && channel->isTwitchChannel())
{ {
if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive))
{ {
if (words.length() > 2) if (words.length() > 2)
{ {
@@ -1003,7 +966,7 @@ void CommandController::registerCommand(QString commandName,
this->commands_[commandName] = commandFunction; this->commands_[commandName] = commandFunction;
this->commandAutoCompletions_.append(commandName); this->defaultChatterinoCommandAutoCompletions_.append(commandName);
} }
QString CommandController::execCustomCommand(const QStringList &words, 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 } // namespace chatterino
@@ -23,11 +23,11 @@ class CommandModel;
class CommandController final : public Singleton class CommandController final : public Singleton
{ {
public: public:
SignalVector<Command> items_; SignalVector<Command> items;
QString execCommand(const QString &text, std::shared_ptr<Channel> channel, QString execCommand(const QString &text, std::shared_ptr<Channel> channel,
bool dryRun); bool dryRun);
QStringList getDefaultTwitchCommandList(); QStringList getDefaultChatterinoCommandList();
virtual void initialize(Settings &, Paths &paths) override; virtual void initialize(Settings &, Paths &paths) override;
virtual void save() override; virtual void save() override;
@@ -61,7 +61,7 @@ private:
std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>> std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>>
commandsSetting_; commandsSetting_;
QStringList commandAutoCompletions_; QStringList defaultChatterinoCommandAutoCompletions_;
}; };
} // namespace chatterino } // namespace chatterino
+37
View File
@@ -38,4 +38,41 @@ static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
{0, 255, 127}, // SpringGreen {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 } // namespace chatterino
+11
View File
@@ -7,6 +7,17 @@
namespace chatterino { 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() QString generateUuid()
{ {
auto uuid = QUuid::createUuid(); auto uuid = QUuid::createUuid();
+7
View File
@@ -5,6 +5,13 @@
namespace chatterino { 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 generateUuid();
QString formatRichLink(const QString &url, bool file = false); QString formatRichLink(const QString &url, bool file = false);
+2 -2
View File
@@ -46,7 +46,7 @@ CommandPage::CommandPage()
view->setTitles({"Trigger", "Command"}); view->setTitles({"Trigger", "Command"});
view->getTableView()->horizontalHeader()->setStretchLastSection(true); view->getTableView()->horizontalHeader()->setStretchLastSection(true);
view->addButtonPressed.connect([] { view->addButtonPressed.connect([] {
getApp()->commands->items_.append( getApp()->commands->items.append(
Command{"/command", "I made a new command HeyGuys"}); Command{"/command", "I made a new command HeyGuys"});
}); });
@@ -65,7 +65,7 @@ CommandPage::CommandPage()
{ {
if (int index = line.indexOf(' '); index != -1) if (int index = line.indexOf(' '); index != -1)
{ {
getApp()->commands->items_.insert( getApp()->commands->items.insert(
Command(line.mid(0, index), line.mid(index + 1))); Command(line.mid(0, index), line.mid(index + 1)));
} }
} }