mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Add a new completion API for experimental plugins feature. (#5000)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
e4258160cd
commit
fd4cac2c2c
|
@ -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)
|
||||
|
|
21
docs/chatterino.d.ts
vendored
21
docs/chatterino.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -26,8 +26,12 @@ public:
|
|||
|
||||
/// @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 {
|
||||
|
|
|
@ -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;
|
||||
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
@ -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)";
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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{};
|
||||
|
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <magic_enum/magic_enum.hpp>
|
||||
# include <QList>
|
||||
|
||||
# include <cassert>
|
||||
# include <optional>
|
||||
# include <string>
|
||||
# include <string_view>
|
||||
# include <type_traits>
|
||||
# include <variant>
|
||||
# include <vector>
|
||||
struct lua_State;
|
||||
class QJsonObject;
|
||||
|
@ -19,6 +24,12 @@ struct CommandContext;
|
|||
|
||||
namespace chatterino::lua {
|
||||
|
||||
namespace api {
|
||||
struct CompletionList;
|
||||
} // namespace api
|
||||
|
||||
constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
||||
|
||||
/**
|
||||
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
||||
*
|
||||
|
@ -52,20 +63,136 @@ StackIdx push(lua_State *L, const CommandContext &ctx);
|
|||
StackIdx push(lua_State *L, const QString &str);
|
||||
StackIdx push(lua_State *L, const std::string &str);
|
||||
StackIdx push(lua_State *L, const bool &b);
|
||||
StackIdx push(lua_State *L, const int &b);
|
||||
|
||||
// returns OK?
|
||||
bool peek(lua_State *L, bool *out, StackIdx idx = -1);
|
||||
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.
|
||||
*/
|
||||
QString toString(lua_State *L, StackIdx idx = -1);
|
||||
|
||||
// This object ensures that the stack is of expected size when it is destroyed
|
||||
class StackGuard
|
||||
{
|
||||
int expected;
|
||||
lua_State *L;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Use this constructor if you expect the stack size to be the same on the
|
||||
* destruction of the object as its creation
|
||||
*/
|
||||
StackGuard(lua_State *L)
|
||||
: expected(lua_gettop(L))
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you expect the stack size changing, diff is the expected difference
|
||||
* Ex: diff=3 means three elements added to the stack
|
||||
*/
|
||||
StackGuard(lua_State *L, int diff)
|
||||
: expected(lua_gettop(L) + diff)
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
~StackGuard()
|
||||
{
|
||||
if (expected < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int after = lua_gettop(this->L);
|
||||
if (this->expected != after)
|
||||
{
|
||||
stackDump(this->L, "StackGuard check tripped");
|
||||
// clang-format off
|
||||
// clang format likes to insert a new line which means that some builds won't show this message fully
|
||||
assert(false && "internal error: lua stack was not in an expected state");
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
// This object isn't meant to be passed around
|
||||
StackGuard operator=(StackGuard &) = delete;
|
||||
StackGuard &operator=(StackGuard &&) = delete;
|
||||
StackGuard(StackGuard &) = delete;
|
||||
StackGuard(StackGuard &&) = delete;
|
||||
|
||||
// This function tells the StackGuard that the stack isn't in an expected state but it was handled
|
||||
void handled()
|
||||
{
|
||||
this->expected = -1;
|
||||
}
|
||||
};
|
||||
|
||||
/// TEMPLATES
|
||||
|
||||
template <typename T>
|
||||
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
||||
{
|
||||
if (lua_isnil(L, idx))
|
||||
{
|
||||
*out = std::nullopt;
|
||||
return true;
|
||||
}
|
||||
|
||||
*out = T();
|
||||
return peek(L, out->operator->(), idx);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool peek(lua_State *L, std::vector<T> *vec, StackIdx idx = -1)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
{
|
||||
lua::stackDump(L, "!table");
|
||||
qCDebug(chatterinoLua)
|
||||
<< "value is not a table, type is" << lua_type(L, idx);
|
||||
return false;
|
||||
}
|
||||
auto len = lua_rawlen(L, idx);
|
||||
if (len == 0)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "value has 0 length";
|
||||
return true;
|
||||
}
|
||||
if (len > 1'000'000)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "value is too long";
|
||||
return false;
|
||||
}
|
||||
// count like lua
|
||||
for (int i = 1; i <= len; i++)
|
||||
{
|
||||
lua_geti(L, idx, i);
|
||||
std::optional<T> obj;
|
||||
if (!lua::peek(L, &obj))
|
||||
{
|
||||
//lua_seti(L, LUA_REGISTRYINDEX, 1); // lazy
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Failed to convert lua object into c++: at array index " << i
|
||||
<< ":";
|
||||
stackDump(L, "bad conversion into string");
|
||||
return false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
vec->push_back(obj.value());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts object at stack index idx to enum given by template parameter T
|
||||
*/
|
||||
|
@ -150,6 +277,7 @@ StackIdx push(lua_State *L, T inp)
|
|||
template <typename T>
|
||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
||||
{
|
||||
StackGuard guard(L, -1);
|
||||
auto ok = peek(L, out, idx);
|
||||
if (ok)
|
||||
{
|
||||
|
@ -186,6 +314,58 @@ StackIdx pushEnumTable(lua_State *L)
|
|||
return out;
|
||||
}
|
||||
|
||||
// Represents a Lua function on the stack
|
||||
template <typename ReturnType, typename... Args>
|
||||
class CallbackFunction
|
||||
{
|
||||
StackIdx stackIdx_;
|
||||
lua_State *L;
|
||||
|
||||
public:
|
||||
CallbackFunction(lua_State *L, StackIdx stackIdx)
|
||||
: stackIdx_(stackIdx)
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
// this type owns the stackidx, it must not be trivially copiable
|
||||
CallbackFunction operator=(CallbackFunction &) = delete;
|
||||
CallbackFunction(CallbackFunction &) = delete;
|
||||
|
||||
// Permit only move
|
||||
CallbackFunction &operator=(CallbackFunction &&) = default;
|
||||
CallbackFunction(CallbackFunction &&) = default;
|
||||
|
||||
~CallbackFunction()
|
||||
{
|
||||
lua_remove(L, this->stackIdx_);
|
||||
}
|
||||
|
||||
std::variant<int, ReturnType> operator()(Args... arguments)
|
||||
{
|
||||
lua_pushvalue(this->L, this->stackIdx_);
|
||||
( // apparently this calls lua::push() for every Arg
|
||||
[this, &arguments] {
|
||||
lua::push(this->L, arguments);
|
||||
}(),
|
||||
...);
|
||||
|
||||
int res = lua_pcall(L, sizeof...(Args), 1, 0);
|
||||
if (res != LUA_OK)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "error is: " << res;
|
||||
return {res};
|
||||
}
|
||||
|
||||
ReturnType val;
|
||||
if (!lua::pop(L, &val))
|
||||
{
|
||||
return {ERROR_BAD_PEEK};
|
||||
}
|
||||
return {val};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace chatterino::lua
|
||||
|
||||
#endif
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
|
@ -85,10 +87,51 @@ public:
|
|||
return this->loadDirectory_;
|
||||
}
|
||||
|
||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
||||
using LuaCompletionCallback =
|
||||
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
|
||||
bool>;
|
||||
std::optional<LuaCompletionCallback> getCompletionCallback()
|
||||
{
|
||||
if (this->state_ == nullptr || !this->error_.isNull())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// this uses magic enum to help automatic tooling find usages
|
||||
auto typ =
|
||||
lua_getfield(this->state_, LUA_REGISTRYINDEX,
|
||||
QString("c2cb-%1")
|
||||
.arg(magic_enum::enum_name<lua::api::EventType>(
|
||||
lua::api::EventType::CompletionRequested)
|
||||
.data())
|
||||
.toStdString()
|
||||
.c_str());
|
||||
if (typ != LUA_TFUNCTION)
|
||||
{
|
||||
lua_pop(this->state_, 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
// move
|
||||
return std::make_optional<lua::CallbackFunction<
|
||||
lua::api::CompletionList, QString, QString, int, bool>>(
|
||||
this->state_, lua_gettop(this->state_));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the plugin crashes while evaluating the main file, this function will return the error
|
||||
*/
|
||||
QString error()
|
||||
{
|
||||
return this->error_;
|
||||
}
|
||||
|
||||
private:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
||||
QString error_;
|
||||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
# include <variant>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -46,15 +47,13 @@ void PluginController::loadPlugins()
|
|||
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||
for (const auto &info :
|
||||
dir.entryInfoList(QDir::NoFilter | QDir::NoDotAndDotDot))
|
||||
dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
|
||||
{
|
||||
if (info.isDir())
|
||||
{
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
}
|
||||
|
||||
bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||
{
|
||||
// look for init.lua
|
||||
|
@ -139,6 +138,7 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
static const luaL_Reg c2Lib[] = {
|
||||
{"system_msg", lua::api::c2_system_msg},
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"register_callback", lua::api::c2_register_callback},
|
||||
{"send_msg", lua::api::c2_send_msg},
|
||||
{"log", lua::api::c2_log},
|
||||
{nullptr, nullptr},
|
||||
|
@ -146,14 +146,17 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
lua_pushglobaltable(L);
|
||||
auto global = lua_gettop(L);
|
||||
|
||||
// count of elements in C2LIB + LogLevel
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 5);
|
||||
// count of elements in C2LIB + LogLevel + EventType
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
||||
|
||||
luaL_setfuncs(L, c2Lib, 0);
|
||||
|
||||
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, global, "c2");
|
||||
|
||||
// ban functions
|
||||
|
@ -198,6 +201,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
auto pluginName = pluginDir.dirName();
|
||||
lua_State *l = luaL_newstate();
|
||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||
auto *temp = plugin.get();
|
||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
||||
|
||||
if (getArgs().safeMode)
|
||||
|
@ -220,9 +224,10 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||
if (err != 0)
|
||||
{
|
||||
temp->error_ = lua::humanErrorText(l, err);
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
||||
<< lua::humanErrorText(l, err);
|
||||
<< temp->error_;
|
||||
return;
|
||||
}
|
||||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||
|
@ -307,5 +312,52 @@ const std::map<QString, std::unique_ptr<Plugin>> &PluginController::plugins()
|
|||
return this->plugins_;
|
||||
}
|
||||
|
||||
}; // namespace chatterino
|
||||
std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
||||
const QString &query, const QString &fullTextContent, int cursorPosition,
|
||||
bool isFirstWord) const
|
||||
{
|
||||
QStringList results;
|
||||
|
||||
for (const auto &[name, pl] : this->plugins())
|
||||
{
|
||||
if (!pl->error().isNull())
|
||||
{
|
||||
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(query, fullTextContent, cursorPosition, isFirstWord);
|
||||
if (std::holds_alternative<int>(errOrList))
|
||||
{
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
||||
if (list.hideOthers)
|
||||
{
|
||||
results = QStringList(list.values.begin(), list.values.end());
|
||||
return {true, results};
|
||||
}
|
||||
results += QStringList(list.values.begin(), list.values.end());
|
||||
}
|
||||
}
|
||||
|
||||
return {false, results};
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -36,6 +36,7 @@ public:
|
|||
// This is required to be public because of c functions
|
||||
Plugin *getPluginByStatePtr(lua_State *L);
|
||||
|
||||
// TODO: make a function that iterates plugins that aren't errored/enabled
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &plugins() const;
|
||||
|
||||
/**
|
||||
|
@ -52,6 +53,10 @@ public:
|
|||
*/
|
||||
static bool isPluginEnabled(const QString &id);
|
||||
|
||||
std::pair<bool, QStringList> updateCustomCompletions(
|
||||
const QString &query, const QString &fullTextContent,
|
||||
int cursorPosition, bool isFirstWord) const;
|
||||
|
||||
private:
|
||||
void loadPlugins();
|
||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||
|
@ -64,5 +69,5 @@ private:
|
|||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||
};
|
||||
|
||||
}; // namespace chatterino
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -170,8 +170,9 @@ void ResizingTextEdit::keyPressEvent(QKeyEvent *event)
|
|||
// First type pressing tab after modifying a message, we refresh our
|
||||
// completion model
|
||||
this->completer_->setModel(completionModel);
|
||||
completionModel->updateResults(currentCompletion,
|
||||
this->isFirstWord());
|
||||
completionModel->updateResults(
|
||||
currentCompletion, this->toPlainText(),
|
||||
this->textCursor().position(), this->isFirstWord());
|
||||
this->completionInProgress_ = true;
|
||||
{
|
||||
// this blocks cursor movement events from resetting tab completion
|
||||
|
|
|
@ -108,6 +108,16 @@ void PluginsPage::rebuildContent()
|
|||
warningLabel->setStyleSheet("color: #f00");
|
||||
pluginEntry->addRow(warningLabel);
|
||||
}
|
||||
if (!plugin->error().isNull())
|
||||
{
|
||||
auto *errorLabel =
|
||||
new QLabel("There was an error while loading this plugin: " +
|
||||
plugin->error(),
|
||||
this->dataFrame_);
|
||||
errorLabel->setStyleSheet("color: #f00");
|
||||
errorLabel->setWordWrap(true);
|
||||
pluginEntry->addRow(errorLabel);
|
||||
}
|
||||
|
||||
auto *description =
|
||||
new QLabel(plugin->meta.description, this->dataFrame_);
|
||||
|
|
Loading…
Reference in a new issue