diff --git a/CHANGELOG.md b/CHANGELOG.md index 33be03f1..4143892a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ - Dev: Refactor `Image` & Image's `Frames`. (#4773) - Dev: Add `WindowManager::getLastSelectedWindow()` to replace `getMainWindow()`. (#4816) - Dev: Clarify signal connection lifetimes where applicable. (#4818) -- Dev: Laid the groundwork for advanced input completion strategies. (#4639) +- Dev: Laid the groundwork for advanced input completion strategies. (#4639, #4846) ## 2.4.5 diff --git a/src/controllers/completion/TabCompletionModel.cpp b/src/controllers/completion/TabCompletionModel.cpp index b4f77b78..3d8afd36 100644 --- a/src/controllers/completion/TabCompletionModel.cpp +++ b/src/controllers/completion/TabCompletionModel.cpp @@ -83,11 +83,12 @@ std::optional if (getSettings()->userCompletionOnlyWithAt) { - return SourceKind::Emote; + // All kinds but user are possible + return SourceKind::EmoteCommand; } - // Either is possible, use unified source - return SourceKind::EmoteAndUser; + // Any kind is possible + return SourceKind::EmoteUserCommand; } std::unique_ptr TabCompletionModel::buildSource( @@ -95,25 +96,55 @@ std::unique_ptr TabCompletionModel::buildSource( { switch (kind) { - case SourceKind::Emote: - return std::make_unique( - &this->channel_, - std::make_unique()); - case SourceKind::User: - return std::make_unique( - &this->channel_, - std::make_unique()); - case SourceKind::Command: - return std::make_unique( - std::make_unique(true)); - case SourceKind::EmoteAndUser: + case SourceKind::Emote: { + return this->buildEmoteSource(); + } + case SourceKind::User: { + return this->buildUserSource(); + } + case SourceKind::Command: { + return this->buildCommandSource(); + } + case SourceKind::EmoteCommand: { + std::vector> sources; + sources.push_back(this->buildEmoteSource()); + sources.push_back(this->buildCommandSource()); + return std::make_unique( - &this->channel_, - std::make_unique(), - std::make_unique()); + std::move(sources)); + } + case SourceKind::EmoteUserCommand: { + std::vector> sources; + sources.push_back(this->buildEmoteSource()); + sources.push_back(this->buildUserSource()); + sources.push_back(this->buildCommandSource()); + + return std::make_unique( + std::move(sources)); + } default: return nullptr; } } +std::unique_ptr TabCompletionModel::buildEmoteSource() const +{ + return std::make_unique( + &this->channel_, + std::make_unique()); +} + +std::unique_ptr TabCompletionModel::buildUserSource() const +{ + return std::make_unique( + &this->channel_, std::make_unique()); +} + +std::unique_ptr TabCompletionModel::buildCommandSource() + const +{ + return std::make_unique( + std::make_unique(true)); +} + } // namespace chatterino diff --git a/src/controllers/completion/TabCompletionModel.hpp b/src/controllers/completion/TabCompletionModel.hpp index 20473b6f..84a0753d 100644 --- a/src/controllers/completion/TabCompletionModel.hpp +++ b/src/controllers/completion/TabCompletionModel.hpp @@ -30,7 +30,13 @@ public: void updateResults(const QString &query, bool isFirstWord = false); private: - enum class SourceKind { Emote, User, Command, EmoteAndUser }; + enum class SourceKind { + Emote, + User, + Command, + EmoteCommand, + EmoteUserCommand + }; /// @brief Updates the internal completion source based on the current query. /// The completion source will only change if the deduced completion kind @@ -47,6 +53,10 @@ private: std::unique_ptr buildSource(SourceKind kind) const; + std::unique_ptr buildEmoteSource() const; + std::unique_ptr buildUserSource() const; + std::unique_ptr buildCommandSource() const; + Channel &channel_; std::unique_ptr source_{}; std::optional sourceKind_{}; diff --git a/src/controllers/completion/sources/CommandSource.cpp b/src/controllers/completion/sources/CommandSource.cpp index b114969b..74bb3716 100644 --- a/src/controllers/completion/sources/CommandSource.cpp +++ b/src/controllers/completion/sources/CommandSource.cpp @@ -14,11 +14,17 @@ namespace { { if (command.startsWith('/') || command.startsWith('.')) { - out.push_back({command.mid(1), command.at(0)}); + out.push_back({ + .name = command.mid(1), + .prefix = command.at(0), + }); } else { - out.push_back({command, '/'}); + out.push_back({ + .name = command, + .prefix = "", + }); } } diff --git a/src/controllers/completion/sources/CommandSource.hpp b/src/controllers/completion/sources/CommandSource.hpp index 66c2a54c..7c9e1017 100644 --- a/src/controllers/completion/sources/CommandSource.hpp +++ b/src/controllers/completion/sources/CommandSource.hpp @@ -13,7 +13,7 @@ namespace chatterino::completion { struct CommandItem { QString name{}; - QChar prefix{}; + QString prefix{}; }; class CommandSource : public Source diff --git a/src/controllers/completion/sources/UnifiedSource.cpp b/src/controllers/completion/sources/UnifiedSource.cpp index e68c860b..a0f462ac 100644 --- a/src/controllers/completion/sources/UnifiedSource.cpp +++ b/src/controllers/completion/sources/UnifiedSource.cpp @@ -2,21 +2,18 @@ namespace chatterino::completion { -UnifiedSource::UnifiedSource( - const Channel *channel, - std::unique_ptr emoteStrategy, - std::unique_ptr userStrategy, - ActionCallback callback) - : emoteSource_(channel, std::move(emoteStrategy), callback) - , usersSource_(channel, std::move(userStrategy), callback, - false) // disable adding @ to front +UnifiedSource::UnifiedSource(std::vector> sources) + : sources_(std::move(sources)) { } void UnifiedSource::update(const QString &query) { - this->emoteSource_.update(query); - this->usersSource_.update(query); + // Update all sources + for (const auto &source : this->sources_) + { + source->update(query); + } } void UnifiedSource::addToListModel(GenericListModel &model, @@ -24,28 +21,28 @@ void UnifiedSource::addToListModel(GenericListModel &model, { if (maxCount == 0) { - this->emoteSource_.addToListModel(model, 0); - this->usersSource_.addToListModel(model, 0); + for (const auto &source : this->sources_) + { + source->addToListModel(model, 0); + } return; } - // Otherwise, make sure to only add maxCount elements in total. We prioritize - // accepting results from the emote source before the users source (arbitrarily). - + // Make sure to only add maxCount elements in total. int startingSize = model.rowCount(); + int used = 0; - // Add up to maxCount elements - this->emoteSource_.addToListModel(model, maxCount); - - int used = model.rowCount() - startingSize; - if (used >= maxCount) + for (const auto &source : this->sources_) { - // Used up our limit on emotes - return; + source->addToListModel(model, maxCount - used); + // Calculate how many items have been added so far + used = model.rowCount() - startingSize; + if (used >= maxCount) + { + // Used up all of limit + break; + } } - - // Only add maxCount - used to ensure the total added doesn't exceed maxCount - this->usersSource_.addToListModel(model, maxCount - used); } void UnifiedSource::addToStringList(QStringList &list, size_t maxCount, @@ -53,28 +50,28 @@ void UnifiedSource::addToStringList(QStringList &list, size_t maxCount, { if (maxCount == 0) { - this->emoteSource_.addToStringList(list, 0, isFirstWord); - this->usersSource_.addToStringList(list, 0, isFirstWord); + for (const auto &source : this->sources_) + { + source->addToStringList(list, 0, isFirstWord); + } return; } - // Otherwise, make sure to only add maxCount elements in total. We prioritize - // accepting results from the emote source before the users source (arbitrarily). - + // Make sure to only add maxCount elements in total. int startingSize = list.size(); + int used = 0; - // Add up to maxCount elements - this->emoteSource_.addToStringList(list, maxCount, isFirstWord); - - int used = list.size() - startingSize; - if (used >= maxCount) + for (const auto &source : this->sources_) { - // Used up our limit on emotes - return; + source->addToStringList(list, maxCount - used, isFirstWord); + // Calculate how many items have been added so far + used = list.size() - startingSize; + if (used >= maxCount) + { + // Used up all of limit + break; + } } - - // Only add maxCount - used to ensure the total added doesn't exceed maxCount - this->usersSource_.addToStringList(list, maxCount - used, isFirstWord); } } // namespace chatterino::completion diff --git a/src/controllers/completion/sources/UnifiedSource.hpp b/src/controllers/completion/sources/UnifiedSource.hpp index 15a0f12f..416a7436 100644 --- a/src/controllers/completion/sources/UnifiedSource.hpp +++ b/src/controllers/completion/sources/UnifiedSource.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/Channel.hpp" +#include "controllers/completion/sources/CommandSource.hpp" #include "controllers/completion/sources/EmoteSource.hpp" #include "controllers/completion/sources/Source.hpp" #include "controllers/completion/sources/UserSource.hpp" @@ -13,20 +14,9 @@ namespace chatterino::completion { class UnifiedSource : public Source { public: - using ActionCallback = std::function; - - /// @brief Initializes a unified completion source for the given channel. - /// Resolves both emotes and usernames for autocompletion. - /// @param channel Channel to initialize emotes and users from. Must be a - /// TwitchChannel or completion is a no-op. - /// @param emoteStrategy Strategy for selecting emotes - /// @param userStrategy Strategy for selecting users - /// @param callback ActionCallback to invoke upon InputCompletionItem selection. - /// See InputCompletionItem::action(). Can be nullptr. - UnifiedSource(const Channel *channel, - std::unique_ptr emoteStrategy, - std::unique_ptr userStrategy, - ActionCallback callback = nullptr); + /// @brief Initializes a unified completion source. + /// @param sources Vector of sources to unify + UnifiedSource(std::vector> sources); void update(const QString &query) override; void addToListModel(GenericListModel &model, @@ -35,8 +25,7 @@ public: bool isFirstWord = false) const override; private: - EmoteSource emoteSource_; - UserSource usersSource_; + std::vector> sources_; }; } // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/CommandStrategy.cpp b/src/controllers/completion/strategies/CommandStrategy.cpp index f5aad454..9edf4e40 100644 --- a/src/controllers/completion/strategies/CommandStrategy.cpp +++ b/src/controllers/completion/strategies/CommandStrategy.cpp @@ -2,6 +2,16 @@ namespace chatterino::completion { +QString normalizeQuery(const QString &query) +{ + if (query.startsWith('/') || query.startsWith('.')) + { + return query.mid(1); + } + + return query; +} + CommandStrategy::CommandStrategy(bool startsWithOnly) : startsWithOnly_(startsWithOnly) { @@ -11,11 +21,7 @@ void CommandStrategy::apply(const std::vector &items, std::vector &output, const QString &query) const { - QString normalizedQuery = query; - if (normalizedQuery.startsWith('/') || normalizedQuery.startsWith('.')) - { - normalizedQuery = normalizedQuery.mid(1); - } + QString normalizedQuery = normalizeQuery(query); if (startsWithOnly_) {