Add optional Lua scripting module, examples, and validation
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/core/process.hpp>
|
||||
#include <faset/editor/build_service.hpp>
|
||||
#include <faset/scripting/project.hpp>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
@@ -114,6 +115,7 @@ struct BuildService::Impl {
|
||||
std::condition_variable finished;
|
||||
Json scene;
|
||||
Json asset_manifests = Json::object();
|
||||
scripting::LuaProject lua;
|
||||
fs::path output;
|
||||
};
|
||||
BuildConfig config;
|
||||
@@ -240,11 +242,16 @@ struct BuildService::Impl {
|
||||
Json build(Job& job, bool exporting = false) {
|
||||
const auto& configuration = exporting ? config.export_configuration : config.configuration;
|
||||
const auto native_directory = config.build_directory / configuration;
|
||||
checkpoint(job, "Configuring C++ gameplay", .05);
|
||||
if (!fs::is_regular_file(config.project_root / "Scripts" / "Gameplay.cpp") ||
|
||||
!fs::is_regular_file(config.project_root / "Scripts" / "Gameplay.hpp"))
|
||||
throw std::runtime_error("Project Scripts/Gameplay.cpp and Gameplay.hpp are required; "
|
||||
"create a project scaffold first");
|
||||
checkpoint(job, "Configuring gameplay", .05);
|
||||
job.lua = scripting::loadLuaProject(config.project_root);
|
||||
const auto cpp = config.project_root / "Scripts" / "Gameplay.cpp";
|
||||
const auto hpp = config.project_root / "Scripts" / "Gameplay.hpp";
|
||||
const bool has_cpp = fs::is_regular_file(cpp), has_hpp = fs::is_regular_file(hpp);
|
||||
if (has_cpp != has_hpp || (!has_cpp && !job.lua.enabled()))
|
||||
throw std::runtime_error("Project requires Scripts/Gameplay.cpp and Gameplay.hpp, "
|
||||
"or Lua entry scripts declared in project.faset.json");
|
||||
const auto cpp_source = has_cpp ? read_text(cpp) : std::string{};
|
||||
const auto hpp_source = has_hpp ? read_text(hpp) : std::string{};
|
||||
fs::create_directories(native_directory);
|
||||
std::vector<std::string> arguments = {config.cmake,
|
||||
"-S",
|
||||
@@ -278,6 +285,10 @@ struct BuildService::Impl {
|
||||
arguments.insert(arguments.end(), config.configure_arguments.begin(),
|
||||
config.configure_arguments.end());
|
||||
arguments.push_back("-DCMAKE_BUILD_TYPE=" + configuration);
|
||||
// Project declarations, not a stale cache or a user-supplied override, determine
|
||||
// whether the packaged game has a Lua VM linked into it.
|
||||
arguments.push_back(std::string("-DFASET_ENABLE_LUA=") +
|
||||
(job.lua.enabled() ? "ON" : "OFF"));
|
||||
run(job, std::move(arguments), config.project_root);
|
||||
checkpoint(job, "Compiling and linking Player", .25);
|
||||
run(job,
|
||||
@@ -292,8 +303,18 @@ struct BuildService::Impl {
|
||||
fs::create_directories(staging);
|
||||
try {
|
||||
const auto schema_file = staging / "schema.json";
|
||||
run(job, {path_to_utf8(exporter), "--output", path_to_utf8(schema_file)},
|
||||
config.project_root);
|
||||
std::vector<std::string> export_arguments = {path_to_utf8(exporter), "--output",
|
||||
path_to_utf8(schema_file)};
|
||||
if (job.lua.enabled()) {
|
||||
scripting::writeLuaSources(job.lua, staging);
|
||||
atomic_write_json(staging / "project.faset.json",
|
||||
{{"format", "faset.project"},
|
||||
{"version", 1},
|
||||
{"scripting", {{"lua", {{"scripts", job.lua.scripts}}}}}});
|
||||
export_arguments.insert(export_arguments.end(),
|
||||
{"--project", path_to_utf8(staging)});
|
||||
}
|
||||
run(job, std::move(export_arguments), config.project_root);
|
||||
auto schema = read_json(schema_file);
|
||||
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
|
||||
!schema.contains("types") || !schema.at("types").is_array())
|
||||
@@ -303,10 +324,11 @@ struct BuildService::Impl {
|
||||
(void)authoring::gameplay_schemas(schema);
|
||||
std::string fingerprint = sha256_file(player) + sha256_file(exporter) +
|
||||
read_text(native_directory / "CMakeCache.txt");
|
||||
for (const auto& file : {"Gameplay.cpp", "Gameplay.hpp"})
|
||||
fingerprint += read_text(config.project_root / "Scripts" / file);
|
||||
fingerprint += cpp_source + hpp_source + job.lua.fingerprint;
|
||||
fingerprint = sha256(fingerprint);
|
||||
schema["build_fingerprint"] = fingerprint;
|
||||
if (job.lua.enabled())
|
||||
schema["lua_fingerprint"] = job.lua.fingerprint;
|
||||
atomic_write_json(schema_file, schema);
|
||||
copy_required_file(player, staging / ("faset_player" + executable_suffix()));
|
||||
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
|
||||
@@ -320,10 +342,19 @@ struct BuildService::Impl {
|
||||
{"id", job.status.id},
|
||||
{"fingerprint", fingerprint},
|
||||
{"configuration", configuration},
|
||||
{"lua_enabled", job.lua.enabled()},
|
||||
{"lua_fingerprint", job.lua.fingerprint},
|
||||
{"player", "faset_player" + executable_suffix()},
|
||||
{"schema", "schema.json"}};
|
||||
atomic_write_json(staging / "manifest.json", manifest);
|
||||
checkpoint(job, "Publishing build generation", .68);
|
||||
if (job.lua.enabled() &&
|
||||
scripting::loadLuaProject(staging).fingerprint != job.lua.fingerprint)
|
||||
throw std::runtime_error("Lua build snapshot changed during schema export");
|
||||
if (scripting::loadLuaProject(config.project_root).fingerprint != job.lua.fingerprint ||
|
||||
has_cpp != fs::is_regular_file(cpp) || has_hpp != fs::is_regular_file(hpp) ||
|
||||
(has_cpp && (read_text(cpp) != cpp_source || read_text(hpp) != hpp_source)))
|
||||
throw std::runtime_error("Gameplay sources changed during the build; build again");
|
||||
fs::rename(staging, generation);
|
||||
atomic_write_json(config.cache_root / "last_build.json",
|
||||
{{"generation", job.status.id}, {"fingerprint", fingerprint}});
|
||||
@@ -333,6 +364,8 @@ struct BuildService::Impl {
|
||||
{"configuration", configuration},
|
||||
{"player", path_to_utf8(generation / ("faset_player" + executable_suffix()))},
|
||||
{"schema", path_to_utf8(generation / "schema.json")},
|
||||
{"lua_enabled", job.lua.enabled()},
|
||||
{"lua_fingerprint", job.lua.fingerprint},
|
||||
{"fingerprint", fingerprint}};
|
||||
} catch (...) {
|
||||
std::error_code error;
|
||||
@@ -442,7 +475,8 @@ struct BuildService::Impl {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void package_notices(const fs::path& destination, const fs::path& native_directory) {
|
||||
void package_notices(const fs::path& destination, const fs::path& native_directory,
|
||||
bool lua_enabled) {
|
||||
fs::create_directories(destination);
|
||||
auto lock = read_json(config.engine_root / "dependencies.lock.json");
|
||||
const std::vector<std::string> runtime_dependencies = {"sdl3", "entt", "box2d",
|
||||
@@ -476,6 +510,11 @@ struct BuildService::Impl {
|
||||
throw std::runtime_error("Cannot package required license notices for " + name);
|
||||
used[name] = lock.at("dependencies").at(name);
|
||||
}
|
||||
if (lua_enabled) {
|
||||
copy_required_file(config.engine_root / "docs" / "licenses" / "Lua.txt",
|
||||
destination / "lua" / "LICENSE.txt");
|
||||
used["lua"] = lock.at("dependencies").at("lua");
|
||||
}
|
||||
atomic_write_json(destination / "dependencies.json", used);
|
||||
if (fs::is_regular_file(config.engine_root / "LICENSE"))
|
||||
copy_required_file(config.engine_root / "LICENSE", destination / "Faset-LICENSE");
|
||||
@@ -529,6 +568,16 @@ struct BuildService::Impl {
|
||||
checkpoint(job, "Cooking export snapshot", .72);
|
||||
write_cooked_scene(staging / "scene.fscene", job.scene);
|
||||
auto build_directory = path_from_utf8(built.at("directory").get<std::string>());
|
||||
if (job.lua.enabled()) {
|
||||
// Never read live Scripts files for a published game: schemas, source,
|
||||
// and fingerprint all originate in the same validated build snapshot.
|
||||
const auto captured = scripting::loadLuaProject(build_directory);
|
||||
if (captured.fingerprint != job.lua.fingerprint)
|
||||
throw std::runtime_error("Lua build snapshot is corrupt");
|
||||
scripting::writeLuaSources(captured, staging);
|
||||
copy_required_file(build_directory / "project.faset.json",
|
||||
staging / "project.faset.json");
|
||||
}
|
||||
copy_required_file(build_directory / ("faset_player" + executable_suffix()),
|
||||
staging / ("faset_player" + executable_suffix()));
|
||||
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||
@@ -546,12 +595,15 @@ struct BuildService::Impl {
|
||||
checkpoint(job, "Packaging assets and notices", .80);
|
||||
package_assets(job, staging);
|
||||
package_notices(staging / "Notices",
|
||||
path_from_utf8(built.at("build_directory").get<std::string>()));
|
||||
path_from_utf8(built.at("build_directory").get<std::string>()),
|
||||
job.lua.enabled());
|
||||
atomic_write(
|
||||
staging / "README.txt",
|
||||
"Run faset_player" + executable_suffix() +
|
||||
" to start this game.\nThe executable loads scene.fscene and assets beside "
|
||||
"it.\nKeep shaders/, assets/, and Notices/ with the executable.\nA compatible "
|
||||
"it.\nKeep shaders/, assets/, Notices/, and any Scripts/ and "
|
||||
"project.faset.json "
|
||||
"with the executable.\nA compatible "
|
||||
"Vulkan 1.3 driver and the supported OS runtime are required.\n");
|
||||
#ifdef _WIN32
|
||||
atomic_write(staging / "Windows-Runtime.txt",
|
||||
@@ -567,6 +619,9 @@ struct BuildService::Impl {
|
||||
"--scene", path_to_utf8(staging / "scene.fscene"), "--assets",
|
||||
path_to_utf8(staging)},
|
||||
staging);
|
||||
if (job.lua.enabled() &&
|
||||
scripting::loadLuaProject(staging).fingerprint != job.lua.fingerprint)
|
||||
throw std::runtime_error("Packaged Lua snapshot changed during validation");
|
||||
Json files = Json::array();
|
||||
for (const auto& entry : fs::recursive_directory_iterator(staging)) {
|
||||
if (entry.is_symlink())
|
||||
@@ -581,6 +636,8 @@ struct BuildService::Impl {
|
||||
{"version", 1},
|
||||
{"generation", job.status.id},
|
||||
{"build_fingerprint", built.at("fingerprint")},
|
||||
{"lua_enabled", job.lua.enabled()},
|
||||
{"lua_fingerprint", job.lua.fingerprint},
|
||||
{"scene_hash", sha256(job.scene.dump())},
|
||||
{"asset_generations", Json::object()},
|
||||
{"configuration", built.at("configuration")},
|
||||
|
||||
@@ -180,6 +180,8 @@ struct EditorUI::Impl {
|
||||
std::string attempted_theme, attempted_layout, applied_theme, applied_layout,
|
||||
presentation_error;
|
||||
std::chrono::steady_clock::time_point last_presentation_poll{};
|
||||
std::chrono::steady_clock::time_point last_schema_poll{};
|
||||
Json schema_state;
|
||||
bool simulation_open = false;
|
||||
bool project_settings_open = false;
|
||||
Json project_settings_state;
|
||||
@@ -435,7 +437,7 @@ struct EditorUI::Impl {
|
||||
button(
|
||||
toolbar, "step", "Step", [this] { call("faset_play_control", {{"command", "step"}}); },
|
||||
55);
|
||||
button(toolbar, "build", "Build C++", [this] { call("faset_build"); }, 94);
|
||||
button(toolbar, "build", "Build", [this] { call("faset_build"); }, 94);
|
||||
button(
|
||||
toolbar, "export", "Export",
|
||||
[this] { call("faset_export", {{"document", document}, {"output", "Exports"}}); }, 64);
|
||||
@@ -510,7 +512,7 @@ struct EditorUI::Impl {
|
||||
assetbar.layout.height = 30;
|
||||
assetbar.layout.padding = 2;
|
||||
assetbar.layout.gap = 5;
|
||||
label(assetbar, "asset-path", "Project assets", 135);
|
||||
label(assetbar, "asset-path", "Project files", 135);
|
||||
auto& search = assetbar.add(Kind::TextField, "asset-search", "");
|
||||
search.layout.width = 220;
|
||||
search.on_preview = [this](Widget& w) {
|
||||
@@ -1093,6 +1095,12 @@ struct EditorUI::Impl {
|
||||
void open_source() {
|
||||
if (source_file.empty())
|
||||
return;
|
||||
if (path_from_utf8(source_file).extension() == ".lua") {
|
||||
const auto result = call("faset_script_open", {{"path", source_file}});
|
||||
if (!result.is_null())
|
||||
status = "Opened in external editor: " + source_file;
|
||||
return;
|
||||
}
|
||||
auto result = call("faset_document_open", {{"path", source_file}});
|
||||
if (!result.is_null())
|
||||
choose_document(result.at("id"));
|
||||
@@ -1167,15 +1175,20 @@ struct EditorUI::Impl {
|
||||
ui.find("pause")->enabled = session.playing();
|
||||
ui.find("step")->enabled = session.playing();
|
||||
ui.find("pause")->text = paused ? "Resume" : "Pause";
|
||||
const auto schema_state = call("faset_schema_status");
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
// Source fingerprints read complete script bytes; do not hash them every render frame.
|
||||
if (schema_state.is_null() || now - last_schema_poll >= std::chrono::seconds(1)) {
|
||||
schema_state = call("faset_schema_status");
|
||||
last_schema_poll = now;
|
||||
}
|
||||
if (!schema_state.is_null()) {
|
||||
const bool stale = schema_state.value("stale", false);
|
||||
ui.find("build")->text = stale ? "Build C++ !" : "Build C++";
|
||||
ui.find("build")->text = stale ? "Build !" : "Build";
|
||||
ui.find("build")->tooltip =
|
||||
stale ? "Gameplay schema is stale: " + schema_state.value("error", std::string())
|
||||
: "Build gameplay and refresh metadata";
|
||||
: "Incremental gameplay build and C++/Lua Inspector metadata refresh";
|
||||
if (stale && status == "Ready")
|
||||
status = "Gameplay schema is stale; Build C++ to refresh";
|
||||
status = "Gameplay schema is stale; Build to refresh Inspector metadata";
|
||||
}
|
||||
|
||||
ui.find("project-title")->text = session.project().value("name", std::string("Project")) +
|
||||
@@ -1604,7 +1617,7 @@ struct EditorUI::Impl {
|
||||
last_assets = now;
|
||||
files = Json::array();
|
||||
try {
|
||||
for (const std::string folder : {"Assets", "Scenes"}) {
|
||||
for (const std::string folder : {"Assets", "Scenes", "Scripts"}) {
|
||||
const auto directory = session.config().project_root / folder;
|
||||
if (!std::filesystem::exists(directory))
|
||||
continue;
|
||||
@@ -1615,6 +1628,8 @@ struct EditorUI::Impl {
|
||||
break;
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
if (folder == "Scripts" && entry.path().extension() != ".lua")
|
||||
continue;
|
||||
auto relative = generic_path_to_utf8(
|
||||
std::filesystem::relative(entry.path(), session.config().project_root));
|
||||
if (relative.find(".faset-") != std::string::npos)
|
||||
@@ -1636,6 +1651,11 @@ struct EditorUI::Impl {
|
||||
assets = result.at("assets");
|
||||
}
|
||||
auto& list = *ui.find("asset-items");
|
||||
const bool script = path_from_utf8(source_file).extension() == ".lua";
|
||||
ui.find("asset-open")->text = script ? "Open Script" : "Open Scene";
|
||||
ui.find("asset-open")->tooltip =
|
||||
script ? "Open Lua source in your external editor" : "Open a project scene";
|
||||
ui.find("asset-import")->enabled = !script;
|
||||
std::set<std::string> keep;
|
||||
for (const auto& file : files) {
|
||||
const auto path = file.get<std::string>();
|
||||
@@ -1683,7 +1703,8 @@ struct EditorUI::Impl {
|
||||
keep.insert(row.id);
|
||||
}
|
||||
if (keep.empty()) {
|
||||
label(list, "assets-empty", "Place GLB, glTF, PNG or JPEG in Assets, then Import.");
|
||||
label(list, "assets-empty",
|
||||
"Import images/models from Assets, or open Lua sources from Scripts.");
|
||||
keep.insert("assets-empty");
|
||||
}
|
||||
trim_children(list, keep);
|
||||
|
||||
+109
-7
@@ -3,6 +3,7 @@
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/editor/session.hpp>
|
||||
#include <faset/scripting/project.hpp>
|
||||
|
||||
namespace faset::editor {
|
||||
namespace {
|
||||
@@ -130,24 +131,32 @@ Json Session::assets_list() const {
|
||||
return {{"assets", list}};
|
||||
}
|
||||
std::string Session::source_signature() const {
|
||||
const auto lua = scripting::loadLuaProject(config_.project_root);
|
||||
const auto directory = config_.project_root / "Scripts";
|
||||
std::vector<std::filesystem::path> files;
|
||||
if (std::filesystem::exists(directory))
|
||||
for (const auto& file : std::filesystem::recursive_directory_iterator(directory))
|
||||
if (file.is_regular_file())
|
||||
if (file.is_regular_file() && (!lua.enabled() || file.path().extension() != ".lua"))
|
||||
files.push_back(file.path());
|
||||
std::sort(files.begin(), files.end());
|
||||
std::string contents;
|
||||
for (const auto& file : files)
|
||||
contents += generic_path_to_utf8(file.lexically_relative(directory)) + ":" +
|
||||
sha256_file(file) + "\n";
|
||||
if (lua.enabled())
|
||||
contents += "lua:" + lua.fingerprint + "\n";
|
||||
return sha256(contents);
|
||||
}
|
||||
Json Session::schema_status() const {
|
||||
return {{"loaded", schema_loaded_},
|
||||
{"stale", !schema_loaded_ || schema_source_signature_ != source_signature() ||
|
||||
!schema_error_.empty()},
|
||||
{"error", schema_error_}};
|
||||
try {
|
||||
const auto signature = source_signature();
|
||||
return {{"loaded", schema_loaded_},
|
||||
{"stale", !schema_loaded_ || schema_source_signature_ != signature ||
|
||||
!schema_error_.empty()},
|
||||
{"error", schema_error_}};
|
||||
} catch (const std::exception& error) {
|
||||
return {{"loaded", schema_loaded_}, {"stale", true}, {"error", error.what()}};
|
||||
}
|
||||
}
|
||||
Json Session::jobs() const {
|
||||
Json list = Json::array();
|
||||
@@ -185,9 +194,14 @@ void Session::launch_player(Json scene, const std::filesystem::path& executable)
|
||||
options.arguments = {
|
||||
path_to_utf8(executable), "--scene", path_to_utf8(snapshot), "--assets",
|
||||
path_to_utf8(assets_.cache_root()), "--control", path_to_utf8(control_path_)};
|
||||
if (scripting::loadLuaProject(config_.project_root).enabled()) {
|
||||
options.arguments.insert(options.arguments.end(),
|
||||
{"--project", path_to_utf8(config_.project_root), "--watch-lua"});
|
||||
}
|
||||
options.working_directory = config_.project_root;
|
||||
player_ = std::make_unique<Process>(options);
|
||||
log("Play started in a separate Player process");
|
||||
log("Play started in a separate Player process; Lua projects reload changed scripts and reset "
|
||||
"simulation state automatically");
|
||||
}
|
||||
void Session::stop_player() {
|
||||
if (!pending_play_job_.empty()) {
|
||||
@@ -439,7 +453,7 @@ void Session::register_commands() {
|
||||
"Failed builds retain metadata but mark it stale.",
|
||||
schema(Json::object()), [&](const Json&) { return schema_status(); }, true);
|
||||
commands_.add("faset_build",
|
||||
"Incrementally compile C++ gameplay and export its metadata in separate native "
|
||||
"Incrementally compile gameplay and export C++/Lua metadata in separate native "
|
||||
"processes. Returns a job ID.",
|
||||
schema(Json::object()), [&](const Json&) {
|
||||
const auto signature = source_signature();
|
||||
@@ -447,6 +461,94 @@ void Session::register_commands() {
|
||||
submitted_sources_[id] = signature;
|
||||
return Json{{"job", id}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_lua_refresh",
|
||||
"Validate Lua behavior schemas in a separate process and refresh Inspector metadata. "
|
||||
"Uses the incremental build; script edits do not require C++ compilation. Returns a job "
|
||||
"ID.",
|
||||
schema(Json::object()), [&](const Json&) {
|
||||
require(scripting::loadLuaProject(config_.project_root).enabled(), "lua.disabled",
|
||||
"Declare scripting.lua.scripts in project.faset.json first");
|
||||
const auto signature = source_signature();
|
||||
const auto id = builds_.start_build();
|
||||
submitted_sources_[id] = signature;
|
||||
return Json{{"job", id}};
|
||||
});
|
||||
commands_.add("faset_lua_reload",
|
||||
"Reload Lua in the running development Player. Successful reload resets the Play "
|
||||
"snapshot and script state; invalid edits keep the current running version.",
|
||||
schema(Json::object()), [&](const Json&) {
|
||||
require(bool(player_), "play.not_running", "Player is not running");
|
||||
require(scripting::loadLuaProject(config_.project_root).enabled(),
|
||||
"lua.disabled", "The current project has no Lua behaviors");
|
||||
atomic_write_json(control_path_, {{"sequence", ++control_sequence_},
|
||||
{"command", "reload-lua"}});
|
||||
return Json{{"queued", true}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_lua_setup",
|
||||
"Install Faset LuaLS type annotations in .faset/lua and create .luarc.json only if absent. "
|
||||
"Existing user language-server configuration is never overwritten.",
|
||||
schema(Json::object()), [&](const Json&) {
|
||||
const auto source = config_.engine_root / "tools/lua";
|
||||
const auto annotations = project_path(config_.project_root, ".faset/lua/faset.lua");
|
||||
const auto configuration = project_path(config_.project_root, ".luarc.json");
|
||||
atomic_write(annotations, read_text(source / "faset.lua"));
|
||||
const bool create_configuration = !std::filesystem::exists(configuration);
|
||||
if (create_configuration)
|
||||
atomic_write_json(configuration, read_json(source / "luarc.json"));
|
||||
log(create_configuration
|
||||
? "LuaLS configured: .luarc.json and .faset/lua/faset.lua"
|
||||
: "LuaLS annotations updated; existing .luarc.json preserved. Add .faset/lua "
|
||||
"to workspace.library if needed");
|
||||
return Json{{"annotations", ".faset/lua/faset.lua"},
|
||||
{"configuration_created", create_configuration}};
|
||||
});
|
||||
commands_.add(
|
||||
"faset_script_open",
|
||||
"Open a project Lua source in an external editor, never as an executable. Optional editor "
|
||||
"is an argv array (default: project editor.script_editor, then zed). Exact {file} and "
|
||||
"{project} arguments are replaced; no shell expansion is performed.",
|
||||
schema({{"path", text}, {"editor", {{"type", "array"}, {"items", text}, {"minItems", 1}}}},
|
||||
{"path"}),
|
||||
[&](const Json& args) {
|
||||
const auto file = project_path(config_.project_root,
|
||||
path_from_utf8(args.at("path").get<std::string>()));
|
||||
require(file.extension() == ".lua" && std::filesystem::is_regular_file(file),
|
||||
"lua.source", "Select an existing .lua file inside the project");
|
||||
Json command = Json::array({"zed", "{file}"});
|
||||
const auto settings = project();
|
||||
if (settings.contains("editor") && settings.at("editor").is_object() &&
|
||||
settings.at("editor").contains("script_editor"))
|
||||
command = settings.at("editor").at("script_editor");
|
||||
if (args.contains("editor"))
|
||||
command = args.at("editor");
|
||||
require(command.is_array() && !command.empty(), "lua.editor",
|
||||
"Configure editor.script_editor as a nonempty executable/argument array");
|
||||
std::vector<std::string> arguments;
|
||||
bool has_file = false;
|
||||
for (const auto& part : command) {
|
||||
require(part.is_string(), "lua.editor", "Editor arguments must be strings");
|
||||
auto argument = part.get<std::string>();
|
||||
require(argument.find('\0') == std::string::npos, "lua.editor",
|
||||
"Editor arguments cannot contain NUL");
|
||||
if (argument == "{file}") {
|
||||
require(!arguments.empty(), "lua.editor", "The first argument is the editor");
|
||||
argument = path_to_utf8(file);
|
||||
has_file = true;
|
||||
} else if (argument == "{project}") {
|
||||
require(!arguments.empty(), "lua.editor", "The first argument is the editor");
|
||||
argument = path_to_utf8(std::filesystem::absolute(config_.project_root));
|
||||
}
|
||||
arguments.push_back(std::move(argument));
|
||||
}
|
||||
require(!arguments.front().empty(), "lua.editor", "Choose an editor executable");
|
||||
if (!has_file)
|
||||
arguments.push_back(path_to_utf8(file));
|
||||
launch_detached(arguments, config_.project_root);
|
||||
log("Opened Lua source in external editor: " + args.at("path").get<std::string>());
|
||||
return Json{{"opened", args.at("path")}};
|
||||
});
|
||||
commands_.add("faset_export",
|
||||
"Build, validate and export a resolved authoring snapshot to a project-relative "
|
||||
"output directory. Returns a job ID.",
|
||||
|
||||
Reference in New Issue
Block a user