Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Tab completion for Twitch commands #3144

Merged
merged 12 commits into from
Dec 26, 2021
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 47 additions & 20 deletions src/common/CompletionModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -85,21 +86,40 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
// Twitch channel
auto tc = dynamic_cast<TwitchChannel *>(&this->channel_);

std::function<void(const QString &str, TaggedString::Type type)> 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<QChar> 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())
{
Expand Down Expand Up @@ -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::CustomCommand);
}

// Default Chatterino commands
for (auto &command : getApp()->commands->getDefaultChatterinoCommandList())
{
addString(command.name, TaggedString::Command);
addString(command, TaggedString::ChatterinoCommand);
}

for (auto &command : getApp()->commands->getDefaultTwitchCommandList())
// Default Twitch commands
for (auto &command : TWITCH_DEFAULT_COMMANDS)
{
addString(command, TaggedString::Command);
addString(command, TaggedString::TwitchCommand);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/common/CompletionModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ class CompletionModel : public QAbstractListModel
EmoteEnd,
// end emotes

Command,
CustomCommand,
ChatterinoCommand,
TwitchCommand,
};

TaggedString(const QString &string, Type type);
Expand Down
65 changes: 14 additions & 51 deletions src/controllers/commands/CommandController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
Expand All @@ -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");
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1114,9 +1077,9 @@ QString CommandController::execCustomCommand(const QStringList &words,
}
}

QStringList CommandController::getDefaultTwitchCommandList()
QStringList CommandController::getDefaultChatterinoCommandList()
{
return this->commandAutoCompletions_;
return this->defaultChatterinoCommandAutoCompletions_;
}

} // namespace chatterino
6 changes: 3 additions & 3 deletions src/controllers/commands/CommandController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ class CommandModel;
class CommandController final : public Singleton
{
public:
SignalVector<Command> items_;
SignalVector<Command> items;

QString execCommand(const QString &text, std::shared_ptr<Channel> channel,
bool dryRun);
QStringList getDefaultTwitchCommandList();
QStringList getDefaultChatterinoCommandList();

virtual void initialize(Settings &, Paths &paths) override;
virtual void save() override;
Expand Down Expand Up @@ -61,7 +61,7 @@ class CommandController final : public Singleton
std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>>
commandsSetting_;

QStringList commandAutoCompletions_;
QStringList defaultChatterinoCommandAutoCompletions_;
};

} // namespace chatterino
37 changes: 37 additions & 0 deletions src/providers/twitch/TwitchCommon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,41 @@ static const std::vector<QColor> 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
11 changes: 11 additions & 0 deletions src/util/Helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions src/util/Helpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/settingspages/CommandPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"});
});

Expand All @@ -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)));
}
}
Expand Down