Open project C++ and Lua sources at diagnostic locations
This commit is contained in:
+94
-43
@@ -502,51 +502,102 @@ void Session::register_commands() {
|
|||||||
{"configuration_created", create_configuration},
|
{"configuration_created", create_configuration},
|
||||||
{"scripts_configuration_created", create_scripts_configuration}};
|
{"scripts_configuration_created", create_scripts_configuration}};
|
||||||
});
|
});
|
||||||
commands_.add(
|
const auto open_source = [this](const Json& args, bool lua_only) -> Json {
|
||||||
"faset_script_open",
|
const auto path_error = lua_only ? "lua.source" : "source.path";
|
||||||
"Open a project Lua source in an external editor, never as an executable. Optional editor "
|
const auto editor_error = lua_only ? "lua.editor" : "source.editor";
|
||||||
"is an argv array (default: project editor.script_editor, then zed). Exact {file} and "
|
const auto requested = path_from_utf8(args.at("path").get<std::string>());
|
||||||
"{project} arguments are replaced; no shell expansion is performed.",
|
require(!requested.empty() && !requested.is_absolute(), path_error,
|
||||||
schema({{"path", text}, {"editor", {{"type", "array"}, {"items", text}, {"minItems", 1}}}},
|
"Choose an existing source inside Scripts");
|
||||||
{"path"}),
|
for (const auto& part : requested)
|
||||||
[&](const Json& args) {
|
require(part != "..", path_error, "Source path cannot traverse outside Scripts");
|
||||||
const auto file = project_path(config_.project_root,
|
std::filesystem::path file;
|
||||||
path_from_utf8(args.at("path").get<std::string>()));
|
try {
|
||||||
require(file.extension() == ".lua" && std::filesystem::is_regular_file(file),
|
file = project_path(config_.project_root, requested);
|
||||||
"lua.source", "Select an existing .lua file inside the project");
|
} catch (const std::exception&) {
|
||||||
Json command = Json::array({"zed", "{file}"});
|
throw Error(path_error, "Source path must stay inside project Scripts");
|
||||||
const auto settings = project();
|
}
|
||||||
if (settings.contains("editor") && settings.at("editor").is_object() &&
|
const auto project_root = std::filesystem::weakly_canonical(config_.project_root);
|
||||||
settings.at("editor").contains("script_editor"))
|
const auto relative = file.lexically_relative(project_root);
|
||||||
command = settings.at("editor").at("script_editor");
|
const auto name = generic_path_to_utf8(relative);
|
||||||
if (args.contains("editor"))
|
const auto extension = path_to_utf8(file.extension());
|
||||||
command = args.at("editor");
|
const bool supported = extension == ".cpp" || extension == ".hpp" ||
|
||||||
require(command.is_array() && !command.empty(), "lua.editor",
|
extension == ".h" || extension == ".cc" ||
|
||||||
"Configure editor.script_editor as a nonempty executable/argument array");
|
extension == ".cxx" || extension == ".lua";
|
||||||
std::vector<std::string> arguments;
|
require(name.starts_with("Scripts/") && supported &&
|
||||||
bool has_file = false;
|
(!lua_only || extension == ".lua") &&
|
||||||
for (const auto& part : command) {
|
std::filesystem::is_regular_file(file),
|
||||||
require(part.is_string(), "lua.editor", "Editor arguments must be strings");
|
path_error, "Choose an existing source file inside Scripts");
|
||||||
auto argument = part.get<std::string>();
|
const auto line = args.value("line", 1);
|
||||||
require(argument.find('\0') == std::string::npos, "lua.editor",
|
const auto column = args.value("column", 1);
|
||||||
"Editor arguments cannot contain NUL");
|
require(line > 0 && column > 0, path_error, "Source line and column must be positive");
|
||||||
if (argument == "{file}") {
|
Json command = Json::array({"zed", "{file}:{line}:{column}"});
|
||||||
require(!arguments.empty(), "lua.editor", "The first argument is the editor");
|
const auto settings = project();
|
||||||
argument = path_to_utf8(file);
|
if (settings.contains("editor") && settings.at("editor").is_object() &&
|
||||||
has_file = true;
|
settings.at("editor").contains("script_editor"))
|
||||||
} else if (argument == "{project}") {
|
command = settings.at("editor").at("script_editor");
|
||||||
require(!arguments.empty(), "lua.editor", "The first argument is the editor");
|
if (args.contains("editor"))
|
||||||
argument = path_to_utf8(std::filesystem::absolute(config_.project_root));
|
command = args.at("editor");
|
||||||
}
|
require(command.is_array() && !command.empty(), editor_error,
|
||||||
arguments.push_back(std::move(argument));
|
"Configure editor.script_editor as a nonempty executable/argument array");
|
||||||
|
auto replace = [](std::string& target, std::string_view token,
|
||||||
|
const std::string& replacement) {
|
||||||
|
bool found = false;
|
||||||
|
std::size_t position = 0;
|
||||||
|
while ((position = target.find(token, position)) != std::string::npos) {
|
||||||
|
target.replace(position, token.size(), replacement);
|
||||||
|
position += replacement.size();
|
||||||
|
found = true;
|
||||||
}
|
}
|
||||||
require(!arguments.front().empty(), "lua.editor", "Choose an editor executable");
|
return found;
|
||||||
if (!has_file)
|
};
|
||||||
arguments.push_back(path_to_utf8(file));
|
std::vector<std::string> arguments;
|
||||||
|
bool has_file = false;
|
||||||
|
for (const auto& part : command) {
|
||||||
|
require(part.is_string(), editor_error, "Editor arguments must be strings");
|
||||||
|
auto argument = part.get<std::string>();
|
||||||
|
require(argument.find('\0') == std::string::npos, editor_error,
|
||||||
|
"Editor arguments cannot contain NUL");
|
||||||
|
if (arguments.empty())
|
||||||
|
require(!argument.empty() && argument.find('{') == std::string::npos,
|
||||||
|
editor_error, "The first argument must be an editor executable");
|
||||||
|
else {
|
||||||
|
has_file |= replace(argument, "{file}", path_to_utf8(file));
|
||||||
|
replace(argument, "{line}", std::to_string(line));
|
||||||
|
replace(argument, "{column}", std::to_string(column));
|
||||||
|
replace(argument, "{project}", path_to_utf8(project_root));
|
||||||
|
}
|
||||||
|
arguments.push_back(std::move(argument));
|
||||||
|
}
|
||||||
|
if (!has_file)
|
||||||
|
arguments.push_back(path_to_utf8(file));
|
||||||
|
try {
|
||||||
launch_detached(arguments, config_.project_root);
|
launch_detached(arguments, config_.project_root);
|
||||||
log("Opened Lua source in external editor: " + args.at("path").get<std::string>());
|
} catch (const std::exception& error) {
|
||||||
return Json{{"opened", args.at("path")}};
|
throw Error(editor_error, "Could not launch external source editor",
|
||||||
});
|
{{"reason", error.what()}});
|
||||||
|
}
|
||||||
|
log("Opened source in external editor: " + name);
|
||||||
|
return {{"opened", name}, {"path", name}, {"line", line}, {"column", column}};
|
||||||
|
};
|
||||||
|
const auto source_schema = schema(
|
||||||
|
{{"path", text},
|
||||||
|
{"line", {{"type", "integer"}, {"minimum", 1}}},
|
||||||
|
{"column", {{"type", "integer"}, {"minimum", 1}}},
|
||||||
|
{"editor", {{"type", "array"}, {"items", text}, {"minItems", 1}}}},
|
||||||
|
{"path"});
|
||||||
|
commands_.add("faset_source_open",
|
||||||
|
"Open a C++ or Lua source under Scripts at a one-based line and column. "
|
||||||
|
"The editor is an argv array; {file}, {line}, {column} and {project} are "
|
||||||
|
"substituted without a shell.",
|
||||||
|
source_schema, [open_source](const Json& args) {
|
||||||
|
return open_source(args, false);
|
||||||
|
});
|
||||||
|
commands_.add("faset_script_open",
|
||||||
|
"Open a project Lua source in an external editor. This compatibility command "
|
||||||
|
"retains Lua-only validation and uses the same safe source launcher.",
|
||||||
|
source_schema, [open_source](const Json& args) {
|
||||||
|
return open_source(args, true);
|
||||||
|
});
|
||||||
commands_.add("faset_export",
|
commands_.add("faset_export",
|
||||||
"Build, validate and export a resolved authoring snapshot to a project-relative "
|
"Build, validate and export a resolved authoring snapshot to a project-relative "
|
||||||
"output directory. Returns a job ID.",
|
"output directory. Returns a job ID.",
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
#include <faset/editor/session.hpp>
|
#include <faset/editor/session.hpp>
|
||||||
|
#include <chrono>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
int main() {
|
int main(int argc, char** argv) {
|
||||||
using namespace faset;
|
using namespace faset;
|
||||||
|
if (argc >= 3 && std::string_view(argv[1]) == "--editor-probe") {
|
||||||
|
Json arguments = Json::array();
|
||||||
|
for (int index = 3; index < argc; ++index)
|
||||||
|
arguments.push_back(argv[index]);
|
||||||
|
atomic_write_json(path_from_utf8(argv[2]), arguments);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const auto root = std::filesystem::temp_directory_path() /
|
const auto root = std::filesystem::temp_directory_path() /
|
||||||
path_from_utf8("Faset Café 世界 session " + new_id());
|
path_from_utf8("Faset Café 世界 session " + new_id());
|
||||||
try {
|
try {
|
||||||
@@ -115,6 +124,32 @@ int main() {
|
|||||||
{{"path", "Scripts/behavior.lua"}, {"editor", {"{file}"}}}, "lua.editor");
|
{{"path", "Scripts/behavior.lua"}, {"editor", {"{file}"}}}, "lua.editor");
|
||||||
reject_command("faset_script_open", {{"path", "Scripts/behavior.lua"}, {"editor", {""}}},
|
reject_command("faset_script_open", {{"path", "Scripts/behavior.lua"}, {"editor", {""}}},
|
||||||
"lua.editor");
|
"lua.editor");
|
||||||
|
const auto probe = root / "editor-argv.json";
|
||||||
|
const auto opened = commands.call(
|
||||||
|
"faset_source_open",
|
||||||
|
{{"path", "Scripts/Gameplay.cpp"}, {"line", 17}, {"column", 4},
|
||||||
|
{"editor", Json::array({path_to_utf8(std::filesystem::absolute(path_from_utf8(argv[0]))),
|
||||||
|
"--editor-probe", path_to_utf8(probe),
|
||||||
|
"{file}:{line}:{column}", "{project}"})}});
|
||||||
|
for (int attempt = 0; attempt < 100 && !std::filesystem::exists(probe); ++attempt)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
require(opened.at("path") == "Scripts/Gameplay.cpp" && opened.at("line") == 17 &&
|
||||||
|
std::filesystem::is_regular_file(probe) &&
|
||||||
|
read_json(probe) ==
|
||||||
|
Json::array({path_to_utf8(root / "Scripts/Gameplay.cpp") + ":17:4",
|
||||||
|
path_to_utf8(root)}),
|
||||||
|
"test", "Source navigation passes a Unicode path and location as literal argv");
|
||||||
|
reject_command("faset_source_open", {{"path", "../outside.cpp"}}, "source.path");
|
||||||
|
reject_command("faset_source_open", {{"path", "Scripts/missing.cpp"}}, "source.path");
|
||||||
|
reject_command("faset_source_open", {{"path", "Scripts"}}, "source.path");
|
||||||
|
atomic_write(root / "Scripts/tool.exe", "not a source\n");
|
||||||
|
reject_command("faset_source_open", {{"path", "Scripts/tool.exe"}}, "source.path");
|
||||||
|
std::filesystem::create_directories(root / "Outside");
|
||||||
|
std::error_code link_error;
|
||||||
|
std::filesystem::create_symlink(root / "Outside/secret.cpp",
|
||||||
|
root / "Scripts/escape.cpp", link_error);
|
||||||
|
if (!link_error)
|
||||||
|
reject_command("faset_source_open", {{"path", "Scripts/escape.cpp"}}, "source.path");
|
||||||
auto bad_lua_project = session.project();
|
auto bad_lua_project = session.project();
|
||||||
bad_lua_project["scripting"] = {{"lua", {{"scripts", {"Scripts/missing.lua"}}}}};
|
bad_lua_project["scripting"] = {{"lua", {{"scripts", {"Scripts/missing.lua"}}}}};
|
||||||
atomic_write_json(root / "project.faset.json", bad_lua_project);
|
atomic_write_json(root / "project.faset.json", bad_lua_project);
|
||||||
|
|||||||
Reference in New Issue
Block a user