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

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

Merged
merged 3 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 49 additions & 18 deletions src/controllers/completion/TabCompletionModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,68 @@ std::optional<TabCompletionModel::SourceKind>

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<completion::Source> TabCompletionModel::buildSource(
SourceKind kind) const
{
switch (kind)
{
case SourceKind::Emote:
return std::make_unique<completion::EmoteSource>(
&this->channel_,
std::make_unique<completion::ClassicTabEmoteStrategy>());
case SourceKind::User:
return std::make_unique<completion::UserSource>(
&this->channel_,
std::make_unique<completion::ClassicUserStrategy>());
case SourceKind::Command:
return std::make_unique<completion::CommandSource>(
std::make_unique<completion::CommandStrategy>(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<std::unique_ptr<completion::Source>> sources;
sources.push_back(this->buildEmoteSource());
sources.push_back(this->buildCommandSource());

return std::make_unique<completion::UnifiedSource>(
std::move(sources));
}
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>(
&this->channel_,
std::make_unique<completion::ClassicTabEmoteStrategy>(),
std::make_unique<completion::ClassicUserStrategy>());
std::move(sources));
}
default:
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()
dnsge marked this conversation as resolved.
Show resolved Hide resolved
const
{
return std::make_unique<completion::CommandSource>(
std::make_unique<completion::CommandStrategy>(true));
}

} // namespace chatterino
12 changes: 11 additions & 1 deletion src/controllers/completion/TabCompletionModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ class TabCompletionModel : public QStringListModel
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
Expand All @@ -47,6 +53,10 @@ class TabCompletionModel : public QStringListModel

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_;
std::unique_ptr<completion::Source> source_{};
std::optional<SourceKind> sourceKind_{};
Expand Down
10 changes: 8 additions & 2 deletions src/controllers/completion/sources/CommandSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/controllers/completion/sources/CommandSource.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace chatterino::completion {

struct CommandItem {
QString name{};
QChar prefix{};
QString prefix{};
};

class CommandSource : public Source
Expand Down
77 changes: 37 additions & 40 deletions src/controllers/completion/sources/UnifiedSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,76 @@

namespace chatterino::completion {

UnifiedSource::UnifiedSource(
const Channel *channel,
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
UnifiedSource::UnifiedSource(std::vector<std::unique_ptr<Source>> 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,
size_t maxCount) const
{
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,
bool isFirstWord) const
{
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
21 changes: 5 additions & 16 deletions src/controllers/completion/sources/UnifiedSource.hpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -13,20 +14,9 @@ namespace chatterino::completion {
class UnifiedSource : public Source
{
public:
using ActionCallback = std::function<void(const QString &)>;

/// @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<EmoteSource::EmoteStrategy> emoteStrategy,
std::unique_ptr<UserSource::UserStrategy> userStrategy,
ActionCallback callback = nullptr);
/// @brief Initializes a unified completion source.
/// @param sources Vector of sources to unify
UnifiedSource(std::vector<std::unique_ptr<Source>> sources);

void update(const QString &query) override;
void addToListModel(GenericListModel &model,
Expand All @@ -35,8 +25,7 @@ class UnifiedSource : public Source
bool isFirstWord = false) const override;

private:
EmoteSource emoteSource_;
UserSource usersSource_;
std::vector<std::unique_ptr<Source>> sources_;
};

} // namespace chatterino::completion
16 changes: 11 additions & 5 deletions src/controllers/completion/strategies/CommandStrategy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -11,11 +21,7 @@ void CommandStrategy::apply(const std::vector<CommandItem> &items,
std::vector<CommandItem> &output,
const QString &query) const
{
QString normalizedQuery = query;
if (normalizedQuery.startsWith('/') || normalizedQuery.startsWith('.'))
{
normalizedQuery = normalizedQuery.mid(1);
}
QString normalizedQuery = normalizeQuery(query);

if (startsWithOnly_)
{
Expand Down