Move events to sol

including tab completion
This commit is contained in:
Mm2PL 2024-10-06 16:06:46 +02:00
parent 1008904fb1
commit 48a3adc8cf
No known key found for this signature in database
GPG key ID: 94AC9B80EFA15ED9
7 changed files with 62 additions and 94 deletions

View file

@ -3,19 +3,22 @@
# include "Application.hpp"
# include "common/QLogging.hpp"
# include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginController.hpp"
# include "messages/MessageBuilder.hpp"
# include "providers/twitch/TwitchIrcServer.hpp"
# include "controllers/plugins/SolTypes.hpp" // for lua operations on QString{,List} for CompletionList
# include <lauxlib.h>
# include <lua.h>
# include <lualib.h>
# include <QFileInfo>
# include <QList>
# include <QLoggingCategory>
# include <QTextCodec>
# include <QUrl>
# include <sol/forward.hpp>
# include <sol/state_view.hpp>
# include <utility>
namespace {
using namespace chatterino;
@ -61,38 +64,26 @@ QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
namespace chatterino::lua::api {
int c2_register_callback(lua_State *L)
CompletionList::CompletionList(const sol::table &table)
: values(table.get<QStringList>("values"))
, hideOthers(table["hide_others"])
{
auto *pl = getApp()->getPlugins()->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 typeName = magic_enum::enum_name(evtType);
std::string callbackSavedName;
callbackSavedName.reserve(5 + typeName.size());
callbackSavedName += "c2cb-";
callbackSavedName += typeName;
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str());
sol::table toTable(lua_State *L, const CompletionEvent &ev)
{
return sol::state_view(L).create_table_with(
"query", ev.query, //
"full_text_content", ev.full_text_content, //
"cursor_position", ev.cursor_position, //
"is_first_word", ev.is_first_word //
);
}
lua_pop(L, 2);
return 0;
void c2_register_callback(Plugin *pl, EventType evtType,
sol::protected_function callback)
{
pl->callbacks[evtType] = std::move(callback);
}
int c2_log(lua_State *L)

View file

@ -1,13 +1,16 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/api/ChannelRef.hpp"
# include "controllers/plugins/Plugin.hpp"
# include <lua.h>
# include <QList>
# include <QString>
# include <sol/table.hpp>
# include <cassert>
# include <memory>
# include <vector>
struct lua_State;
namespace chatterino::lua::api {
@ -39,10 +42,12 @@ enum class LogLevel { Debug, Info, Warning, Critical };
* @lua@class CompletionList
*/
struct CompletionList {
CompletionList(const sol::table &);
/**
* @lua@field values string[] The completions
*/
std::vector<QString> values{};
QStringList values;
/**
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
@ -72,6 +77,8 @@ struct CompletionEvent {
bool is_first_word{};
};
sol::table toTable(lua_State *L, const CompletionEvent &ev);
/**
* @includefile common/Channel.hpp
* @includefile controllers/plugins/api/ChannelRef.hpp
@ -96,7 +103,8 @@ struct CompletionEvent {
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
* @exposed c2.register_callback
*/
int c2_register_callback(lua_State *L);
void c2_register_callback(Plugin *pl, EventType evtType,
sol::protected_function callback);
/**
* Writes a message to the Chatterino log.

View file

@ -227,23 +227,6 @@ 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{};

View file

@ -25,7 +25,6 @@ struct CommandContext;
namespace chatterino::lua {
namespace api {
struct CompletionList;
struct CompletionEvent;
} // namespace api
@ -73,7 +72,6 @@ bool peek(lua_State *L, double *out, StackIdx idx = -1);
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1);
/**
* @brief Converts Lua object at stack index idx to a string.

View file

@ -231,6 +231,7 @@ Plugin::~Plugin()
{
// clearing this after the state is gone is not safe to do
this->ownedCommands.clear();
this->callbacks.clear();
lua_close(this->state_);
}
}

View file

@ -2,8 +2,6 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "Application.hpp"
# include "common/Common.hpp"
# include "common/network/NetworkCommon.hpp"
# include "controllers/plugins/api/EventType.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginPermission.hpp"
@ -14,6 +12,7 @@
# include <semver/semver.hpp>
# include <sol/forward.hpp>
# include <optional>
# include <unordered_map>
# include <unordered_set>
# include <vector>
@ -105,35 +104,19 @@ public:
return this->loadDirectory_.absoluteFilePath("data");
}
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
using LuaCompletionCallback =
lua::CallbackFunction<lua::api::CompletionList,
lua::api::CompletionEvent>;
std::optional<LuaCompletionCallback> getCompletionCallback()
std::optional<sol::protected_function> getCompletionCallback()
{
if (this->state_ == nullptr || !this->error_.isNull())
{
return {};
}
// this uses magic enum to help automatic tooling find usages
auto typeName =
magic_enum::enum_name(lua::api::EventType::CompletionRequested);
std::string cbName;
cbName.reserve(5 + typeName.size());
cbName += "c2cb-";
cbName += typeName;
auto typ =
lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str());
if (typ != LUA_TFUNCTION)
auto it =
this->callbacks.find(lua::api::EventType::CompletionRequested);
if (it == this->callbacks.end())
{
lua_pop(this->state_, 1);
return {};
}
// move
return std::make_optional<lua::CallbackFunction<
lua::api::CompletionList, lua::api::CompletionEvent>>(
this->state_, lua_gettop(this->state_));
return it->second;
}
/**
@ -150,6 +133,8 @@ public:
bool hasFSPermissionFor(bool write, const QString &path);
bool hasHTTPPermissionFor(const QUrl &url);
std::map<lua::api::EventType, sol::protected_function> callbacks;
private:
QDir loadDirectory_;
lua_State *state_;

View file

@ -22,6 +22,7 @@
# include <lua.h>
# include <lualib.h>
# include <QJsonDocument>
# include <sol/forward.hpp>
# include <sol/sol.hpp>
# include <memory>
@ -150,7 +151,6 @@ void PluginController::openLibrariesFor(Plugin *plugin, const QDir &pluginDir)
// NOLINTNEXTLINE(*-avoid-c-arrays)
static const luaL_Reg c2Lib[] = {
{"register_callback", lua::api::c2_register_callback},
{"log", lua::api::c2_log},
{"later", lua::api::c2_later},
{nullptr, nullptr},
@ -166,9 +166,6 @@ void PluginController::openLibrariesFor(Plugin *plugin, const QDir &pluginDir)
lua::pushEnumTable<lua::api::LogLevel>(L);
lua_setfield(L, c2libIdx, "LogLevel");
lua::pushEnumTable<lua::api::EventType>(L);
lua_setfield(L, c2libIdx, "EventType");
lua_setfield(L, gtable, "c2");
// ban functions
@ -290,11 +287,17 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
[plugin](const QString &name, sol::protected_function cb) {
return plugin->registerCommand(name, std::move(cb));
});
c2.set_function("register_callback", [plugin](lua::api::EventType ev,
sol::protected_function cb) {
lua::api::c2_register_callback(plugin, ev, std::move(cb));
});
lua::api::ChannelRef::createUserType(c2);
lua::api::HTTPResponse::createUserType(c2);
lua::api::HTTPRequest::createUserType(plugin->state_, c2);
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
}
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
@ -438,32 +441,31 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
continue;
}
lua::StackGuard guard(pl->state_);
auto opt = pl->getCompletionCallback();
if (opt)
{
qCDebug(chatterinoLua)
<< "Processing custom completions from plugin" << name;
auto &cb = *opt;
auto errOrList = cb(lua::api::CompletionEvent{
.query = query,
.full_text_content = fullTextContent,
.cursor_position = cursorPosition,
.is_first_word = isFirstWord,
});
if (std::holds_alternative<int>(errOrList))
sol::state_view view(pl->state_);
auto errOrList = lua::tryCall<sol::table>(
cb,
toTable(pl->state_, lua::api::CompletionEvent{
.query = query,
.full_text_content = fullTextContent,
.cursor_position = cursorPosition,
.is_first_word = isFirstWord,
}));
if (!errOrList.has_value())
{
guard.handled();
int err = std::get<int>(errOrList);
qCDebug(chatterinoLua)
<< "Got error from plugin " << pl->meta.name
<< " while refreshing tab completion: "
<< lua::humanErrorText(pl->state_, err);
<< errOrList.get_unexpected().error();
continue;
}
auto list = std::get<lua::api::CompletionList>(errOrList);
auto list = lua::api::CompletionList(*errOrList);
if (list.hideOthers)
{
results = QStringList(list.values.begin(), list.values.end());