#pragma once // Step 56: Robust Emacs command integration // // ElispCommandBuilder: builds valid Elisp expressions with proper escaping. // EmacsConnection: manages Emacs daemon lifecycle (start, health check, // command execution, auto-restart). // // The connection talks to emacsclient --eval for command execution. // On Windows, Emacs must be on PATH or configured via setEmacsPath(). #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // ElispCommandBuilder — produces valid Elisp command strings // --------------------------------------------------------------------------- class ElispCommandBuilder { public: // Build an Elisp expression to open a file static std::string findFile(const std::string& path) { return "(find-file \"" + escapeString(path) + "\")"; } // Build an Elisp expression to save current buffer static std::string saveBuffer() { return "(save-buffer)"; } // Build an Elisp expression to evaluate arbitrary Elisp static std::string eval(const std::string& elisp) { return "(eval (read \"" + escapeString(elisp) + "\"))"; } // Escape special characters in strings for Elisp embedding static std::string escapeString(const std::string& str) { std::string result; result.reserve(str.size() + 16); for (char c : str) { switch (c) { case '\\': result += "\\\\"; break; case '"': result += "\\\""; break; case '\n': result += "\\n"; break; case '\t': result += "\\t"; break; case '\r': result += "\\r"; break; default: result += c; break; } } return result; } // Build an Elisp expression to set a variable static std::string setVariable(const std::string& name, const std::string& value) { return "(setq " + name + " \"" + escapeString(value) + "\")"; } // Build a progn block from multiple expressions static std::string progn(const std::vector& exprs) { std::string result = "(progn"; for (const auto& e : exprs) { result += " " + e; } result += ")"; return result; } // Build an Elisp expression to list functions matching a prefix/regex static std::string aproposFunctions(const std::string& prefixRegex) { std::string pattern = prefixRegex; return "(mapconcat #'symbol-name (apropos-internal \"" + escapeString(pattern) + "\" 'fboundp) \"\\n\")"; } // Build an Elisp expression to describe a function (signature + doc) static std::string describeFunction(const std::string& name) { std::string sym = escapeString(name); return "(let* ((sym '" + sym + ") " "(args (if (fboundp 'help-function-arglist) " " (help-function-arglist sym t) nil)) " "(doc (or (documentation sym t) \"\"))) " "(concat (if args (prin1-to-string args) \"\") \"\\n\" doc))"; } // Build an Elisp expression to resolve a key binding to a command name static std::string keyBinding(const std::string& keySequence) { return "(let ((cmd (key-binding (kbd \"" + escapeString(keySequence) + "\")))) " "(if cmd (symbol-name cmd) \"\"))"; } // Build an Elisp expression to call a command interactively static std::string callInteractive(const std::string& command) { return "(call-interactively '" + escapeString(command) + ")"; } // Build an Elisp expression to run an extended command by name static std::string executeExtendedCommand(const std::string& command) { std::string sym = escapeString(command); return "(if (commandp '" + sym + ") " "(call-interactively '" + sym + ") " "(concat \"Unknown command: \" \"" + sym + "\"))"; } // Build an Elisp expression for the current mode line static std::string modeLine() { return "(format-mode-line mode-line-format)"; } // Build an Elisp expression to read a buffer for a given file static std::string bufferStringForFile(const std::string& path) { return "(with-current-buffer (find-file-noselect \"" + escapeString(path) + "\") (buffer-string))"; } // Build an Elisp expression to replace a buffer's contents and save static std::string replaceBufferForFile(const std::string& path, const std::string& text) { return "(with-current-buffer (find-file-noselect \"" + escapeString(path) + "\") " "(erase-buffer) " "(insert \"" + escapeString(text) + "\") " "(save-buffer))"; } }; // --------------------------------------------------------------------------- // EmacsConnection — manages Emacs daemon lifecycle // --------------------------------------------------------------------------- class EmacsConnection { public: EmacsConnection() = default; // Set custom path to emacs/emacsclient executables void setEmacsPath(const std::string& path) { emacsPath_ = path; } void setEmacsclientPath(const std::string& path) { emacsclientPath_ = path; } // Start Emacs daemon bool startDaemon() { 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 + "\""; } #ifdef _WIN32 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 || isDaemonAlive()) { daemonRunning_ = true; return true; } lastError_ = outLog.empty() ? "Failed to start Emacs daemon" : outLog; return false; #else // Launch daemon in the background to avoid blocking editor startup. cmd += " >/tmp/whetstone-emacs-daemon.log 2>&1 &"; int result = runCommand(cmd); if (result != 0) { lastError_ = "Failed to start Emacs daemon"; return false; } outLog = "Starting Emacs daemon in background."; for (int i = 0; i < 15; ++i) { if (isDaemonAlive()) { daemonRunning_ = true; return true; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } lastError_ = "Emacs daemon did not become ready in time"; return false; #endif } // Check if daemon is running bool isDaemonAlive() const { // Try to evaluate a simple expression std::string cmd = emacsclientPath_ + " --eval \"(+ 1 1)\" 2>&1"; FILE* pipe = openPipe(cmd.c_str(), "r"); if (!pipe) return false; char buf[256]; std::string output; while (fgets(buf, sizeof(buf), pipe)) { output += buf; } int ret = closePipe(pipe); // If emacsclient returns 0 and outputs "2", daemon is alive return (ret == 0 && output.find("2") != std::string::npos); } // Send command to Emacs and get result virtual std::string sendCommand(const std::string& elispCommand) { lastError_.clear(); if (!daemonRunning_ && !isDaemonAlive()) { lastError_ = "Emacs daemon is not running"; return ""; } std::string cmd = emacsclientPath_ + " --eval \"" + ElispCommandBuilder::escapeString(elispCommand) + "\" 2>&1"; FILE* pipe = openPipe(cmd.c_str(), "r"); if (!pipe) { lastError_ = "Failed to execute emacsclient"; return ""; } char buf[4096]; std::string output; while (fgets(buf, sizeof(buf), pipe)) { output += buf; } int ret = closePipe(pipe); // Trim trailing newline while (!output.empty() && (output.back() == '\n' || output.back() == '\r')) output.pop_back(); if (ret != 0) { lastError_ = output.empty() ? "emacsclient returned error" : output; // Check if daemon died if (!isDaemonAlive()) { daemonRunning_ = false; lastError_ = "Emacs daemon crashed"; } } return output; } std::string getBufferText(const std::string& path) { return sendCommand(ElispCommandBuilder::bufferStringForFile(path)); } bool setBufferText(const std::string& path, const std::string& text) { std::string result = sendCommand(ElispCommandBuilder::replaceBufferForFile(path, text)); if (!lastError_.empty() && result == "error") return false; return true; } bool openFileInEmacsFrame(const std::string& path) { lastError_.clear(); std::string cmd = emacsclientPath_ + " -c -n \"" + path + "\""; int code = runCommand(cmd); if (code != 0) { lastError_ = "emacsclient failed to open frame"; return false; } return true; } // Auto-restart daemon if it died bool ensureDaemon() { if (daemonRunning_ && isDaemonAlive()) return true; daemonRunning_ = false; return startDaemon(); } // Get the last error std::string getLastError() const { return lastError_; } // Check if we believe the daemon is running bool isConnected() const { return daemonRunning_; } protected: // Wrappers for popen/pclose to allow testing without real Emacs virtual FILE* openPipe(const char* cmd, const char* mode) const { #ifdef _WIN32 return _popen(cmd, mode); #else return popen(cmd, mode); #endif } virtual int closePipe(FILE* pipe) const { #ifdef _WIN32 return _pclose(pipe); #else return pclose(pipe); #endif } virtual int runCommand(const std::string& cmd) const { return std::system(cmd.c_str()); } bool daemonRunning_ = false; private: std::string emacsPath_ = "emacs"; std::string emacsclientPath_ = "emacsclient"; 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 // --------------------------------------------------------------------------- class MockEmacsConnection : public EmacsConnection { public: MockEmacsConnection() { // Simulate daemon as running daemonRunning_ = true; } bool startDaemon() { daemonRunning_ = true; 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) override { lastSentCommand_ = elispCommand; // Simulate responses for known commands if (elispCommand.find("buffer-string") != std::string::npos) { return "mock-buffer"; } if (elispCommand.find("erase-buffer") != std::string::npos) { return "t"; } if (elispCommand.find("find-file") != std::string::npos) { return "t"; // success } if (elispCommand.find("save-buffer") != std::string::npos) { return "t"; } if (elispCommand == "(+ 1 1)") { return "2"; } if (elispCommand.find("package-alist") != std::string::npos) { return "use-package|2.4.1|Declarative package configuration\n" "cl-lib|1.0|Common Lisp extensions for Emacs"; } if (elispCommand.find("package-archive-contents") != std::string::npos) { return "magit|3.4.0|Git porcelain inside Emacs\n" "projectile|2.7.0|Project interaction library"; } if (elispCommand.find("(require '") != std::string::npos) { return "t"; } if (elispCommand.find("apropos-internal") != std::string::npos) { if (elispCommand.find("use-package") != std::string::npos) { return "use-package\nuse-package-alias"; } if (elispCommand.find("projectile") != std::string::npos) { return "projectile-find-file\nprojectile-switch-project"; } if (elispCommand.find("cl-") != std::string::npos) { return "cl-loop\ncl-mapcar"; } return ""; } if (elispCommand.find("help-function-arglist") != std::string::npos) { if (elispCommand.find("use-package") != std::string::npos) { return "(name &rest args)\nConfigure package via use-package."; } if (elispCommand.find("projectile") != std::string::npos) { return "(project)\nProjectile project switching."; } return "()\nEmacs function."; } if (elispCommand.find("key-binding") != std::string::npos) { if (elispCommand.find("C-x C-s") != std::string::npos) { return "save-buffer"; } if (elispCommand.find("C-x C-f") != std::string::npos) { return "find-file"; } return ""; } if (elispCommand.find("call-interactively") != std::string::npos) { return "t"; } if (elispCommand.find("format-mode-line") != std::string::npos) { return "Emacs: Fundamental (Whetstone)"; } // Unknown command — simulate error lastError_ = "void-function"; return "error"; } bool ensureDaemon() { daemonRunning_ = true; return true; } std::string getLastError() const { return lastError_; } // Test helper: get the last command sent std::string getLastSentCommand() const { return lastSentCommand_; } std::string getLastInitPath() const { return lastInitPath_; } std::string getLastRunCommand() const { return lastRunCommand_; } protected: int runCommand(const std::string& cmd) const override { lastRunCommand_ = cmd; return 0; } private: std::string lastError_; std::string lastSentCommand_; std::string lastInitPath_; mutable std::string lastRunCommand_; };