Step 126: build system detection and error jump
This commit is contained in:
@@ -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)
|
||||
|
||||
131
editor/src/BuildSystem.h
Normal file
131
editor/src/BuildSystem.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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<std::string> agentLog;
|
||||
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
|
||||
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();
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
40
editor/tests/step126_test.cpp
Normal file
40
editor/tests/step126_test.cpp
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user