Skip to content

Commit

Permalink
Add a new completion API for experimental plugins feature. (#5000)
Browse files Browse the repository at this point in the history
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
  • Loading branch information
Mm2PL and pajlada committed Dec 10, 2023
1 parent e425816 commit fd4cac2
Show file tree
Hide file tree
Showing 13 changed files with 448 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Minor: Add an option to use new experimental smarter emote completion. (#4987)
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
- Minor: Add a new completion API for experimental plugins feature. (#5000)
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
Expand Down
21 changes: 21 additions & 0 deletions docs/chatterino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,25 @@ declare module c2 {
): boolean;
function send_msg(channel: String, text: String): boolean;
function system_msg(channel: String, text: String): boolean;

class CompletionList {
values: String[];
hide_others: boolean;
}

enum EventType {
RegisterCompletions = "RegisterCompletions",
}

type CbFuncCompletionsRequested = (
query: string,
full_text_content: string,
cursor_position: number,
is_first_word: boolean
) => CompletionList;
type CbFunc<T> = T extends EventType.RegisterCompletions
? CbFuncCompletionsRequested
: never;

function register_callback<T>(type: T, func: CbFunc<T>): void;
}
19 changes: 18 additions & 1 deletion src/controllers/completion/TabCompletionModel.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "controllers/completion/TabCompletionModel.hpp"

#include "Application.hpp"
#include "common/Channel.hpp"
#include "controllers/completion/sources/CommandSource.hpp"
#include "controllers/completion/sources/EmoteSource.hpp"
Expand All @@ -9,6 +10,9 @@
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
#include "controllers/completion/strategies/CommandStrategy.hpp"
#include "controllers/completion/strategies/SmartEmoteStrategy.hpp"
#include "controllers/plugins/LuaUtilities.hpp"
#include "controllers/plugins/Plugin.hpp"
#include "controllers/plugins/PluginController.hpp"
#include "singletons/Settings.hpp"

namespace chatterino {
Expand All @@ -19,7 +23,9 @@ TabCompletionModel::TabCompletionModel(Channel &channel, QObject *parent)
{
}

void TabCompletionModel::updateResults(const QString &query, bool isFirstWord)
void TabCompletionModel::updateResults(const QString &query,
const QString &fullTextContent,
int cursorPosition, bool isFirstWord)
{
this->updateSourceFromQuery(query);

Expand All @@ -29,6 +35,17 @@ void TabCompletionModel::updateResults(const QString &query, bool isFirstWord)

// Copy results to this model
QStringList results;
#ifdef CHATTERINO_HAVE_PLUGINS
// Try plugins first
bool done{};
std::tie(done, results) = getApp()->plugins->updateCustomCompletions(
query, fullTextContent, cursorPosition, isFirstWord);
if (done)
{
this->setStringList(results);
return;
}
#endif
this->source_->addToStringList(results, 0, isFirstWord);
this->setStringList(results);
}
Expand Down
6 changes: 5 additions & 1 deletion src/controllers/completion/TabCompletionModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ class TabCompletionModel : public QStringListModel

/// @brief Updates the model based on the completion query
/// @param query Completion query
/// @param fullTextContent Full text of the input, used by plugins for contextual completion
/// @param cursorPosition Number of characters behind the cursor from the
/// beginning of fullTextContent, also used by plugins
/// @param isFirstWord Whether the completion is the first word in the input
void updateResults(const QString &query, bool isFirstWord = false);
void updateResults(const QString &query, const QString &fullTextContent,
int cursorPosition, bool isFirstWord = false);

private:
enum class SourceKind {
Expand Down
32 changes: 32 additions & 0 deletions src/controllers/plugins/LuaAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@ int c2_register_command(lua_State *L)
return 1;
}

int c2_register_callback(lua_State *L)
{
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
if (pl == nullptr)
{
luaL_error(L, "internal error: no plugin");
return 0;
}
EventType evtType{};
if (!lua::peek(L, &evtType, 1))
{
luaL_error(L, "cannot get event name (1st arg of register_callback, "
"expected a string)");
return 0;
}
if (lua_isnoneornil(L, 2))
{
luaL_error(L, "missing argument for register_callback: function "
"\"pointer\"");
return 0;
}

auto callbackSavedName = QString("c2cb-%1").arg(
magic_enum::enum_name<EventType>(evtType).data());
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());

lua_pop(L, 2);

return 0;
}

int c2_send_msg(lua_State *L)
{
QString text;
Expand Down Expand Up @@ -167,6 +198,7 @@ int c2_system_msg(lua_State *L)
lua::push(L, false);
return 1;
}

const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
if (chn->isEmpty())
{
Expand Down
24 changes: 23 additions & 1 deletion src/controllers/plugins/LuaAPI.hpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#pragma once

#ifdef CHATTERINO_HAVE_PLUGINS
# include <QString>

# include <vector>

struct lua_State;
namespace chatterino::lua::api {
// names in this namespace reflect what's visible inside Lua and follow the lua naming scheme
// function names in this namespace reflect what's visible inside Lua and follow the lua naming scheme

// NOLINTBEGIN(readability-identifier-naming)
// Following functions are exposed in c2 table.
int c2_register_command(lua_State *L);
int c2_register_callback(lua_State *L);
int c2_send_msg(lua_State *L);
int c2_system_msg(lua_State *L);
int c2_log(lua_State *L);
Expand All @@ -23,6 +27,24 @@ int g_import(lua_State *L);
// Represents "calls" to qCDebug, qCInfo ...
enum class LogLevel { Debug, Info, Warning, Critical };

// Exposed as c2.EventType
// Represents callbacks c2 can do into lua world
enum class EventType {
CompletionRequested,
};

/**
* This is for custom completion, a registered function returns this type
* however in Lua array part (value) and object part (hideOthers) are in the same
* table.
*/
struct CompletionList {
std::vector<QString> values{};

// exposed as hide_others
bool hideOthers{};
};

} // namespace chatterino::lua::api

#endif
44 changes: 44 additions & 0 deletions src/controllers/plugins/LuaUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# include "common/Channel.hpp"
# include "common/QLogging.hpp"
# include "controllers/commands/CommandContext.hpp"
# include "controllers/plugins/LuaAPI.hpp"

# include <lauxlib.h>
# include <lua.h>
Expand Down Expand Up @@ -75,6 +76,9 @@ QString humanErrorText(lua_State *L, int errCode)
case LUA_ERRFILE:
errName = "(file error)";
break;
case ERROR_BAD_PEEK:
errName = "(unable to convert value to c++)";
break;
default:
errName = "(unknown error type)";
}
Expand Down Expand Up @@ -111,6 +115,7 @@ StackIdx push(lua_State *L, const std::string &str)

StackIdx push(lua_State *L, const CommandContext &ctx)
{
StackGuard guard(L, 1);
auto outIdx = pushEmptyTable(L, 2);

push(L, ctx.words);
Expand All @@ -127,8 +132,27 @@ StackIdx push(lua_State *L, const bool &b)
return lua_gettop(L);
}

StackIdx push(lua_State *L, const int &b)
{
lua_pushinteger(L, b);
return lua_gettop(L);
}

bool peek(lua_State *L, bool *out, StackIdx idx)
{
StackGuard guard(L);
if (!lua_isboolean(L, idx))
{
return false;
}

*out = bool(lua_toboolean(L, idx));
return true;
}

bool peek(lua_State *L, double *out, StackIdx idx)
{
StackGuard guard(L);
int ok{0};
auto v = lua_tonumberx(L, idx, &ok);
if (ok != 0)
Expand All @@ -140,6 +164,7 @@ bool peek(lua_State *L, double *out, StackIdx idx)

bool peek(lua_State *L, QString *out, StackIdx idx)
{
StackGuard guard(L);
size_t len{0};
const char *str = lua_tolstring(L, idx, &len);
if (str == nullptr)
Expand All @@ -156,6 +181,7 @@ bool peek(lua_State *L, QString *out, StackIdx idx)

bool peek(lua_State *L, QByteArray *out, StackIdx idx)
{
StackGuard guard(L);
size_t len{0};
const char *str = lua_tolstring(L, idx, &len);
if (str == nullptr)
Expand All @@ -172,6 +198,7 @@ bool peek(lua_State *L, QByteArray *out, StackIdx idx)

bool peek(lua_State *L, std::string *out, StackIdx idx)
{
StackGuard guard(L);
size_t len{0};
const char *str = lua_tolstring(L, idx, &len);
if (str == nullptr)
Expand All @@ -186,6 +213,23 @@ bool peek(lua_State *L, std::string *out, StackIdx idx)
return true;
}

bool peek(lua_State *L, api::CompletionList *out, StackIdx idx)
{
StackGuard guard(L);
int typ = lua_getfield(L, idx, "values");
if (typ != LUA_TTABLE)
{
lua_pop(L, 1);
return false;
}
if (!lua::pop(L, &out->values, -1))
{
return false;
}
lua_getfield(L, idx, "hide_others");
return lua::pop(L, &out->hideOthers);
}

QString toString(lua_State *L, StackIdx idx)
{
size_t len{};
Expand Down
Loading

0 comments on commit fd4cac2

Please sign in to comment.