Step 126: build system detection and error jump

This commit is contained in:
Bill
2026-02-09 15:31:15 -07:00
parent 6a31d24b72
commit da8212746c
7 changed files with 254 additions and 1 deletions

View File

@@ -716,6 +716,10 @@ target_link_libraries(step125_integration_test PRIVATE
tree_sitter_cpp tree_sitter_cpp
tree_sitter_elisp) 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(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED) find_package(glad CONFIG REQUIRED)

131
editor/src/BuildSystem.h Normal file
View File

@@ -0,0 +1,131 @@
#pragma once
#include <string>
#include <vector>
#include <filesystem>
#include <sstream>
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<BuildCommand> 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<BuildError> parseErrors(const std::string& output) {
std::vector<BuildError> 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;
}
};

View File

@@ -42,6 +42,7 @@
#include "ZoomUtils.h" #include "ZoomUtils.h"
#include "TerminalPanel.h" #include "TerminalPanel.h"
#include "WebSocketServer.h" #include "WebSocketServer.h"
#include "BuildSystem.h"
#include "IncrementalOptimizer.h" #include "IncrementalOptimizer.h"
#include "ast/Serialization.h" #include "ast/Serialization.h"
#include "ast/Generator.h" #include "ast/Generator.h"
@@ -158,6 +159,10 @@ struct EditorState {
int agentPort = 8765; int agentPort = 8765;
std::vector<std::string> agentLog; std::vector<std::string> agentLog;
std::map<std::string, bool> agentMutationPermissions; std::map<std::string, bool> agentMutationPermissions;
BuildSystem::Type buildType = BuildSystem::Type::None;
std::vector<BuildError> buildErrors;
std::string lastBuildOutput;
std::string lastBuildCommand;
// Custom editor widget state // Custom editor widget state
bool showWhitespace = false; bool showWhitespace = false;
@@ -708,6 +713,7 @@ struct EditorState {
loadSettingsFromDisk(); loadSettingsFromDisk();
registerCommands(); registerCommands();
initAgentServer(); initAgentServer();
refreshBuildSystem();
} }
void initAgentServer() { void initAgentServer() {
@@ -749,6 +755,23 @@ struct EditorState {
agentLog.push_back(msg); 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 buildDiagnosticsJson() const {
json out; json out;
json lspArr = json::array(); json lspArr = json::array();

View File

@@ -62,6 +62,14 @@ public:
return code; 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 { const std::string& getOutput() const {
return output_; return output_;
} }

View File

@@ -111,6 +111,7 @@ int main(int, char**) {
state.workspaceRoot = p; state.workspaceRoot = p;
state.fileTreeDirty = true; state.fileTreeDirty = true;
state.projectSearch.setRoot(state.workspaceRoot); state.projectSearch.setRoot(state.workspaceRoot);
state.refreshBuildSystem();
}); });
SDL_free(droppedFile); SDL_free(droppedFile);
} }
@@ -248,6 +249,7 @@ int main(int, char**) {
state.fileTreeDirty = true; state.fileTreeDirty = true;
state.projectSearch.setRoot(state.workspaceRoot); state.projectSearch.setRoot(state.workspaceRoot);
lastDialogPath = path; lastDialogPath = path;
state.refreshBuildSystem();
} }
} }
if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str())) if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str()))
@@ -1694,6 +1696,51 @@ int main(int, char**) {
ImGui::EndTabItem(); 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) // Problems (LSP diagnostics)
if (ImGui::BeginTabItem("Problems")) { if (ImGui::BeginTabItem("Problems")) {
ImGui::PushFont(monoFont); ImGui::PushFont(monoFont);

View File

@@ -0,0 +1,40 @@
// Step 126 TDD Test: Build system detection + error parsing
#include "BuildSystem.h"
#include <iostream>
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;
}

View File

@@ -433,7 +433,7 @@ External tool integration and agent connectivity.
and results. Agents can be granted/revoked mutation permissions. and results. Agents can be granted/revoked mutation permissions.
*Modifies:* `main.cpp` *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, Detect project type (CMakeLists.txt, setup.py, Cargo.toml, package.json,
Makefile, go.mod) and offer build commands. Build output in Terminal panel. Makefile, go.mod) and offer build commands. Build output in Terminal panel.
Error parsing: click compiler errors to jump to source location. Error parsing: click compiler errors to jump to source location.