diff --git a/PROGRESS.md b/PROGRESS.md index 1639075..c8d5590 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -382,7 +382,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi ## What's Next -Sprint 5 in progress. Step 140 (constructive coding tests) done. Next: Step 141 (Emacs daemon with user config). +Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step 142 (Elisp package browser). --- @@ -486,5 +486,6 @@ Sprint 5 in progress. Step 140 (constructive coding tests) done. Next: Step 141 | 2026-02-09 | Codex | Step 138: Type-aware C++ generation via library call mappings. 2/2 tests pass. | | 2026-02-09 | Codex | Step 139: Library compatibility matrix with default mappings. 3/3 tests pass. | | 2026-02-09 | Codex | Step 140: Constructive coding integration tests covering imports, policy modes, composition, and type-aware generation. 7/7 tests pass. | +| 2026-02-09 | Codex | Step 141: Emacs daemon startup uses user config path with init logging and diagnostics. 4/4 tests pass. | | 2026-02-09 | Codex | Added FEATURE_REQUESTS.md to track security vulnerability awareness and semantic library annotations for future sprints. | | 2026-02-09 | Codex | Added LLM tooling + MCP bridge feature request to FEATURE_REQUESTS.md. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index fbe5b18..102d593 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -778,6 +778,10 @@ add_executable(step140_test tests/step140_test.cpp) target_include_directories(step140_test PRIVATE src) target_link_libraries(step140_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step141_test tests/step141_test.cpp) +target_include_directories(step141_test PRIVATE src) +target_link_libraries(step141_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/CompositionPanel.h b/editor/src/CompositionPanel.h index 1f21fc3..33fa7d7 100644 --- a/editor/src/CompositionPanel.h +++ b/editor/src/CompositionPanel.h @@ -2,6 +2,7 @@ #include "imgui.h" #include "CompositionBuilder.h" #include +#include #include struct CompositionPanelState { @@ -10,6 +11,7 @@ struct CompositionPanelState { char functionName[64] = "compose_pipeline"; std::string inputVar = "x"; int selected = -1; + std::vector steps; }; static bool renderCompositionPanel(CompositionPanelState& state, diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 6561bb9..ca2d770 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -51,6 +51,7 @@ #include "AgentLibraryPolicy.h" #include "CompositionPanel.h" #include "IncrementalOptimizer.h" +#include "EmacsIntegration.h" #include "ast/Serialization.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -72,6 +73,7 @@ #include #include #include +#include using json = nlohmann::json; @@ -136,6 +138,7 @@ struct EditorState { BufferManager buffers; std::map> bufferStates; BufferState* activeBuffer = nullptr; + EmacsConnection emacs; // Find/Replace state bool showFind = false; @@ -224,6 +227,7 @@ struct EditorState { bool analysisPending = false; double analysisLastChange = 0.0; std::vector whetstoneDiagnostics; + std::vector emacsDiagnostics; SettingsManager settings; bool showLspSettings = false; bool showSettingsPanel = false; @@ -367,11 +371,11 @@ struct EditorState { void insertTextAtCursor(const std::string& text) { if (!active() || text.empty()) return; - bool changed = false; - active()->widget.insertText(active()->editBuf, text, changed); - if (changed) { - onTextChanged(); - } + int cursor = active()->widget.getCursor(); + cursor = std::max(0, std::min(cursor, (int)active()->editBuf.size())); + active()->editBuf.insert(cursor, text); + active()->widget.setCursor(cursor + (int)text.size()); + onTextChanged(); } void ensureImportForSymbol(const std::string& library, const std::string& symbol) { @@ -779,10 +783,55 @@ struct EditorState { loadRecentFiles(); loadSettingsFromDisk(); registerCommands(); + startEmacsDaemonFromSettings(); initAgentServer(); refreshBuildSystem(); } + void startEmacsDaemonFromSettings() { + emacsDiagnostics.clear(); + std::string initPath = resolveEmacsInitPath(); + std::string log; + bool ok = emacs.startDaemonWithConfig(initPath, log); + if (!log.empty()) { + outputLog += "[emacs] " + log + "\n"; + } + if (!ok) { + EditorDiagnostic d; + d.uri = "emacs://init"; + d.severity = 1; + d.message = "[Emacs] " + emacs.getLastError(); + d.source = "EmacsDaemon"; + emacsDiagnostics.push_back(d); + return; + } + if (!log.empty()) { + addEmacsLogDiagnostics(log); + } + } + + std::string resolveEmacsInitPath() const { + return ::resolveEmacsInitPath(settings.getEmacsConfigPath()); + } + + void addEmacsLogDiagnostics(const std::string& log) { + std::istringstream ss(log); + std::string line; + while (std::getline(ss, line)) { + std::string lower = line; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + if (lower.find("error") != std::string::npos) { + EditorDiagnostic d; + d.uri = "emacs://init"; + d.severity = 1; + d.message = "[Emacs] " + line; + d.source = "EmacsDaemon"; + emacsDiagnostics.push_back(d); + } + } + } + void initAgentServer() { auto transport = std::make_unique(); agentTransport = transport.get(); @@ -952,8 +1001,20 @@ struct EditorState { }); } + json emacsArr = json::array(); + for (const auto& d : emacsDiagnostics) { + emacsArr.push_back({ + {"uri", d.uri}, + {"message", d.message}, + {"severity", d.severity}, + {"line", d.line}, + {"character", d.character} + }); + } + out["lsp"] = lspArr; out["whetstone"] = whetArr; + out["emacs"] = emacsArr; return out; } diff --git a/editor/src/EmacsIntegration.h b/editor/src/EmacsIntegration.h index ce967a5..282e422 100644 --- a/editor/src/EmacsIntegration.h +++ b/editor/src/EmacsIntegration.h @@ -12,6 +12,7 @@ #include #include #include +#include // --------------------------------------------------------------------------- // ElispCommandBuilder — produces valid Elisp command strings @@ -79,19 +80,37 @@ public: // Start Emacs daemon bool startDaemon() { - std::string cmd = emacsPath_ + " --daemon 2>&1"; - int result = runCommand(cmd); + std::string output; + return startDaemonWithConfig("", output); + } + + // Start Emacs daemon with an explicit init file path + virtual bool startDaemonWithConfig(const std::string& initFilePath, std::string& outLog) { + outLog.clear(); + std::string cmd = emacsPath_ + " --daemon"; + if (!initFilePath.empty()) { + cmd += " --load \"" + initFilePath + "\""; + } + cmd += " 2>&1"; + FILE* pipe = openPipe(cmd.c_str(), "r"); + if (!pipe) { + lastError_ = "Failed to start Emacs daemon"; + return false; + } + char buf[512]; + while (fgets(buf, sizeof(buf), pipe)) { + outLog += buf; + } + int result = closePipe(pipe); if (result == 0) { daemonRunning_ = true; return true; } - // Even if command "fails", check if daemon is actually alive - // (Emacs daemon may return non-zero but still be running) if (isDaemonAlive()) { daemonRunning_ = true; return true; } - lastError_ = "Failed to start Emacs daemon"; + lastError_ = outLog.empty() ? "Failed to start Emacs daemon" : outLog; return false; } @@ -196,6 +215,22 @@ private: std::string lastError_; }; +// Resolve emacs config path to init.el +static inline std::string resolveEmacsInitPath(const std::string& configPath) { + if (configPath.empty()) return ""; + std::filesystem::path p(configPath); + if (std::filesystem::exists(p)) { + if (std::filesystem::is_directory(p)) { + return (p / "init.el").string(); + } + return p.string(); + } + if (p.extension().string().empty()) { + return (p / "init.el").string(); + } + return p.string(); +} + // --------------------------------------------------------------------------- // MockEmacsConnection — for testing without a real Emacs installation // --------------------------------------------------------------------------- @@ -211,6 +246,13 @@ public: return true; } + bool startDaemonWithConfig(const std::string& initFilePath, std::string& outLog) { + lastInitPath_ = initFilePath; + outLog = "Loaded " + initFilePath; + daemonRunning_ = true; + return true; + } + bool isDaemonAlive() const { return daemonRunning_; } std::string sendCommand(const std::string& elispCommand) { @@ -241,8 +283,10 @@ public: // Test helper: get the last command sent std::string getLastSentCommand() const { return lastSentCommand_; } + std::string getLastInitPath() const { return lastInitPath_; } private: std::string lastError_; std::string lastSentCommand_; + std::string lastInitPath_; }; diff --git a/editor/src/ImportManager.h b/editor/src/ImportManager.h index 4f5158f..6f3d516 100644 --- a/editor/src/ImportManager.h +++ b/editor/src/ImportManager.h @@ -16,7 +16,7 @@ struct ImportEditResult { bool changed = false; }; -static inline std::vector splitLines(const std::string& text) { +static inline std::vector importSplitLines(const std::string& text) { std::vector lines; std::stringstream ss(text); std::string line; @@ -81,7 +81,7 @@ static inline ImportEditResult ensureImportPython(const std::string& text, line = "from " + library + " import " + symbol; } - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::vector imports; std::vector rest; for (const auto& l : lines) { @@ -108,7 +108,7 @@ static inline ImportEditResult ensureImportJs(const std::string& text, if (dot != std::string::npos) name = name.substr(dot + 1); std::string line = "import { " + name + " } from '" + library + "';"; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::vector imports; std::vector rest; for (const auto& l : lines) { @@ -134,7 +134,7 @@ static inline ImportEditResult ensureImportRust(const std::string& text, if (symbol.find("::") != std::string::npos) line = "use " + symbol + ";"; else line = "use " + library + "::" + symbol + ";"; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::vector imports; std::vector rest; for (const auto& l : lines) { @@ -157,7 +157,7 @@ static inline ImportEditResult ensureImportGo(const std::string& text, ImportEditResult out{ text, false }; std::string path = "\"" + library + "\""; std::string line = "import " + path; - auto lines = splitLines(text); + auto lines = importSplitLines(text); int pkgLine = -1; int importStart = -1; int importEnd = -1; @@ -228,7 +228,7 @@ static inline ImportEditResult ensureImportElisp(const std::string& text, const std::string& library) { ImportEditResult out{ text, false }; std::string line = "(require '" + library + ")"; - auto lines = splitLines(text); + auto lines = importSplitLines(text); for (const auto& l : lines) { if (trimStr(l) == line) return out; } @@ -244,7 +244,7 @@ static inline ImportEditResult ensureImportCpp(const std::string& text, const std::string& library) { ImportEditResult out{ text, false }; std::string line = "#include <" + library + ">"; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::vector imports; std::vector rest; for (const auto& l : lines) { @@ -281,7 +281,7 @@ static inline ImportEditResult ensureImport(const std::string& text, static inline std::vector findUnusedImportsPython(const std::string& text) { std::vector issues; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::string body; for (const auto& l : lines) { std::string t = trimStr(l); @@ -309,7 +309,7 @@ static inline std::vector findUnusedImportsPython(const std::string static inline std::vector findUnusedImportsJs(const std::string& text) { std::vector issues; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::string body; for (const auto& l : lines) { std::string t = trimStr(l); @@ -341,7 +341,7 @@ static inline std::vector findUnusedImportsJs(const std::string& te static inline std::vector findUnusedImportsRust(const std::string& text) { std::vector issues; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::string body; for (const auto& l : lines) { std::string t = trimStr(l); @@ -365,7 +365,7 @@ static inline std::vector findUnusedImportsRust(const std::string& static inline std::vector findUnusedImportsGo(const std::string& text) { std::vector issues; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::string body; for (const auto& l : lines) { std::string t = trimStr(l); @@ -389,7 +389,7 @@ static inline std::vector findUnusedImportsGo(const std::string& te static inline std::vector findUnusedImportsElisp(const std::string& text) { std::vector issues; - auto lines = splitLines(text); + auto lines = importSplitLines(text); std::string body; for (const auto& l : lines) { std::string t = trimStr(l); diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h index f4c6c63..28b8f11 100644 --- a/editor/src/SettingsManager.h +++ b/editor/src/SettingsManager.h @@ -169,9 +169,9 @@ private: const char* home = std::getenv("USERPROFILE"); if (!home) home = std::getenv("HOME"); if (home) { - emacsConfigPath_ = (std::filesystem::path(home) / ".emacs.d").string(); + emacsConfigPath_ = (std::filesystem::path(home) / ".emacs.d" / "init.el").string(); } else { - emacsConfigPath_ = ".emacs.d"; + emacsConfigPath_ = ".emacs.d/init.el"; } } diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 4d0b2f5..cf8a00b 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -698,6 +698,7 @@ int main(int, char**) { ImGui::Begin("Settings", &state.showSettingsPanel); ImGui::PushFont(uiFont); bool settingsChanged = false; + bool emacsConfigChanged = false; ImGuiIO& io = ImGui::GetIO(); int fontSize = state.settings.getFontSize(); @@ -776,6 +777,7 @@ int main(int, char**) { ImGui::SetNextItemWidth(320); if (InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable())) { settingsChanged = true; + emacsConfigChanged = true; } if (ImGui::CollapsingHeader("LSP Servers", ImGuiTreeNodeFlags_DefaultOpen)) { @@ -799,6 +801,9 @@ int main(int, char**) { if (settingsChanged) { state.saveSettingsToDisk(); } + if (emacsConfigChanged) { + state.startEmacsDaemonFromSettings(); + } ImGui::PopFont(); ImGui::End(); } @@ -1848,7 +1853,8 @@ int main(int, char**) { ImGui::BeginChild("##problemsScroll", ImVec2(0, 0), false); auto diags = state.lsp ? state.lsp->getDiagnostics() : std::vector{}; const auto& whetDiags = state.whetstoneDiagnostics; - if (diags.empty() && whetDiags.empty()) { + const auto& emacsDiags = state.emacsDiagnostics; + if (diags.empty() && whetDiags.empty() && emacsDiags.empty()) { ImGui::TextDisabled("(no diagnostics)"); } else { for (const auto& d : diags) { @@ -1888,6 +1894,12 @@ int main(int, char**) { } } } + for (const auto& d : emacsDiags) { + const char* sev = "Error"; + std::string label = "[" + std::string(sev) + "] " + d.message + + " (" + d.uri + ")"; + ImGui::TextUnformatted(label.c_str()); + } } ImGui::EndChild(); ImGui::PopFont(); diff --git a/editor/tests/step141_test.cpp b/editor/tests/step141_test.cpp new file mode 100644 index 0000000..e768d8b --- /dev/null +++ b/editor/tests/step141_test.cpp @@ -0,0 +1,38 @@ +// Step 141 TDD Test: Emacs daemon with user config path +#include "EmacsIntegration.h" +#include +#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; + + // resolveEmacsInitPath uses directory -> init.el + std::filesystem::path dir = std::filesystem::current_path() / "emacs_cfg_tmp"; + std::filesystem::create_directories(dir); + std::string resolved = resolveEmacsInitPath(dir.string()); + expect(resolved.find("init.el") != std::string::npos, "resolve init.el", passed, failed); + + // Mock emacs daemon startup with init path + MockEmacsConnection mock; + std::string log; + bool ok = mock.startDaemonWithConfig("C:/init.el", log); + expect(ok, "mock start daemon ok", passed, failed); + expect(mock.getLastInitPath() == "C:/init.el", "init path passed", passed, failed); + expect(log.find("Loaded") != std::string::npos, "startup log returned", passed, failed); + + std::filesystem::remove_all(dir); + + std::cout << "\n=== Step 141 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index 55a22ff..4ea1f21 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -172,7 +172,7 @@ what's already available. For Emacs users: respect their init.el, load their packages, expose Emacs functionality through the Whetstone UI. -- [ ] **Step 141: Emacs daemon with user config** +- [x] **Step 141: Emacs daemon with user config** Modify the Orchestrator's Emacs daemon to start with the user's init file. Setting: "Emacs Config Path" (defaults to `~/.emacs.d/init.el`). Daemon loads user's packages (MELPA, ELPA, straight.el, use-package).