diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2fafc66..42f5d79 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -693,6 +693,10 @@ add_executable(step122_test tests/step122_test.cpp) target_include_directories(step122_test PRIVATE src) target_link_libraries(step122_test PRIVATE imgui::imgui) +add_executable(step123_test tests/step123_test.cpp) +target_include_directories(step123_test PRIVATE src) +target_link_libraries(step123_test PRIVATE imgui::imgui) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 9fb4236..2f2ee51 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -146,6 +146,10 @@ struct EditorState { std::string outputLog; bool showTerminalPanel = false; TerminalPanel terminal; + bool runInProgress = false; + bool hasRunResult = false; + int lastRunExitCode = 0; + std::string lastRunCommand; // Custom editor widget state bool showWhitespace = false; @@ -518,10 +522,10 @@ struct EditorState { info.mode = bufferModeToString(it->second->bufferMode); project.buffers.push_back(std::move(info)); } - if (isStructured()) { + if (isStructured() && active()) { syncOrchestratorFromActive(); - if (orchestrator.getAST()) { - project.ast = cloneModule(orchestrator.getAST()); + if (active()->orchestrator.getAST()) { + project.ast = cloneModule(active()->orchestrator.getAST()); } } @@ -877,6 +881,10 @@ struct EditorState { refactorNameA[0] = '\0'; refactorNameB[0] = '\0'; }); + registerCommand("build.run", "Build: Run", keys.getBinding("build.run").toString(), + [this]() { runActiveFile(false); }); + registerCommand("build.build", "Build: Build", keys.getBinding("build.build").toString(), + [this]() { runActiveFile(true); }); } void executeCommand(const std::string& id) { @@ -906,6 +914,70 @@ struct EditorState { active()->highlightsDirty = true; } + std::string buildRunCommand(const std::string& path, + const std::string& language, + bool buildOnly) const { + std::filesystem::path p(path); + std::string ext = p.extension().string(); + std::string quoted = "\"" + path + "\""; + if (language == "python" || ext == ".py") { + return buildOnly ? "python -m py_compile " + quoted : "python " + quoted; + } + if (language == "cpp" || ext == ".cpp" || ext == ".cc" || ext == ".cxx") { + std::string out = (p.parent_path() / p.stem()).string(); +#ifdef _WIN32 + out += ".exe"; +#endif + std::string compile = "g++ " + quoted + " -std=c++20 -o \"" + out + "\""; + if (buildOnly) return compile; + return compile + " && \"" + out + "\""; + } + if (language == "rust" || ext == ".rs") { + return buildOnly ? "cargo build" : "cargo run"; + } + if (language == "go" || ext == ".go") { + return buildOnly ? "go build " + quoted : "go run " + quoted; + } + if (language == "javascript" || ext == ".js") { + return buildOnly ? "node --check " + quoted : "node " + quoted; + } + if (language == "typescript" || ext == ".ts") { + return buildOnly ? "tsc " + quoted : "node " + quoted; + } + return ""; + } + + bool runActiveFile(bool buildOnly) { + if (!active()) return false; + if (active()->path.rfind("(untitled", 0) == 0) { + terminal.append("[terminal] Save the file before running.\n"); + showTerminalPanel = true; + return false; + } + if (active()->modified) { + doSave(); + } + std::string cmd = buildRunCommand(active()->path, active()->language, buildOnly); + if (cmd.empty()) { + terminal.append("[terminal] No runner for language: " + active()->language + "\n"); + showTerminalPanel = true; + return false; + } + std::string cwd = workspaceRoot.empty() + ? std::filesystem::path(active()->path).parent_path().string() + : workspaceRoot; + showTerminalPanel = true; + runInProgress = true; + hasRunResult = false; + lastRunCommand = cmd; + int code = terminal.runAndAppend(cwd, cmd); + terminal.append("[exit code: " + std::to_string(code) + "]\n"); + runInProgress = false; + hasRunResult = true; + lastRunExitCode = code; + return code == 0; + } + // Called after editBuf changes (from ImGui input) void onTextChanged() { if (!active()) return; diff --git a/editor/src/TerminalPanel.h b/editor/src/TerminalPanel.h index 208f5d6..0222c3b 100644 --- a/editor/src/TerminalPanel.h +++ b/editor/src/TerminalPanel.h @@ -54,6 +54,14 @@ public: output_ += text; } + int runAndAppend(const std::string& cwd, const std::string& command) { + std::string out; + int code = runProcess(cwd, command, out); + output_ += "> " + command + "\n"; + output_ += out; + return code; + } + const std::string& getOutput() const { return output_; } @@ -107,29 +115,37 @@ private: void runCommand(const std::string& cwd) { if (commandBuf_.empty()) return; - std::string fullCmd = buildCommandLine(cwd, commandBuf_); + std::string out; + int code = runProcess(cwd, commandBuf_, out); + output_ += "> " + commandBuf_ + "\n"; + output_ += out; + output_ += "[exit code: " + std::to_string(code) + "]\n"; + } + + static int runProcess(const std::string& cwd, const std::string& command, std::string& out) { + std::string fullCmd = buildCommandLine(cwd, command); #ifdef _WIN32 FILE* pipe = _popen(fullCmd.c_str(), "r"); #else FILE* pipe = popen(fullCmd.c_str(), "r"); #endif if (!pipe) { - output_ += "[terminal] Failed to launch command.\n"; - return; + out += "[terminal] Failed to launch command.\n"; + return -1; } - output_ += "> " + commandBuf_ + "\n"; char buffer[4096]; while (!feof(pipe)) { if (fgets(buffer, sizeof(buffer), pipe) != nullptr) { - output_ += buffer; + out += buffer; } } #ifdef _WIN32 - _pclose(pipe); + int code = _pclose(pipe); #else - pclose(pipe); + int code = pclose(pipe); #endif + return code; } static ImVec4 ansiColor(int code, const ImVec4& base) { diff --git a/editor/src/main.cpp b/editor/src/main.cpp index e7e1b58..6249cdb 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -172,6 +172,8 @@ int main(int, char**) { std::string lang = state.active() ? state.active()->language : "python"; state.createBuffer(state.makeUntitledName(), "", lang); } + else if (action == "build.run") state.runActiveFile(false); + else if (action == "build.build") state.runActiveFile(true); else if (action == "view.zoomIn") applyFontSize(state.settings.getFontSize() + 1); else if (action == "view.zoomOut") applyFontSize(state.settings.getFontSize() - 1); } @@ -341,6 +343,15 @@ int main(int, char**) { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Build")) { + if (ImGui::MenuItem("Run", state.keys.getBinding("build.run").toString().c_str())) { + state.runActiveFile(false); + } + if (ImGui::MenuItem("Build", state.keys.getBinding("build.build").toString().c_str())) { + state.runActiveFile(true); + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Language")) { if (ImGui::MenuItem("Python", nullptr, state.active() && state.active()->language == "python")) state.setLanguage("python"); @@ -413,6 +424,20 @@ int main(int, char**) { ImGui::EndPopup(); } if (!canProject) ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(12.0f, 0.0f)); + ImGui::SameLine(); + bool canRunFile = state.active() && !state.active()->readOnly; + if (!canRunFile) ImGui::BeginDisabled(); + if (ImGui::Button("Run >")) { + state.runActiveFile(false); + } + ImGui::SameLine(); + if (ImGui::Button("Build")) { + state.runActiveFile(true); + } + if (!canRunFile) ImGui::EndDisabled(); ImGui::EndMainMenuBar(); } @@ -2219,6 +2244,15 @@ int main(int, char**) { ImGui::Text("Undo: %d", state.active() ? state.active()->undoDepth : 0); ImGui::SameLine(0, 30); + if (state.runInProgress) { + ImGui::Text("Run: Running..."); + } else if (state.hasRunResult) { + ImGui::Text("Run: Exit %d", state.lastRunExitCode); + } else { + ImGui::Text("Run: -"); + } + ImGui::SameLine(0, 30); + // Modified indicator if (state.active() && state.active()->modified) ImGui::Text("Modified"); diff --git a/editor/tests/step123_test.cpp b/editor/tests/step123_test.cpp new file mode 100644 index 0000000..bb4af90 --- /dev/null +++ b/editor/tests/step123_test.cpp @@ -0,0 +1,39 @@ +// Step 123 TDD Test: Run/build command selection +#include "EditorUtils.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; + + EditorState state; + std::string py = state.buildRunCommand("C:\\work\\script.py", "python", false); + expect(py.find("python") != std::string::npos, "python runner", passed, failed); + expect(py.find("script.py") != std::string::npos, "python path", passed, failed); + + std::string cppRun = state.buildRunCommand("C:\\work\\main.cpp", "cpp", false); + expect(cppRun.find("g++") != std::string::npos, "cpp compiler", passed, failed); + expect(cppRun.find("&&") != std::string::npos, "cpp run chain", passed, failed); +#ifdef _WIN32 + expect(cppRun.find(".exe") != std::string::npos, "cpp exe suffix", passed, failed); +#endif + + std::string cppBuild = state.buildRunCommand("C:\\work\\main.cpp", "cpp", true); + expect(cppBuild.find("&&") == std::string::npos, "cpp build only", passed, failed); + + std::string none = state.buildRunCommand("C:\\work\\main.unknown", "unknown", false); + expect(none.empty(), "unsupported language", passed, failed); + + std::cout << "\n=== Step 123 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index 028ddf9..ec9a7b6 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -413,7 +413,7 @@ External tool integration and agent connectivity. workspace root. Scrollable output with ANSI color support. *New:* `TerminalPanel.h` -- [ ] **Step 123: Run code button** +- [x] **Step 123: Run code button** Toolbar button (play icon) to run the current file. Auto-detects runner: Python: `python file.py`, C++: compile and run, Rust: `cargo run`, Go: `go run`, JS: `node file.js`. Output goes to Terminal panel.