fix: Commands now completable when not starting with / (#4846)

* Add commands to completable inputs when no prefix
This commit is contained in:
Daniel Sage
2023-09-30 07:06:16 -04:00
committed by GitHub
parent bdd7d95092
commit 4d8b62364d
8 changed files with 123 additions and 84 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
- Dev: Refactor `Image` & Image's `Frames`. (#4773) - Dev: Refactor `Image` & Image's `Frames`. (#4773)
- Dev: Add `WindowManager::getLastSelectedWindow()` to replace `getMainWindow()`. (#4816) - Dev: Add `WindowManager::getLastSelectedWindow()` to replace `getMainWindow()`. (#4816)
- Dev: Clarify signal connection lifetimes where applicable. (#4818) - 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 ## 2.4.5
@@ -83,11 +83,12 @@ std::optional<TabCompletionModel::SourceKind>
if (getSettings()->userCompletionOnlyWithAt) if (getSettings()->userCompletionOnlyWithAt)
{ {
return SourceKind::Emote; // All kinds but user are possible
return SourceKind::EmoteCommand;
} }
// Either is possible, use unified source // Any kind is possible
return SourceKind::EmoteAndUser; return SourceKind::EmoteUserCommand;
} }
std::unique_ptr<completion::Source> TabCompletionModel::buildSource( std::unique_ptr<completion::Source> TabCompletionModel::buildSource(
@@ -95,25 +96,55 @@ std::unique_ptr<completion::Source> TabCompletionModel::buildSource(
{ {
switch (kind) switch (kind)
{ {
case SourceKind::Emote: case SourceKind::Emote: {
return std::make_unique<completion::EmoteSource>( return this->buildEmoteSource();
&this->channel_, }
std::make_unique<completion::ClassicTabEmoteStrategy>()); case SourceKind::User: {
case SourceKind::User: return this->buildUserSource();
return std::make_unique<completion::UserSource>( }
&this->channel_, case SourceKind::Command: {
std::make_unique<completion::ClassicUserStrategy>()); return this->buildCommandSource();
case SourceKind::Command: }
return std::make_unique<completion::CommandSource>( case SourceKind::EmoteCommand: {
std::make_unique<completion::CommandStrategy>(true)); std::vector<std::unique_ptr<completion::Source>> sources;
case SourceKind::EmoteAndUser: sources.push_back(this->buildEmoteSource());
sources.push_back(this->buildCommandSource());
return std::make_unique<completion::UnifiedSource>( return std::make_unique<completion::UnifiedSource>(
&this->channel_, std::move(sources));
std::make_unique<completion::ClassicTabEmoteStrategy>(), }
std::make_unique<completion::ClassicUserStrategy>()); case SourceKind::EmoteUserCommand: {
std::vector<std::unique_ptr<completion::Source>> sources;
sources.push_back(this->buildEmoteSource());
sources.push_back(this->buildUserSource());
sources.push_back(this->buildCommandSource());
return std::make_unique<completion::UnifiedSource>(
std::move(sources));
}
default: default:
return nullptr; return nullptr;
} }
} }
std::unique_ptr<completion::Source> TabCompletionModel::buildEmoteSource() const
{
return std::make_unique<completion::EmoteSource>(
&this->channel_,
std::make_unique<completion::ClassicTabEmoteStrategy>());
}
std::unique_ptr<completion::Source> TabCompletionModel::buildUserSource() const
{
return std::make_unique<completion::UserSource>(
&this->channel_, std::make_unique<completion::ClassicUserStrategy>());
}
std::unique_ptr<completion::Source> TabCompletionModel::buildCommandSource()
const
{
return std::make_unique<completion::CommandSource>(
std::make_unique<completion::CommandStrategy>(true));
}
} // namespace chatterino } // namespace chatterino
@@ -30,7 +30,13 @@ public:
void updateResults(const QString &query, bool isFirstWord = false); void updateResults(const QString &query, bool isFirstWord = false);
private: 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. /// @brief Updates the internal completion source based on the current query.
/// The completion source will only change if the deduced completion kind /// The completion source will only change if the deduced completion kind
@@ -47,6 +53,10 @@ private:
std::unique_ptr<completion::Source> buildSource(SourceKind kind) const; std::unique_ptr<completion::Source> buildSource(SourceKind kind) const;
std::unique_ptr<completion::Source> buildEmoteSource() const;
std::unique_ptr<completion::Source> buildUserSource() const;
std::unique_ptr<completion::Source> buildCommandSource() const;
Channel &channel_; Channel &channel_;
std::unique_ptr<completion::Source> source_{}; std::unique_ptr<completion::Source> source_{};
std::optional<SourceKind> sourceKind_{}; std::optional<SourceKind> sourceKind_{};
@@ -14,11 +14,17 @@ namespace {
{ {
if (command.startsWith('/') || command.startsWith('.')) 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 else
{ {
out.push_back({command, '/'}); out.push_back({
.name = command,
.prefix = "",
});
} }
} }
@@ -13,7 +13,7 @@ namespace chatterino::completion {
struct CommandItem { struct CommandItem {
QString name{}; QString name{};
QChar prefix{}; QString prefix{};
}; };
class CommandSource : public Source class CommandSource : public Source
@@ -2,21 +2,18 @@
namespace chatterino::completion { namespace chatterino::completion {
UnifiedSource::UnifiedSource( UnifiedSource::UnifiedSource(std::vector<std::unique_ptr<Source>> sources)
const Channel *channel, : sources_(std::move(sources))
std::unique_ptr<EmoteSource::EmoteStrategy> emoteStrategy,
std::unique_ptr<UserSource::UserStrategy> userStrategy,
ActionCallback callback)
: emoteSource_(channel, std::move(emoteStrategy), callback)
, usersSource_(channel, std::move(userStrategy), callback,
false) // disable adding @ to front
{ {
} }
void UnifiedSource::update(const QString &query) void UnifiedSource::update(const QString &query)
{ {
this->emoteSource_.update(query); // Update all sources
this->usersSource_.update(query); for (const auto &source : this->sources_)
{
source->update(query);
}
} }
void UnifiedSource::addToListModel(GenericListModel &model, void UnifiedSource::addToListModel(GenericListModel &model,
@@ -24,28 +21,28 @@ void UnifiedSource::addToListModel(GenericListModel &model,
{ {
if (maxCount == 0) if (maxCount == 0)
{ {
this->emoteSource_.addToListModel(model, 0); for (const auto &source : this->sources_)
this->usersSource_.addToListModel(model, 0); {
source->addToListModel(model, 0);
}
return; return;
} }
// Otherwise, make sure to only add maxCount elements in total. We prioritize // Make sure to only add maxCount elements in total.
// accepting results from the emote source before the users source (arbitrarily).
int startingSize = model.rowCount(); int startingSize = model.rowCount();
int used = 0;
// Add up to maxCount elements for (const auto &source : this->sources_)
this->emoteSource_.addToListModel(model, maxCount);
int used = model.rowCount() - startingSize;
if (used >= maxCount)
{ {
// Used up our limit on emotes source->addToListModel(model, maxCount - used);
return; // 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, void UnifiedSource::addToStringList(QStringList &list, size_t maxCount,
@@ -53,28 +50,28 @@ void UnifiedSource::addToStringList(QStringList &list, size_t maxCount,
{ {
if (maxCount == 0) if (maxCount == 0)
{ {
this->emoteSource_.addToStringList(list, 0, isFirstWord); for (const auto &source : this->sources_)
this->usersSource_.addToStringList(list, 0, isFirstWord); {
source->addToStringList(list, 0, isFirstWord);
}
return; return;
} }
// Otherwise, make sure to only add maxCount elements in total. We prioritize // Make sure to only add maxCount elements in total.
// accepting results from the emote source before the users source (arbitrarily).
int startingSize = list.size(); int startingSize = list.size();
int used = 0;
// Add up to maxCount elements for (const auto &source : this->sources_)
this->emoteSource_.addToStringList(list, maxCount, isFirstWord);
int used = list.size() - startingSize;
if (used >= maxCount)
{ {
// Used up our limit on emotes source->addToStringList(list, maxCount - used, isFirstWord);
return; // 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 } // namespace chatterino::completion
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "controllers/completion/sources/CommandSource.hpp"
#include "controllers/completion/sources/EmoteSource.hpp" #include "controllers/completion/sources/EmoteSource.hpp"
#include "controllers/completion/sources/Source.hpp" #include "controllers/completion/sources/Source.hpp"
#include "controllers/completion/sources/UserSource.hpp" #include "controllers/completion/sources/UserSource.hpp"
@@ -13,20 +14,9 @@ namespace chatterino::completion {
class UnifiedSource : public Source class UnifiedSource : public Source
{ {
public: public:
using ActionCallback = std::function<void(const QString &)>; /// @brief Initializes a unified completion source.
/// @param sources Vector of sources to unify
/// @brief Initializes a unified completion source for the given channel. UnifiedSource(std::vector<std::unique_ptr<Source>> sources);
/// 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<EmoteSource::EmoteStrategy> emoteStrategy,
std::unique_ptr<UserSource::UserStrategy> userStrategy,
ActionCallback callback = nullptr);
void update(const QString &query) override; void update(const QString &query) override;
void addToListModel(GenericListModel &model, void addToListModel(GenericListModel &model,
@@ -35,8 +25,7 @@ public:
bool isFirstWord = false) const override; bool isFirstWord = false) const override;
private: private:
EmoteSource emoteSource_; std::vector<std::unique_ptr<Source>> sources_;
UserSource usersSource_;
}; };
} // namespace chatterino::completion } // namespace chatterino::completion
@@ -2,6 +2,16 @@
namespace chatterino::completion { namespace chatterino::completion {
QString normalizeQuery(const QString &query)
{
if (query.startsWith('/') || query.startsWith('.'))
{
return query.mid(1);
}
return query;
}
CommandStrategy::CommandStrategy(bool startsWithOnly) CommandStrategy::CommandStrategy(bool startsWithOnly)
: startsWithOnly_(startsWithOnly) : startsWithOnly_(startsWithOnly)
{ {
@@ -11,11 +21,7 @@ void CommandStrategy::apply(const std::vector<CommandItem> &items,
std::vector<CommandItem> &output, std::vector<CommandItem> &output,
const QString &query) const const QString &query) const
{ {
QString normalizedQuery = query; QString normalizedQuery = normalizeQuery(query);
if (normalizedQuery.startsWith('/') || normalizedQuery.startsWith('.'))
{
normalizedQuery = normalizedQuery.mid(1);
}
if (startsWithOnly_) if (startsWithOnly_)
{ {