Show source-linked diagnostics and expandable build logs

This commit is contained in:
Emil
2026-09-24 02:01:47 +03:00
parent cf5b702233
commit d3217838cb
2 changed files with 110 additions and 2 deletions
+23
View File
@@ -1,5 +1,28 @@
# Developer diagnostics
## Build errors and source navigation
Build and export jobs report compiler, clang-cl/MSVC, and Lua errors as structured
Console rows. Each row shows severity, message, and a one-based source location.
Choose **Open source** to open a file under the current project's `Scripts/`
directory at that line and column. Diagnostics from engine or external files remain
visible but have no source action. The **Jobs** tab shows whether a verified build
generation was reused, the elapsed phases, and an expandable raw process log;
**Copy full log** retains the complete available output for troubleshooting.
The same action is available over Editor MCP:
```json
{"command":"faset_source_open","arguments":{"path":"Scripts/Gameplay.cpp","line":17,"column":4}}
```
`faset_script_open` remains Lua-only. Both commands use the project's optional
`editor.script_editor` argv array, or Zed by default. The tokens `{file}`,
`{line}`, `{column}`, and `{project}` may appear within one argument, for
example `["zed", "{file}:{line}:{column}"]`. Faset passes each argument
literally to the editor process without shell expansion. An invalid editor
command produces a source-editor error without changing the build result.
The optional Dear ImGui overlay shows renderer counters inside the Editor. The Editor's normal interface remains the retained Faset UI. Enable the diagnostic build explicitly:
```sh
+87 -2
View File
@@ -168,7 +168,7 @@ struct EditorUI::Impl {
Json instance_selection = Json::array(), template_conflicts = Json::array();
std::string document, selected, source_file, asset_filter,
active_bottom = "assets", menu, command_name = "faset_documents", status = "Ready",
gizmo_mode = "Move";
gizmo_mode = "Move", expanded_job_log;
Json current, resolved, files = Json::array(), assets = Json::array(), schemas = Json::object(),
recovery = Json::array();
std::uint64_t shown_revision = std::numeric_limits<std::uint64_t>::max();
@@ -2043,6 +2043,45 @@ struct EditorUI::Impl {
trim_children(conflict_list, conflict_keep);
auto& console = *ui.find("console-items");
std::set<std::string> keep;
std::size_t shown_diagnostics{};
for (const auto& job : all_jobs) {
const auto id = job.at("id").get<std::string>();
const auto diagnostics = job.value("diagnostics", Json::array());
if (!diagnostics.is_array())
continue;
for (std::size_t index = 0; index < diagnostics.size() && shown_diagnostics < 60;
++index, ++shown_diagnostics) {
const auto& diagnostic = diagnostics[index];
const auto row_id = "diagnostic-" + id + "-" + std::to_string(index);
auto& row = console.add(Kind::Row, row_id);
row.layout.height = 27;
row.layout.gap = 8;
keep.insert(row_id);
const auto severity = diagnostic.value("severity", std::string("error"));
label(row, row_id + "-severity", severity, 68);
auto message = diagnostic.value("message", std::string());
auto& detail = row.add(Kind::Label, row_id + "-message", message);
detail.layout.flex = 1;
detail.tooltip = diagnostic.value("phase", std::string()) + ": " + message;
const auto file = diagnostic.value("file", std::string());
const auto line = diagnostic.value("line", 1);
const auto column = diagnostic.value("column", 1);
const auto location = file.empty()
? std::string("—")
: file + ":" + std::to_string(line) + ":" +
std::to_string(column);
label(row, row_id + "-location", location, 235);
auto& open = button(
row, row_id + "-open", "Open source",
[this, file, line, column] {
call("faset_source_open",
{{"path", file}, {"line", line}, {"column", column}});
},
115);
open.enabled = !file.empty();
open.appearance = ui::Appearance::Quiet;
}
}
const auto& logs = session.logs();
for (std::size_t i = logs.size() > 150 ? logs.size() - 150 : 0; i < logs.size(); ++i) {
std::string line = logs[i];
@@ -2065,11 +2104,25 @@ struct EditorUI::Impl {
row.layout.height = 28;
keep.insert(row.id);
const auto state = job.value("state", std::string());
const auto result = job.value("result", Json::object());
std::string reuse;
if (result.is_object() && result.value("schema_cache_hit", false))
reuse = " · verified reuse";
if (result.is_object() && result.contains("phase_times_ms") &&
result.at("phase_times_ms").is_object())
reuse += " · " +
std::to_string(result.at("phase_times_ms").value("total", 0)) + " ms";
auto& text = row.add(Kind::Label, "job-text-" + id,
job.value("kind", std::string("Job")) + " · " + state + " · " +
job.value("stage", std::string()) + " " +
std::to_string(int(job.value("progress", 0.0) * 100)) + "%");
std::to_string(int(job.value("progress", 0.0) * 100)) + "%" +
reuse);
text.layout.flex = 1;
auto& log_button = button(
row, "job-log-toggle-" + id,
expanded_job_log == id ? "Hide log" : "View log",
[this, id] { expanded_job_log = expanded_job_log == id ? "" : id; }, 86);
log_button.appearance = ui::Appearance::Quiet;
auto& cancel = button(
row, "job-cancel-" + id, "Cancel",
[this, id] { call("faset_job_cancel", {{"id", id}}); }, 72);
@@ -2078,6 +2131,38 @@ struct EditorUI::Impl {
row, "job-review-" + id, "Review removals", [this] { active_bottom = "conflicts"; },
135);
review.visible = job.value("kind", std::string()) == "import" && state == "conflict";
if (expanded_job_log == id) {
const auto panel_id = "job-log-panel-" + id;
auto& panel = jobs.add(Kind::Column, panel_id);
panel.layout.height = 170;
panel.layout.scroll = true;
panel.layout.gap = 1;
keep.insert(panel_id);
std::set<std::string> log_keep;
const auto raw = job.value("log", std::string());
auto& copy = button(panel, panel_id + "-copy", "Copy full log",
[this, raw] { renderer.set_clipboard(raw); }, 110);
copy.appearance = ui::Appearance::Quiet;
log_keep.insert(copy.id);
std::vector<std::string> lines;
std::size_t begin{};
while (begin < raw.size()) {
const auto end = raw.find('\n', begin);
lines.push_back(raw.substr(begin, end == std::string::npos
? std::string::npos
: end - begin));
if (end == std::string::npos)
break;
begin = end + 1;
}
const auto first = lines.size() > 100 ? lines.size() - 100 : 0;
for (std::size_t index = first; index < lines.size(); ++index) {
const auto line_id = panel_id + "-line-" + std::to_string(index);
label(panel, line_id, lines[index].substr(0, 600));
log_keep.insert(line_id);
}
trim_children(panel, log_keep);
}
}
if (keep.empty()) {
label(jobs, "jobs-empty", "No active import, build or export jobs.");