Consolidate input completion code in preparation for advanced completion strategies (#4639)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -26,7 +26,7 @@ namespace chatterino {
|
||||
// Channel
|
||||
//
|
||||
Channel::Channel(const QString &name, Type type)
|
||||
: completionModel(*this)
|
||||
: completionModel(*this, nullptr)
|
||||
, lastDate_(QDate::currentDate())
|
||||
, name_(name)
|
||||
, messages_(getSettings()->scrollbackSplitLimit)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/CompletionModel.hpp"
|
||||
#include "common/FlagsEnum.hpp"
|
||||
#include "controllers/completion/TabCompletionModel.hpp"
|
||||
#include "messages/LimitedQueue.hpp"
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
@@ -108,7 +108,7 @@ public:
|
||||
|
||||
static std::shared_ptr<Channel> getEmpty();
|
||||
|
||||
CompletionModel completionModel;
|
||||
TabCompletionModel completionModel;
|
||||
QDate lastDate_;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -56,4 +56,9 @@ std::vector<QString> ChatterSet::filterByPrefix(const QString &prefix) const
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::pair<QString, QString>> ChatterSet::all() const
|
||||
{
|
||||
return {this->items.begin(), this->items.end()};
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -39,6 +39,10 @@ public:
|
||||
/// are in mixed case if available.
|
||||
std::vector<QString> filterByPrefix(const QString &prefix) const;
|
||||
|
||||
/// Get all recent chatters. The first pair element contains the username
|
||||
/// in lowercase, while the second pair element is the original case.
|
||||
std::vector<std::pair<QString, QString>> all() const;
|
||||
|
||||
private:
|
||||
// user name in lower case -> user name in normal case
|
||||
cache::lru_cache<QString, QString> items;
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
#include "common/CompletionModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/ChatterSet.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/Command.hpp"
|
||||
#include "controllers/commands/CommandController.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <QtAlgorithms>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
//
|
||||
// TaggedString
|
||||
//
|
||||
|
||||
CompletionModel::TaggedString::TaggedString(QString _string, Type _type)
|
||||
: string(std::move(_string))
|
||||
, type(_type)
|
||||
{
|
||||
}
|
||||
|
||||
bool CompletionModel::TaggedString::isEmote() const
|
||||
{
|
||||
return this->type > Type::EmoteStart && this->type < Type::EmoteEnd;
|
||||
}
|
||||
|
||||
bool CompletionModel::TaggedString::operator<(const TaggedString &that) const
|
||||
{
|
||||
if (this->isEmote() != that.isEmote())
|
||||
{
|
||||
return this->isEmote();
|
||||
}
|
||||
|
||||
return CompletionModel::compareStrings(this->string, that.string);
|
||||
}
|
||||
|
||||
//
|
||||
// CompletionModel
|
||||
//
|
||||
CompletionModel::CompletionModel(Channel &channel)
|
||||
: channel_(channel)
|
||||
{
|
||||
}
|
||||
|
||||
int CompletionModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
(void)parent; // unused
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
(void)role; // unused
|
||||
|
||||
std::shared_lock lock(this->itemsMutex_);
|
||||
|
||||
auto it = this->items_.begin();
|
||||
std::advance(it, index.row());
|
||||
return {it->string};
|
||||
}
|
||||
|
||||
int CompletionModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
(void)parent; // unused
|
||||
|
||||
std::shared_lock lock(this->itemsMutex_);
|
||||
|
||||
return this->items_.size();
|
||||
}
|
||||
|
||||
void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
|
||||
{
|
||||
std::unique_lock lock(this->itemsMutex_);
|
||||
|
||||
this->items_.clear();
|
||||
|
||||
if (prefix.length() < 2 || !this->channel_.isTwitchChannel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto *app = getIApp();
|
||||
// Twitch channel
|
||||
auto *tc = dynamic_cast<TwitchChannel *>(&this->channel_);
|
||||
|
||||
auto addString = [=, this](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 = app->getAccounts()->twitch.getCurrent())
|
||||
{
|
||||
// Twitch Emotes available globally
|
||||
for (const auto &emote : account->accessEmotes()->emotes)
|
||||
{
|
||||
addString(emote.first.string, TaggedString::TwitchGlobalEmote);
|
||||
}
|
||||
|
||||
// Twitch Emotes available locally
|
||||
auto localEmoteData = account->accessLocalEmotes();
|
||||
if (tc != nullptr &&
|
||||
localEmoteData->find(tc->roomId()) != localEmoteData->end())
|
||||
{
|
||||
for (const auto &emote : localEmoteData->at(tc->roomId()))
|
||||
{
|
||||
addString(emote.first.string,
|
||||
TaggedString::Type::TwitchLocalEmote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7TV Global
|
||||
for (const auto &emote :
|
||||
*app->getTwitch()->getSeventvEmotes().globalEmotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::SeventvGlobalEmote);
|
||||
}
|
||||
// Bttv Global
|
||||
for (const auto &emote : *app->getTwitch()->getBttvEmotes().emotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::BTTVChannelEmote);
|
||||
}
|
||||
|
||||
// Ffz Global
|
||||
for (const auto &emote : *app->getTwitch()->getFfzEmotes().emotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::FFZChannelEmote);
|
||||
}
|
||||
|
||||
// Emojis
|
||||
if (prefix.startsWith(":"))
|
||||
{
|
||||
const auto &emojiShortCodes =
|
||||
app->getEmotes()->getEmojis()->getShortCodes();
|
||||
for (const auto &m : emojiShortCodes)
|
||||
{
|
||||
addString(QString(":%1:").arg(m), TaggedString::Type::Emoji);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Stuff below is available only in regular Twitch channels
|
||||
if (tc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Usernames
|
||||
if (prefix.startsWith("@"))
|
||||
{
|
||||
QString usernamePrefix = prefix;
|
||||
usernamePrefix.remove(0, 1);
|
||||
|
||||
auto chatters = tc->accessChatters()->filterByPrefix(usernamePrefix);
|
||||
|
||||
for (const auto &name : chatters)
|
||||
{
|
||||
addString(
|
||||
"@" + formatUserMention(name, isFirstWord,
|
||||
getSettings()->mentionUsersWithComma),
|
||||
TaggedString::Type::Username);
|
||||
}
|
||||
}
|
||||
else if (!getSettings()->userCompletionOnlyWithAt)
|
||||
{
|
||||
auto chatters = tc->accessChatters()->filterByPrefix(prefix);
|
||||
|
||||
for (const auto &name : chatters)
|
||||
{
|
||||
addString(formatUserMention(name, isFirstWord,
|
||||
getSettings()->mentionUsersWithComma),
|
||||
TaggedString::Type::Username);
|
||||
}
|
||||
}
|
||||
|
||||
// 7TV Channel
|
||||
for (const auto &emote : *tc->seventvEmotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::SeventvChannelEmote);
|
||||
}
|
||||
// Bttv Channel
|
||||
for (const auto &emote : *tc->bttvEmotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
|
||||
}
|
||||
|
||||
// Ffz Channel
|
||||
for (const auto &emote : *tc->ffzEmotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
|
||||
}
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
for (const auto &command : app->getCommands()->pluginCommands())
|
||||
{
|
||||
addString(command, TaggedString::PluginCommand);
|
||||
}
|
||||
#endif
|
||||
// Custom Chatterino commands
|
||||
for (const auto &command : app->getCommands()->items)
|
||||
{
|
||||
addString(command.name, TaggedString::CustomCommand);
|
||||
}
|
||||
|
||||
// Default Chatterino commands
|
||||
for (const auto &command :
|
||||
app->getCommands()->getDefaultChatterinoCommandList())
|
||||
{
|
||||
addString(command, TaggedString::ChatterinoCommand);
|
||||
}
|
||||
|
||||
// Default Twitch commands
|
||||
for (const auto &command : TWITCH_DEFAULT_COMMANDS)
|
||||
{
|
||||
addString(command, TaggedString::TwitchCommand);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<QString> CompletionModel::allItems() const
|
||||
{
|
||||
std::shared_lock lock(this->itemsMutex_);
|
||||
|
||||
std::vector<QString> results;
|
||||
results.reserve(this->items_.size());
|
||||
for (const auto &item : this->items_)
|
||||
{
|
||||
results.push_back(item.string);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool CompletionModel::compareStrings(const QString &a, const QString &b)
|
||||
{
|
||||
// try comparing insensitively, if they are the same then senstively
|
||||
// (fixes order of LuL and LUL)
|
||||
int k = QString::compare(a, b, Qt::CaseInsensitive);
|
||||
if (k == 0)
|
||||
{
|
||||
return a > b;
|
||||
}
|
||||
|
||||
return k < 0;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
|
||||
class InputCompletionTest;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
|
||||
class CompletionModel : public QAbstractListModel
|
||||
{
|
||||
struct TaggedString {
|
||||
enum Type {
|
||||
Username,
|
||||
|
||||
// emotes
|
||||
EmoteStart,
|
||||
FFZGlobalEmote,
|
||||
FFZChannelEmote,
|
||||
BTTVGlobalEmote,
|
||||
BTTVChannelEmote,
|
||||
SeventvGlobalEmote,
|
||||
SeventvChannelEmote,
|
||||
TwitchGlobalEmote,
|
||||
TwitchLocalEmote,
|
||||
TwitchSubscriberEmote,
|
||||
Emoji,
|
||||
EmoteEnd,
|
||||
// end emotes
|
||||
|
||||
CustomCommand,
|
||||
ChatterinoCommand,
|
||||
TwitchCommand,
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginCommand,
|
||||
#endif
|
||||
};
|
||||
|
||||
TaggedString(QString _string, Type type);
|
||||
|
||||
bool isEmote() const;
|
||||
bool operator<(const TaggedString &that) const;
|
||||
|
||||
const QString string;
|
||||
const Type type;
|
||||
};
|
||||
|
||||
public:
|
||||
CompletionModel(Channel &channel);
|
||||
|
||||
int columnCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
void refresh(const QString &prefix, bool isFirstWord = false);
|
||||
|
||||
static bool compareStrings(const QString &a, const QString &b);
|
||||
|
||||
private:
|
||||
std::vector<QString> allItems() const;
|
||||
|
||||
mutable std::shared_mutex itemsMutex_;
|
||||
std::set<TaggedString> items_;
|
||||
|
||||
Channel &channel_;
|
||||
|
||||
friend class ::InputCompletionTest;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
Reference in New Issue
Block a user