From 0214cac51d7ebda76c0fa56d9ce63663afce70fc Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Thu, 13 Jun 2024 14:50:37 +0200 Subject: [PATCH] Lua: Interactive Shell Change-Id: Iedd620abcb62b9dd3e640bcb80ae011016386484 Reviewed-by: hjk --- src/plugins/lua/CMakeLists.txt | 6 + src/plugins/lua/luaengine.cpp | 6 +- src/plugins/lua/luaengine.h | 5 +- src/plugins/lua/luaplugin.cpp | 171 +++++++++++++++ src/plugins/lua/scripts/ilua.lua | 359 +++++++++++++++++++++++++++++++ 5 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 src/plugins/lua/scripts/ilua.lua diff --git a/src/plugins/lua/CMakeLists.txt b/src/plugins/lua/CMakeLists.txt index 8a3bb96dc11..465b3ac1e1a 100644 --- a/src/plugins/lua/CMakeLists.txt +++ b/src/plugins/lua/CMakeLists.txt @@ -50,6 +50,12 @@ qt_add_resources(Lua lua_images_rcc images/settingscategory_lua@2x.png ) +qt_add_resources(Lua lua_script_rcc + PREFIX "/lua" + FILES + scripts/ilua.lua +) + set_source_files_properties(luauibindings.cpp PROPERTY SKIP_AUTOMOC ON PROPERTY SKIP_AUTOGEN ON) if (MSVC) diff --git a/src/plugins/lua/luaengine.cpp b/src/plugins/lua/luaengine.cpp index abfc78957b1..8872e85bce3 100644 --- a/src/plugins/lua/luaengine.cpp +++ b/src/plugins/lua/luaengine.cpp @@ -81,7 +81,8 @@ public: }; // Runs the gives script in a new Lua state. The returned Object manages the lifetime of the state. -std::unique_ptr LuaEngine::runScript(const QString &script, const QString &name) +std::unique_ptr LuaEngine::runScript( + const QString &script, const QString &name, std::function customizeState) { std::unique_ptr opaque = std::make_unique(); @@ -124,6 +125,9 @@ std::unique_ptr LuaEngine::runScript(const QString &script, con for (const auto &func : d->m_autoProviders) func(opaque->lua); + if (customizeState) + customizeState(opaque->lua); + auto result = opaque->lua .safe_script(script.toStdString(), sol::script_pass_on_error, name.toStdString()); diff --git a/src/plugins/lua/luaengine.h b/src/plugins/lua/luaengine.h index 7c8bbda7c89..5dce7c2f531 100644 --- a/src/plugins/lua/luaengine.h +++ b/src/plugins/lua/luaengine.h @@ -108,7 +108,10 @@ public: } // Runs the given script in a new Lua state. The returned Object manages the lifetime of the state. - std::unique_ptr runScript(const QString &script, const QString &name); + std::unique_ptr runScript( + const QString &script, + const QString &name, + std::function customizeState = {}); protected: Utils::expected_str connectHooks( diff --git a/src/plugins/lua/luaplugin.cpp b/src/plugins/lua/luaplugin.cpp index 0290df988c3..54beafafe12 100644 --- a/src/plugins/lua/luaplugin.cpp +++ b/src/plugins/lua/luaplugin.cpp @@ -3,8 +3,10 @@ #include "luaengine.h" #include "luapluginspec.h" +#include "luatr.h" #include +#include #include #include @@ -12,8 +14,15 @@ #include #include +#include +#include +#include #include +#include +#include +#include +#include using namespace Core; using namespace Utils; @@ -49,6 +58,164 @@ public: } }; +class LuaTerminal : public QListView +{ + Q_OBJECT + + std::unique_ptr m_luaState; + sol::function m_readCallback; + + QStringListModel m_model; + +public: + LuaTerminal(QWidget *parent = nullptr) + : QListView(parent) + { + setModel(&m_model); + } + + void showEvent(QShowEvent *) override + { + if (m_luaState) { + return; + } + resetTerminal(); + } + + void keyPressEvent(QKeyEvent *e) override { emit keyPressed(e); } + + void handleRequestResult(const QString &result) + { + auto cb = m_readCallback; + m_readCallback = {}; + cb(result); + } + + void resetTerminal() + { + m_model.setStringList({}); + m_readCallback = {}; + + QFile f(":/lua/scripts/ilua.lua"); + f.open(QIODevice::ReadOnly); + const auto ilua = QString::fromUtf8(f.readAll()); + m_luaState = LuaEngine::instance().runScript(ilua, "ilua.lua", [this](sol::state &lua) { + lua["print"] = [this](sol::variadic_args va) { + const QStringList msgs = LuaEngine::variadicToStringList(va) + .join("\t") + .replace("\r\n", "\n") + .split('\n'); + m_model.setStringList(m_model.stringList() + msgs); + scrollToBottom(); + }; + + sol::table async = lua.script("return require('async')", "_ilua_").get(); + sol::function wrap = async["wrap"]; + + lua["readline_cb"] = [this](const QString &prompt, sol::function callback) { + m_model.setStringList(m_model.stringList() << prompt); + scrollToBottom(); + emit inputRequested(prompt); + m_readCallback = callback; + }; + + lua["readline"] = wrap(lua["readline_cb"]); + }); + + QListView::reset(); + } + +signals: + void inputRequested(const QString &prompt); + void keyPressed(QKeyEvent *e); +}; + +class LineEdit : public FancyLineEdit +{ +public: + using FancyLineEdit::FancyLineEdit; + void keyPressEvent(QKeyEvent *e) override { FancyLineEdit::keyPressEvent(e); } +}; + +class LuaPane : public Core::IOutputPane +{ + Q_OBJECT + +protected: + QWidget *m_ui{nullptr}; + LuaTerminal *m_terminal{nullptr}; + +public: + LuaPane(QObject *parent = nullptr) + : Core::IOutputPane(parent) + { + setId("LuaPane"); + setDisplayName(Tr::tr("Lua")); + setPriorityInStatusBar(20); + } + + QWidget *outputWidget(QWidget *parent) override + { + using namespace Layouting; + + if (!m_ui && parent) { + m_terminal = new LuaTerminal; + LineEdit *inputEdit = new LineEdit; + QLabel *prompt = new QLabel; + + // clang-format off + m_ui = Column { + m_terminal, + Row { prompt, inputEdit }, + }.emerge(); + // clang-format on + + inputEdit->setReadOnly(true); + inputEdit->setHistoryCompleter(Utils::Key("LuaREPL.InputHistory")); + + connect(inputEdit, &QLineEdit::returnPressed, this, [this, inputEdit] { + inputEdit->setReadOnly(true); + m_terminal->handleRequestResult(inputEdit->text()); + inputEdit->onEditingFinished(); + inputEdit->clear(); + }); + connect( + m_terminal, + &LuaTerminal::inputRequested, + this, + [prompt, inputEdit](const QString &p) { + prompt->setText(p); + inputEdit->setReadOnly(false); + }); + connect(m_terminal, &LuaTerminal::keyPressed, this, [inputEdit](QKeyEvent *e) { + inputEdit->keyPressEvent(e); + inputEdit->setFocus(); + }); + } + + return m_ui; + } + + void visibilityChanged(bool) override {}; + + void clearContents() override + { + if (m_terminal) + m_terminal->resetTerminal(); + } + void setFocus() override { outputWidget(nullptr)->setFocus(); } + bool hasFocus() const override { return true; } + bool canFocus() const override { return true; } + + bool canNavigate() const override { return false; } + bool canNext() const override { return false; } + bool canPrevious() const override { return false; } + void goToNext() override {} + void goToPrev() override {} + + QList toolBarWidgets() const override { return {}; } +}; + class LuaPlugin : public IPlugin { Q_OBJECT @@ -56,6 +223,7 @@ class LuaPlugin : public IPlugin private: std::unique_ptr m_luaEngine; + LuaPane *m_pane = nullptr; public: LuaPlugin() {} @@ -79,6 +247,9 @@ public: addInstallModule(); Core::JsExpander::registerGlobalObject("Lua", [] { return new LuaJsExtension(); }); + + m_pane = new LuaPane; + ExtensionSystem::PluginManager::addObject(m_pane); } bool delayedInitialize() final diff --git a/src/plugins/lua/scripts/ilua.lua b/src/plugins/lua/scripts/ilua.lua new file mode 100644 index 00000000000..3efaf0b788a --- /dev/null +++ b/src/plugins/lua/scripts/ilua.lua @@ -0,0 +1,359 @@ +-- ilua.lua +-- A more friendly Lua interactive prompt +-- doesn't need '=' +-- will try to print out tables recursively, subject to the pretty_print_limit value. +-- Steve Donovan, 2007 +-- +local pretty_print_limit = 20 +local max_depth = 7 +local table_clever = true +local prompt = '> ' +local verbose = false +local strict = true +-- suppress strict warnings +_ = true + +-- imported global functions +local sub = string.sub +local match = string.match +local find = string.find +local push = table.insert +local pop = table.remove +local append = table.insert +local concat = table.concat +local floor = math.floor +local write = io.write +local read = io.read + +local collisions = {} +local G_LIB = {} +local declared = {} +local line_handler_fn, global_handler_fn +local print_handlers = {} + +ilua = {} +local num_prec +local num_all + +local jstack = {} + +local function oprint(...) + print(...) +end + +local function join(tbl, delim, limit, depth) + if not limit then limit = pretty_print_limit end + if not depth then depth = max_depth end + local n = #tbl + local res = '' + local k = 0 + -- very important to avoid disgracing ourselves with circular referencs... + if #jstack > depth then + return "..." + end + for i, t in ipairs(jstack) do + if tbl == t then + return "" + end + end + push(jstack, tbl) + -- this is a hack to work out if a table is 'list-like' or 'map-like' + -- you can switch it off with ilua.table_options {clever = false} + local is_list + if table_clever then + local index1 = n > 0 and tbl[1] + local index2 = n > 1 and tbl[2] + is_list = index1 and index2 + end + if is_list then + for i, v in ipairs(tbl) do + res = res .. delim .. val2str(v) + k = k + 1 + if k > limit then + res = res .. " ... " + break + end + end + else + for key, v in pairs(tbl) do + if type(key) == 'number' then + key = '[' .. tostring(key) .. ']' + else + key = tostring(key) + end + res = res .. delim .. key .. '=' .. val2str(v) + k = k + 1 + if k > limit then + res = res .. " ... " + break + end + end + end + pop(jstack) + return sub(res, 2) +end + + +function val2str(val) + local tp = type(val) + if print_handlers[tp] then + local s = print_handlers[tp](val) + return s or '?' + end + if tp == 'function' then + return tostring(val) + elseif tp == 'table' then + if val.__tostring then + return tostring(val) + else + return '{' .. join(val, ',') .. '}' + end + elseif tp == 'string' then + return "'" .. val .. "'" + elseif tp == 'number' then + -- we try only to apply floating-point precision for numbers deemed to be floating-point, + -- unless the 3rd arg to precision() is true. + if num_prec and (num_all or floor(val) ~= val) then + return num_prec:format(val) + else + return tostring(val) + end + else + return tostring(val) + end +end + +function _pretty_print(...) + local arg = table.pack(...) + for i, val in ipairs(arg) do + oprint(val2str(val)) + end + _G['_'] = arg[1] +end + +function compile(line) + if verbose then oprint(line) end + local f, err = load(line, 'local') + return err, f +end + +function evaluate(chunk) + local ok, res = pcall(chunk) + if not ok then + return res + end + return nil -- meaning, fine! +end + +function eval_lua(line) + -- is the line handler interested? + if line_handler_fn then + line = line_handler_fn(line) + -- returning nil here means that the handler doesn't want + -- Lua to see the string + if not line then return end + end + -- is it an expression? + local err, chunk = compile('_pretty_print(' .. line .. ')') + if err then + -- otherwise, a statement? + err, chunk = compile(line) + end + -- if compiled ok, then evaluate the chunk + if not err then + err = evaluate(chunk) + end + -- if there was any error, print it out + if err then + oprint(err) + end +end + +local function quit(code, msg) + io.stderr:write(msg, '\n') + os.exit(code) +end + +-- functions available in scripts +function ilua.precision(len, prec, all) + if not len then + num_prec = nil + else + num_prec = '%' .. len .. '.' .. prec .. 'f' + end + num_all = all +end + +function ilua.table_options(t) + if t.limit then pretty_print_limit = t.limit end + if t.depth then max_depth = t.depth end + if t.clever ~= nil then table_clever = t.clever end +end + +-- inject @tbl into the global namespace +function ilua.import(tbl, dont_complain, lib) + lib = lib or '' + if type(tbl) == 'table' then + for k, v in pairs(tbl) do + local key = rawget(_G, k) + -- NB to keep track of collisions! + if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then + append(collisions, { k, lib, G_LIB[k] }) + end + _G[k] = v + G_LIB[k] = lib + end + end + if not dont_complain and #collisions > 0 then + for i, coll in ipairs(collisions) do + local name, lib, oldlib = coll[1], coll[2], coll[3] + write('warning: ', lib, '.', name, ' overwrites ') + if oldlib then + write(oldlib, '.', name, '\n') + else + write('global ', name, '\n') + end + end + end +end + +function ilua.print_handler(name, handler) + print_handlers[name] = handler +end + +function ilua.line_handler(handler) + line_handler_fn = handler +end + +function ilua.global_handler(handler) + global_handler_fn = handler +end + +function ilua.print_variables() + for name, v in pairs(declared) do + print(name, type(_G[name])) + end +end + +-- +-- strict.lua +-- checks uses of undeclared global variables +-- All global variables must be 'declared' through a regular assignment +-- (even assigning nil will do) in a main chunk before being used +-- anywhere. +-- +local function set_strict() + local mt = getmetatable(_G) + if mt == nil then + mt = {} + setmetatable(_G, mt) + end + + local function what() + local d = debug.getinfo(3, "S") + return d and d.what or "C" + end + + mt.__newindex = function(t, n, v) + declared[n] = true + rawset(t, n, v) + end + + mt.__index = function(t, n) + if not declared[n] and what() ~= "C" then + local lookup = global_handler_fn and global_handler_fn(n) + if not lookup then + error("variable '" .. n .. "' is not declared", 2) + else + return lookup + end + end + return rawget(t, n) + end +end + +--- Initial operations which may not succeed! +-- try to bring in any ilua configuration file; don't complain if this is unsuccessful +pcall(function() + require 'ilua-defs' +end) + +-- process command-line parameters +if arg then + local i = 1 + + local function parm_value(opt, parm, def) + local val = parm:sub(3) + if #val == 0 then + i = i + 1 + if i > #arg then + if not def then + quit(-1, "expecting parameter for option '-" .. opt .. "'") + else + return def + end + end + val = arg[i] + end + return val + end + + while i <= #arg do + local v = arg[i] + local opt = v:sub(1, 1) + if opt == '-' then + opt = v:sub(2, 2) + if opt == 'h' then + quit(0, "ilua (-l lib) (-L lib) (lua files)") + elseif opt == 'l' then + require(parm_value(opt, v)) + elseif opt == 'L' then + local lib = parm_value(opt, v) + local tbl = require(lib) + -- we cannot always trust require to return the table! + if type(tbl) ~= 'table' then + tbl = _G[lib] + end + ilua.import(tbl, true, lib) + elseif opt == 't' or opt == 'T' then + local file + if opt == 'T' then + file = 'ilua_' .. os.date('%y_%m_%d_%H_%M') .. '.log' + else + file = parm_value(opt, v, "ilua.log") + end + print('saving transcript "' .. file .. '"') + elseif opt == 's' then + strict = false + elseif opt == 'v' then + verbose = true + end + else -- a plain file to be executed immediately + dofile(v) + end + i = i + 1 + end +end + +print 'ILUA: Lua 5.4.6 Copyright (C) 1994-2007 Lua.org, PUC-Rio' + +-- any import complaints? +ilua.import() + +-- enable 'not declared' error +if strict then + set_strict() +end + +local a = require('async') + +a.sync(function() + local line = a.wait(readline(prompt)) + + while line do + ---if line == 'quit' then break end + eval_lua(line) + ---saveline(line) + line = a.wait(readline(prompt)) + end +end)()