From da8212746cdd082f52233450c807c4a98ed900b5 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 15:31:15 -0700 Subject: [PATCH] Step 126: build system detection and error jump --- editor/CMakeLists.txt | 4 ++ editor/src/BuildSystem.h | 131 ++++++++++++++++++++++++++++++++++ editor/src/EditorState.h | 23 ++++++ editor/src/TerminalPanel.h | 8 +++ editor/src/main.cpp | 47 ++++++++++++ editor/tests/step126_test.cpp | 40 +++++++++++ sprint4_plan.md | 2 +- 7 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 editor/src/BuildSystem.h create mode 100644 editor/tests/step126_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f7eedc0..267232e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -716,6 +716,10 @@ target_link_libraries(step125_integration_test PRIVATE tree_sitter_cpp tree_sitter_elisp) +add_executable(step126_test tests/step126_test.cpp) +target_include_directories(step126_test PRIVATE src) +target_link_libraries(step126_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/BuildSystem.h b/editor/src/BuildSystem.h new file mode 100644 index 0000000..2c89b18 --- /dev/null +++ b/editor/src/BuildSystem.h @@ -0,0 +1,131 @@ +#pragma once +#include +#include +#include +#include + +struct BuildCommand { + std::string label; + std::string command; +}; + +struct BuildError { + std::string file; + int line = 0; + int col = 0; + std::string message; +}; + +class BuildSystem { +public: + enum class Type { + None, + CMake, + Python, + Cargo, + Npm, + Make, + Go + }; + + static Type detect(const std::string& root) { + std::filesystem::path p(root); + if (std::filesystem::exists(p / "CMakeLists.txt")) return Type::CMake; + if (std::filesystem::exists(p / "setup.py")) return Type::Python; + if (std::filesystem::exists(p / "Cargo.toml")) return Type::Cargo; + if (std::filesystem::exists(p / "package.json")) return Type::Npm; + if (std::filesystem::exists(p / "Makefile")) return Type::Make; + if (std::filesystem::exists(p / "go.mod")) return Type::Go; + return Type::None; + } + + static std::vector commandsFor(Type type) { + switch (type) { + case Type::CMake: + return {{"Build", "cmake --build ."}}; + case Type::Python: + return {{"Build", "python setup.py build"}}; + case Type::Cargo: + return {{"Build", "cargo build"}}; + case Type::Npm: + return {{"Build", "npm run build"}}; + case Type::Make: + return {{"Build", "make"}}; + case Type::Go: + return {{"Build", "go build ./..."}}; + default: + return {}; + } + } + + static const char* typeName(Type type) { + switch (type) { + case Type::CMake: return "CMake"; + case Type::Python: return "Python"; + case Type::Cargo: return "Cargo"; + case Type::Npm: return "NPM"; + case Type::Make: return "Make"; + case Type::Go: return "Go"; + default: return "None"; + } + } + + static std::vector parseErrors(const std::string& output) { + std::vector errors; + std::istringstream iss(output); + std::string line; + while (std::getline(iss, line)) { + BuildError err; + if (parseGccClang(line, err) || parseMsvc(line, err)) { + errors.push_back(err); + } + } + return errors; + } + +private: + static bool parseGccClang(const std::string& line, BuildError& out) { + size_t p1 = line.find(':'); + size_t p2 = p1 == std::string::npos ? std::string::npos : line.find(':', p1 + 1); + size_t p3 = p2 == std::string::npos ? std::string::npos : line.find(':', p2 + 1); + if (p1 == std::string::npos || p2 == std::string::npos || p3 == std::string::npos) return false; + std::string file = line.substr(0, p1); + std::string linePart = line.substr(p1 + 1, p2 - p1 - 1); + std::string colPart = line.substr(p2 + 1, p3 - p2 - 1); + if (!isNumber(linePart) || !isNumber(colPart)) return false; + int lineNum = std::atoi(linePart.c_str()); + int colNum = std::atoi(colPart.c_str()); + std::string rest = line.substr(p3 + 1); + if (rest.find("error") == std::string::npos) return false; + out.file = file; + out.line = lineNum; + out.col = colNum; + out.message = rest; + return true; + } + + static bool parseMsvc(const std::string& line, BuildError& out) { + size_t p1 = line.find('('); + size_t p2 = line.find(')', p1 + 1); + size_t p3 = line.find("error", p2 == std::string::npos ? 0 : p2); + if (p1 == std::string::npos || p2 == std::string::npos || p3 == std::string::npos) return false; + std::string file = line.substr(0, p1); + std::string linePart = line.substr(p1 + 1, p2 - p1 - 1); + size_t comma = linePart.find(','); + if (comma != std::string::npos) linePart = linePart.substr(0, comma); + int lineNum = std::atoi(linePart.c_str()); + out.file = file; + out.line = lineNum; + out.col = 0; + out.message = line.substr(p3); + return true; + } + + static bool isNumber(const std::string& s) { + if (s.empty()) return false; + for (char c : s) { + if (c < '0' || c > '9') return false; + } + return true; + } +}; diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index fda55c9..ee0448f 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -42,6 +42,7 @@ #include "ZoomUtils.h" #include "TerminalPanel.h" #include "WebSocketServer.h" +#include "BuildSystem.h" #include "IncrementalOptimizer.h" #include "ast/Serialization.h" #include "ast/Generator.h" @@ -158,6 +159,10 @@ struct EditorState { int agentPort = 8765; std::vector agentLog; std::map agentMutationPermissions; + BuildSystem::Type buildType = BuildSystem::Type::None; + std::vector buildErrors; + std::string lastBuildOutput; + std::string lastBuildCommand; // Custom editor widget state bool showWhitespace = false; @@ -708,6 +713,7 @@ struct EditorState { loadSettingsFromDisk(); registerCommands(); initAgentServer(); + refreshBuildSystem(); } void initAgentServer() { @@ -749,6 +755,23 @@ struct EditorState { agentLog.push_back(msg); } + void refreshBuildSystem() { + buildType = BuildSystem::detect(workspaceRoot); + } + + int runBuildCommand(const BuildCommand& cmd) { + if (workspaceRoot.empty()) { + terminal.append("[build] No workspace root set.\n"); + return -1; + } + showTerminalPanel = true; + lastBuildCommand = cmd.command; + int code = terminal.runAndCapture(workspaceRoot, cmd.command, lastBuildOutput); + buildErrors = BuildSystem::parseErrors(lastBuildOutput); + outputLog += "[build] " + cmd.label + " => exit " + std::to_string(code) + "\n"; + return code; + } + json buildDiagnosticsJson() const { json out; json lspArr = json::array(); diff --git a/editor/src/TerminalPanel.h b/editor/src/TerminalPanel.h index 0222c3b..b1c63f7 100644 --- a/editor/src/TerminalPanel.h +++ b/editor/src/TerminalPanel.h @@ -62,6 +62,14 @@ public: return code; } + int runAndCapture(const std::string& cwd, const std::string& command, std::string& out) { + out.clear(); + int code = runProcess(cwd, command, out); + output_ += "> " + command + "\n"; + output_ += out; + return code; + } + const std::string& getOutput() const { return output_; } diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 917df47..b0d1ed1 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -111,6 +111,7 @@ int main(int, char**) { state.workspaceRoot = p; state.fileTreeDirty = true; state.projectSearch.setRoot(state.workspaceRoot); + state.refreshBuildSystem(); }); SDL_free(droppedFile); } @@ -248,6 +249,7 @@ int main(int, char**) { state.fileTreeDirty = true; state.projectSearch.setRoot(state.workspaceRoot); lastDialogPath = path; + state.refreshBuildSystem(); } } if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str())) @@ -1694,6 +1696,51 @@ int main(int, char**) { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Build")) { + ImGui::PushFont(uiFont); + ImGui::Text("Detected: %s", BuildSystem::typeName(state.buildType)); + auto cmds = BuildSystem::commandsFor(state.buildType); + if (cmds.empty()) { + ImGui::TextDisabled("(no build system detected)"); + } else { + for (const auto& cmd : cmds) { + if (ImGui::Button(cmd.label.c_str())) { + state.runBuildCommand(cmd); + } + ImGui::SameLine(); + ImGui::TextUnformatted(cmd.command.c_str()); + } + } + + ImGui::Separator(); + ImGui::TextUnformatted("Errors"); + if (state.buildErrors.empty()) { + ImGui::TextDisabled("(none)"); + } else { + for (const auto& err : state.buildErrors) { + std::string label = err.file + ":" + std::to_string(err.line); + if (err.col > 0) label += ":" + std::to_string(err.col); + label += " " + err.message; + if (ImGui::Selectable(label.c_str())) { + std::filesystem::path p(err.file); + std::string path = p.is_absolute() + ? p.string() + : (std::filesystem::path(state.workspaceRoot) / p).string(); + if (state.buffers.hasBuffer(path)) state.switchToBuffer(path); + else state.doOpen(path); + if (state.active()) { + state.jumpTo(state.active(), + std::max(0, err.line - 1), + std::max(0, err.col - 1)); + } + } + } + } + + ImGui::PopFont(); + ImGui::EndTabItem(); + } + // Problems (LSP diagnostics) if (ImGui::BeginTabItem("Problems")) { ImGui::PushFont(monoFont); diff --git a/editor/tests/step126_test.cpp b/editor/tests/step126_test.cpp new file mode 100644 index 0000000..b4572e4 --- /dev/null +++ b/editor/tests/step126_test.cpp @@ -0,0 +1,40 @@ +// Step 126 TDD Test: Build system detection + error parsing +#include "BuildSystem.h" +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + // Parse GCC/Clang style + std::string gcc = "src/main.cpp:12:5: error: something bad"; + auto errs = BuildSystem::parseErrors(gcc); + expect(errs.size() == 1, "parse gcc error", passed, failed); + if (!errs.empty()) { + expect(errs[0].file == "src/main.cpp", "gcc file", passed, failed); + expect(errs[0].line == 12, "gcc line", passed, failed); + expect(errs[0].col == 5, "gcc col", passed, failed); + } + + // Parse MSVC style + std::string msvc = "C:/proj/main.cpp(42): error C2143: syntax error"; + auto errs2 = BuildSystem::parseErrors(msvc); + expect(errs2.size() == 1, "parse msvc error", passed, failed); + if (!errs2.empty()) { + expect(errs2[0].file.find("main.cpp") != std::string::npos, "msvc file", passed, failed); + expect(errs2[0].line == 42, "msvc line", passed, failed); + } + + std::cout << "\n=== Step 126 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index 0d34df0..311a941 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -433,7 +433,7 @@ External tool integration and agent connectivity. and results. Agents can be granted/revoked mutation permissions. *Modifies:* `main.cpp` -- [ ] **Step 126: Build system integration** +- [x] **Step 126: Build system integration** Detect project type (CMakeLists.txt, setup.py, Cargo.toml, package.json, Makefile, go.mod) and offer build commands. Build output in Terminal panel. Error parsing: click compiler errors to jump to source location.